text/Java

전략 패턴(strategy pattern) 간단 정리

hoonzii 2023. 3. 7. 20:55
반응형

유튜브로 출퇴근하면서 간간히 개발 관련 얘기들을 듣고는 하는데 언젠가 봐야지 했던 디자인패턴 강의

알고리즘에 떠서 (강의하시는 분 말로는 디자인 패턴의 꽃이라 더라…) 코드 한번 쳐보고~ 정리하고자 글을 적습니다.

제목에 써있듯이 간단 정리이기에 자세한 패턴 이야기들은 참고 링크의 글을 읽어주길 바랍니다~

 

 

내가 이해한 전략 패턴 (strategy pattern)

  • 특정 객체의 행위의 변경이 잦을때, 해당 클래스의 행위 함수 구현부를 바꾸는 게 아니라 “행위 클래스”를 구현해서 주입!
  • 어떤 행위인지, 행위 상세 내용에 대해서는 주체 클래스는 알지 못함 (ex. Thread.run(), Sort(New Comparator <Object o1, Object o2> { … }) )
  • 행위의 변경이 필요 시 해당 행위 클래스를 찾아가 행위만 수정
  • 행위의 추가가 필요 시 새로운 행위 클래스를 추가

아래의 예시를 같이 살펴보자.

public class Context {
    public int asksOperation(int a, int b, String operation){
        if(operation.equals("+"))
            return a + b;
        else if(operation.equals("-"))
            return a - b;
        else if(operation.equals("*"))
            return a * b;
    }
}

이런 연산을 도와주는 Context 클래스가 존재할 때 우리는

public class mainClass {
    public static void main(String[] args){
        Context context = new Context();
        System.out.println("1 + 2 = "+context.asksOperation(1,2,"+"));
    }
}

이런 식으로 구현해 잘 사용할 수 있다.

만약 사용자의 요청으로 “/” 연산이 추가되어야 한다면? Context의 클래스를 직접 수정해야 한다.

(게다가 나누기의 경우, divideByZeroException도 필요하다…!)

 

예시의 경우, 단순 연산이지만 사용자의 요청이 좀 더 복잡하고 Context 클래스를 변경하기 어렵다면…

+,-,* 등의 연산 “행위” 를 클래스로 분리해 추가 사항에 좀 더 유연하고 안전하게 바꿔보자.

바로 아래의 예제 처럼

ref) https://www.tutorialspoint.com/design_pattern/strategy_pattern.htm

오른쪽 가장 상단 main 클래스는 Context를 호출해 연산을 호출한다.

Context는 Strategy를 주입받아 해당 Strategy의 연산(excute)을 한다.

  • 주입되는 strategy에 따라 연산의 내용은 달라지고,
  • Context의 경우, 원하는 대로 Strategy를 주입해 연산을 바꿀 수 있지만 상세 연산 내용은 Strategy만 알고, 세부 내용은 Context 클래스는 신경 쓰지 않는다.

operationStrategy를 만들어준다. 인터페이스로 만들어 구현해야 하는 행위를 정의해준다.

public interface OperaionStartegy {
    public int doOperaion(int a, int b);
}

이제 저 행위를 구체화하는 클래스를 만들어 준다.

// 더하기 행위를 구체화한 클래스
public class AddOperaion implements OperaionStartegy{
    @Override
    public int doOperaion(int a, int b) {
        return a + b;
    }
}

// 빼기 행위를 구체화한 클래스
public class SubOperation implements OperaionStartegy{
    @Override
    public int doOperaion(int a, int b) {
        return a - b;
    }
}

// 곱하기 행위를 구체화한 클래스
public class MultiplyOperaion implements OperaionStartegy{
    @Override
    public int doOperaion(int a, int b) {
        return a * b;
    }
}

위 변화로 Context의 경우 코드가 조금 변경된다. Strategy를 주입받고, 주입받은 Strategy의 doOperaion을 실행한다.

public class Context {

    private OperaionStartegy operaionStartegy;

    public void setOperaionStartegy(OperaionStartegy operaionStartegy){
        this.operaionStartegy = operaionStartegy;
    }

    public int asksOperation(int a, int b){
        return operaionStartegy.doOperaion(a,b);
    }
}

그에 따라 Main 역시 호출하는 부분이 달라진다. “+”를 직접 넘기는 게 아니라

“+”에 해당하는 Strategy를 생성해 넘겨준다.

public class mainClass {
    public static void main(String[] args){
        Context context = new Context();
        context.setOperaionStartegy(new AddOperaion()); // 전략 생성! & 넘겨주기
        System.out.println("1 + 2 = "+context.asksOperation(1,2)); // 연산결과
    }
}

 

위에 적어 놓은 요청사항을 다시 보자.

“/” 나누기 연산이 추가돼야 한다면? 아까처럼 Context를 수정하는 게 아니다. (놀랍게도 Context는 변하는 게 전혀 없다!!!)

새로운 연산(행위)에 대한 클래스를 정의해 추가해 준다.

public class DivideOperaion implements OperaionStartegy{
    @Override
    public int doOperaion(int a, int b) {
        if(b == 0) {
            System.out.println("not divede by Zero");
            return 0;
        }
        return a / b;
    }
}

main 클래스에서 호출할 때, 새로운 연산을 선언해 넘겨준다.

public class mainClass {
    public static void main(String[] args){
        Context context = new Context();
        context.setOperaionStartegy(new DivideOperaion());
        System.out.println("1 / 2 = "+context.asksOperation(1,2));
    }
}

 

Java의 대표적인 전략 패턴으로는 Comparator 가 있다고 한다. 생각해 보면

  • Comparator의 비교조건 구현에 따라 객체 비교 달라짐
  • 인터페이스이기 때문에 사용자가 직접 구현해야 하는 부분(익명 객체? 이용 가능하다는데)

이라는 점에서 지금까지 나도 모르게 전략패턴을 사용하고 있었다는 걸 알게 됐다.

이제 코드를 보면 “new Comparator 자동 완성을 치면 왜 저렇게 주렁주렁 나오는지”에 대한 이유를 알게 되었다.

  1. Comparator는 전략 패턴을 이용, 행위가 무엇인지까지는 정의하지 않음 (인터페이스)
  2. 행위에 대한 구체적 구현은 사용자가 비교하고자 하는 객체에 따라 달라짐
public class mainClass {
    public static void main(String[] args){
        List<Integer> nums = new ArrayList<>(Arrays.asList(1,3,2,4));
        System.out.println("before sort : "+nums);

        nums.sort(new Comparator<Integer>() { //
            @Override
            public int compare(Integer o1, Integer o2) {
                return o1 - o2;
            }
        });
        System.out.println("after sort : "+nums);
    }
}

이건 SOLID 원칙 중 OCP (open-close-principle)에 만족하게 구현하는 것이라고 하는데, 그렇게 세세하게 알고 싶진 않고 이 패턴을 알고 있으니 나중에 연산 복잡한 거 있으면 잘 구별해서 써야겠다.

 

참고

반응형