1. 전략(Strategy) 패턴

728x90

특정 디자인 패턴을 정리하기 이전에 기본적으로 디자인 패턴이 뭔지 알아야 할 것이다. 

디자인 패턴이란? 

객체 지향 설계는 소프트웨어로 해결하고자 하는 문제를 다루면서, 동시에 재설계 없이 또는 재설계를 최소화하면서 요구 사항의 변화를 수용할 수 있도록 만들어 준다. 객체 지향 설계를 하다 보면, 이전과 비슷한 상황에서 사용했던 설계를 재사용하는 경우가 발생한다. 이런 설계는 특정 상황에 맞는 해결책을 빠르게 찾을 수 있도록 도와주는데, 이렇게 반복적으로 사용되는 설계는 클래스, 객체의 구성, 객체 간 메시지 흐름에서 일정 패턴을 갖는다. 이런 패턴을 습득함으로써 다음과 같은 이득을 얻을 수 있다. 

  • 상황에 맞는 올바른 설계를 더 빠르게 적용할 수 있다. 
  • 각 패턴의 장단점을 통해서 설계를 선택하는데 도움을 얻을 수 있다.
  • 설계 패턴에 이름을 붙임으로써 시스템의 문서화, 이해, 유지 보수에 도움을 얻을 수 있다. 

(이 분야에서 자주 사용되는 패턴들을 모아서 집대성한 다양한 책이 존재하는데, 그 중에서도 GoF의 디자인 패턴이 많은 프로그래머들에게 도움을 주었다)


전략(Strategy) 패턴

전략패턴은 특정 콘텍스트(Context)에서 알고리즘(전략)을 별도로 분리하는 설계 방법이다. 전략 패턴에서 콘텍스트는 사용할 전략을 직접 선택하지 않는다. 대신, 콘텍스트의 클라이언트가 콘텍스트에 사용할 전략을 전달해 준다. 그리고 전략이 어떤 메서드를 제공할 지의 여부는 콘텍스트가 전략을 어떤 식으로 사용하느냐에 따라 달라진다. 

조금 쉽게 얘기하자면(나름 쉽게 얘기하자면) 여러 알고리즘을 하나의 추상적인 접근점(여기서는 인터페이스)을 만들어 접근 점에서 서로 교환 가능하도록 하는 패턴이다.

기본적인 설계는 위의 그림과 같다. 

예를 들어보자! 

한 과일 매장이 상황에 따라 다른 가격 할인 정책을 적용하고 있고, 매장의 첫 손님을 위한 '첫 손님 할인' 정책과 저녁 시간대에 신선도가 떨어진 과일에 대한 '덜 신선한 과일 할인' 정책이 있다면, 아래와 같은 코드를 작성할 것이다. 

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); // 첫 손님 할인
            } else if (!item.isFresh()) {
                sum += (int) (item.getPirce() * 0.8); // 덜 신선한 것 20% 할인
            } else {
                sum += item.getPrice();
            }
        return sum;
    }
}

딱히 이 코드가 틀렸다는 것은 아니다(다들 그렇게 생각할 것이다). 하지만 이 코드는 다음과 같은 문제점을 포함한다. 

  • 서로 다른 계산 정책들이 한 코드에 섞여 있어, 정책이 추가될수록 코드 분석을 어렵게 만든다. 
  • 가격 정책이 추가될 때마다 calculate 메서드를 수정하는 것이 점점 어려워진다. 

이런 문제를 해결하기 위한 방법 중의 하나는 '가격 할인 정책'을 별도 객체로 분리하는 것이다. 

이제부터 DiscountStrategy 인터페이스를 생성하여 상품의 할인 금액 계산을 추상화하고, 각 콘크리트 클래스(전략 콘크리트 클래스)는 상황에 맞는 할인 계산 알고리즘을 제공한다. Calculator 클래스는 가격 합산 계산의 책임을 진다. 

여기서 가격 할인 알고리즘(계산 방법)을 추상화하고 있는 DiscountStarategy를 전략(Strategy)이라고 부르고 가격 계산 기능 자체의 책임을 갖고 있는 Calculator를 콘텍스트(Context)라고 부르는데, 이렇게 특정 콘텍스트에서 알고리즘(전략)을 별도로 분리하는 설계 방법이 전략패턴이다. 

이제 전략 패턴을 적용한 Calculator를 구현한다(이제 Calculator는 변할 일이 없다). 

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

 

이제 위의 Calculator 클래스는 생성자를 통해서 사용할 전략 객체를 전달받고, calculate() 메서드에서 각 Item의 가격을 계산할 때 전략 객체를 사용하고 있다. 위 코드에서 Calculator는 각 Item 별로 할인 정책을 적용하고 있으므로 DiscountStrategy 인터페이스는 아래와 같이 정의 될 것이다. 

public interface DiscountStrategy {
    int getDiscountPrice(Item item);
}

만약 각 아이템 별로 할인 정책이 있고 전체 금액에 대한 할인 정책이 별도로 필요하다면 아래와 같이 전체 금액 할인을 위한 메서드를 추가한다. 

public interface DiscountStrategy {
    int getDiscountPrice(Item item);
    int getDiscountPrice(int totalPrice);
}

또는, 전체 금액 할인 정책을 위한 전략을 별도 인터페이스로 분리할 수도 있을 것이다. 

public interface ItemDiscountStrategy {
    int getDiscountPrice(Item item);
}

public interface TotalPriceDiscountStrategy {
    int getDiscountPrice(int totalPrice);
}

 

전략 객체는 콘텍스트를 사용하는 클라이언트에서 직접 생성한다. 예를 들어, 첫 번째 손님에 대한 할인 정책을 FirstGuestDiscountStrategy 구현 클래스로 구현한다고 하자. 

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

 

첫 번째 손님이 들어와서 계산을 하면, 계산기에서 첫 번째 손님 할인 적용 버튼을 누른 뒤에 계산 버튼을 누를 것이다. 이를 처리하는 코드는 다음과 같은 방식으로 작성될 것이다. 

private DiscountStrategy strategy;

public void onFirstGuestButtonClick() {
    // 첫 손님 할인 버튼 누를 때 생성
    strategy = new FirstGuestDiscountStrategy();
}

public void onCalculationButtonClick() {
    // 계산 버튼 누를 때 실행 
    Calculator cal = new Calculator(strategy);
    int price = cal.calculate(items);
    ...
}

위 코드를 보면 Calculator를 사용하는 코드에서 FirstGuestDiscountStrategy 클래스의 객체를 생성하는 것을알 수 있다. 이는 콘텍스트를 사용하는 클라이언트가 전략의 상세 구현에 대한 의존이 발생한다는 것을 뜻한다. 

콘텍스트의 클라이언트가 전략의 인터페이스가 아닌 상세 구현을 안다는 것이 문제처럼 보일 수 있지만, 이 경우에는 전략의 콘크리트 클래스와 클라이언트의 코드가 쌍을 이루기 때문에 유지 보수 문제가 발생할 가능성이 줄어든다. (또한 클라이언트의 버튼 처리 코드에서 전략 객체를 직접 생성하는 것이 오히려 코드 이해를 높이고 코드 응집을 높여 주는 효과를 갖는다)

전략 패턴을 적용할 때 얻을 수 있는 이점은 콘텍스트 코드의 변경 없이 새로운 전략을 추가할 수 있다는 점이다. 앞의 예제에서도 마지막 손님 대폭 할인 정책을 추가하는 경우, 계산의 틀인 Calculator 클래스의 코드는 변경되지 않는다. 단지 새로운 할인 정책을 구현한 LastGuestDiscountStrategy 클래스를 추가하고, 마지막 손님 대폭 할인 버튼 클릭을 처리하는 코드에서 LastGuestDiscountStrategy의 객체를 생성해 주기만 하면 된다. 

private DiscountStrategy strategy;

public void onLastGuestButtonClick() {
    // 마지막 손님 대폭 할인 버튼 누를 때 생성
    strategy = new LastGuestDiscountStrategy();
}

public void onCalculationButtonClick() {
    // 계산 버튼 누를 때 실행 됨
    Calculator cal = new Calculator(strategy);
    int price = cal.calculate(items);
    ...
}

전략 패턴을 적용함으로써 Calculator 클래스는 할인 정책 확장에는 열려 있고 변경에는 닫혀 있게 된다. 즉 '개방 폐쇄 원칙'(SOLID원칙의 OCP)을 따르는 구조를 갖게 된다. 

일반적으로 if-else로 구성된 코드 블록이 비슷한 기능(비슷한 알고리즘)을 수행하는 경우에 전략 패턴을 적용함으로써 코드를 확장 가능하도록 변경할 수 있다. (본 예제에서도 calculate() 메서드의 if-else 블록에 전략 패턴을 적용함으로써 새로운 할인 정책을 보다 쉽게 추가할 수 있도록 만들었다) 

 

이 외에도 완전히 동일한 기능을 제공하지만 성능의 장단점에 따라 알고리즘을 선택해야 하는 경우에도 전략패턴을 사용한다. 

 

 

참고도서: (개발자가 반드시 정복해야 할) 객체지향과 디자인 패턴

참고강의 : https://www.inflearn.com/course/%EC%9E%90%EB%B0%94-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4/lecture/3171

 

자바 디자인 패턴의 이해 - Gof Design Pattern - 인프런

자바 디자인 패턴 이해하기 강좌 입니다. 여러가지 디자인 패턴들을 알아보며 디자인 패턴에 대한 이해도를 높이도록 도와줍니다. Gof Design Pattern을 자바 언어로 설명한 강의. 의미 있고 쉬운 예제를 준비하려고 노력했습니다. 중급 프로그래밍 언어 Java MVC 온라인 강의 자바 디자인 패턴

www.inflearn.com

 

728x90

'디자인 패턴' 카테고리의 다른 글

전략 패턴 (Strategy Pattern)  (0) 2020.10.02
3. 브릿지 패턴 (Bridge Pattern)  (0) 2020.04.25
2. 어댑터 패턴 (Adapter Pattern)  (0) 2020.04.24