Published 2021. 12. 23. 11:17
반응형

전략 패턴이란?


동일한 기능의 알고리즘이 다를 경우 해당 알고리즘을 쉽게 변경하기 위한 패턴이다.
DI를 활용한 방식으로 OCP에 특화되어, 변경에 용이하다.

전략 패턴을 사용하지 않으면 어떻게 안좋은데?


동일한 기능이나 알고리즘이 달라져 자주 변경이 된다면 보통 if-else로 많이 작성을 할 것이다.
어떤 상황인지 살펴 보자.

아래는 과일 가게에서 할인 정책을 적용해 가격을 계산하는 기능이다.

  • 해당 모듈은 가격을 계산하는 모듈이다.
  • 가격 계산과 할인 정책이 같이 들어가 있다.

아마 보통 이렇게 작성되어 있는 코드가 많을 것이다. 나도 회사에서 100% 이렇게 되어있다.

public class Calculator {

    public int calculate(boolean firstGuest, List<Item> items) {
        int sum = 0;
        for (Item item : items) {
            if (firstGuest) {
                sum += (int) (item.getPrice() * 0.9); // 첫 손님 10% 할인
            } else if (!item.isFresh()) {
                sum += (int) (item.getPrice() * 0.8); // 덜 신선한 것 20% 할인
            } else {
                sum += item.getPrice();
            }
        }
        return sum;
    }
}

이렇게 작성하면 뭐가 문제가 될까?

  • 정책이 추가 될수록 점점 if-else는 많아지고 가독성은 떨어지게 된다. 메서드 하나가 1000줄 이상되는 것을 많이 봤는데 지옥이다.
    그래서 망가질까 무서워 그대로 복사해서 또 if-else를 추가하는 현상이 발생된다.
  • 코드 변경이 모든 곳에 영향을 미치게 된다.
    이 코드를 100군데에서 사용하고 있다고 가정하자. 근데 1군데에서만 첫 손님 할인 정책이 빠져야 한다고 하자. 어떻게 할까?
    파라미터를 하나 더 던져서 그걸로 구분을 하던가 할 것이다.
    아니면 마지막 손님은 50%할인 정책이 추가 되었다고 하자. 어떻게 할까? 역시나 파라미터를 하나 더 던져서 구분할 것이다.
    실제 어떻게 되는지 살펴보자.

아래는 마지막 손님은 50%할인 정책이 추가된 코드이다.
기가 막히다. 100군데에 모두 lastGuest 파라미터를 추가해 주어야한다. 그리고 실수로 firstGuest, lastGuest를 모두 true로 넘기는 실수도 할 수 있다.
최악이다.

    public int calculate(boolean firstGuest, boolean lastGuest, List<Item> items) {
        int sum = 0;
        for (Item item : items) {
                        ...
            } else if (lastGuest) {
                sum += (int) (item.getPrice() * 0.5); // 마지막 손님 50% 할인
            } else {
                sum += item.getPrice();
            }
        }
        return sum;
    }

그럼 전략 패턴으로 개선 해보자.

전략 패턴은 동일한 기능인데 알고리즘이 다른 경우 사용한다고 했다. 즉, 변경에 용이한 것이다.

DI를 하여 알고리즘을 주입 받는 것이다. 아래 개선한 코드를 보자.
DiscountStrategy 라는 인터페이스로 역할을 지정했다. DiscountStrategy가 전략이 되는 것이다. 할인 정책 전략인 것이다.
계산을 하기 위해 전략을 사용하는 클래스는 Context 클래스라고 한다.
전략을 구현한 클래스를 콘크리트 클래스라고 한다.

public class Calculator {

        private DiscountStrategy discountStrategy;

        public Calculator(DiscountStrategy discountStrategy) {
            this.discountStrategy = discountStrategy; 
        }

    public int calculate(List<Item> items) {
        int sum = 0;
        for (Item item : items) {
                    sum += discountStrategy.getDiscountPrice(item);
        }
        return sum;
    }
}

전략 인터페이스는 아래와 같이 된다.

public interface DiscountStrategy {

    int getDiscountPrice(Item item);
}

각각의 콘크리트 클래스는 아래와 같이 할 수 있다.

첫 번째 손님 할인 정책

public class FirstGuestDiscountStrategy implements DiscountStrategy {

    @Override
    public int getDiscountPrice(Item item) {
        return (int) (item.getPrice() * 0.9);
    }
}

덜 신선한 과일 할인 정책

public class NotFreshDiscountStrategy implements DiscountStrategy {

    @Override
    public int getDiscountPrice(Item item) {
        return (int) (item.getPrice() * 0.8);
    }
}

그럼 클라이언트에서 어떻게 사용할 수 있을까?

예로 100군데에서 Caluclator 클래스의 calculate()를 사용하고 있고 그 중 한군데의 코드라고 생각하자.
이렇게 되면 Calculator 클래스는 전혀 변동이 없게 된다. 새로운 정책이 추가 되더라도 이렇게 클라이언트에서 구현체만 넘겨주면 되는 것이다.

이렇게 생각할 수도 있다. 뭐야? 결국 if-else 아니야?
맞다. 결국 어디선가는 if -else가 있어야 한다. 하지만 핵심은 가격 합산 기능이다. 이 합산 로직이 동일한 기능이고, 할인 정책을 변경 요소이다.
즉, calculate()가 변경이 되어도 100군데 모두 동일하게 적용될 것을 바라고, 할인 정책만 상활별로 변동이 되는 상황인 것이다.

if (첫 번째 손님) {
    Calculator calculator = new Calculator(new FirstGuestDiscountStrategy());

    List<Item> items = item 리스트
    int price = calculator.getDiscountPrice(items);
} else if (덜 신선한 과일) {
    Calculator calculator = new Calculator(new NotFreshDiscountStrategy());

    List<Item> items = item 리스트
    int price = calculator.getDiscountPrice(items);    
}

정리


  • 전략 패턴이란 동일한 기능의 알고리즘이 다를 경우 변경에 용이하기 위해 사용된다.
    Context 코드 변경없이 전략을 변경할 수 있다.
    → 코드상으로 비슷한 코드가 if-else로 반복 된다면 전략 패턴을 사용할 신호이다.
  • 변경 되는 부분을 추상화한 것이 전략이다.
    → 알고리즘 추상화
  • 전략의 구현체를 콘크리트 클래스라고 한다.
    → 알고리즘
  • 전략을 사용하는 클래스를 Context 클래스라고 한다.
    → 동일한 기능
  • 전략의 결정은 클라이언트가 결정한다.(DI)

참고 자료


개발자가 반드시 정복해야할 객체지향과 디자인패턴 p.175

반응형

'기타 IT' 카테고리의 다른 글

상태 패턴이란  (0) 2022.01.05
템플릿 메서드 패턴  (0) 2021.12.29
디미터 법칙과 Tell, Don't Ask 법칙  (3) 2021.12.10
POSIX의 EOF(End Of File) 규칙  (0) 2021.12.03
ec2에서 톰캣 서버 포트 포워딩 하기  (0) 2021.10.02
복사했습니다!