개발 이야기/디자인 패턴

[디자인 패턴] 전략(Strategy) 패턴이란?

제이온 (Jayon) 2021. 3. 10.

안녕하세요? 제이온입니다.

 

전략 패턴을 시작으로, 주로 사용되는 디자인 패턴을 소개하려고 합니다.

 

 

디자인 패턴이란?

객체 지향 설계는 소프트웨어로 해결하고자 하는 문제를 다루면서, 동시에 재설계 없이 또는 재설계를 최소화하면서 요구 사항의 변화를 수용할 수 있도록 만들어 주었습니다. 객체 지향 설계를 하다 보면, 이전과 비슷한 상황에서 사용했던 설계를 재사용하는 경우가 발생하는데, 이렇게 자주 사용되는 설계를 모아서 일정한 패턴을 찾게 되었습니다. 그리고 우리는 이 패턴을 '디자인 패턴'이라고 부릅니다.

 

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

 

 

전략(Strategy) 패턴

전략 패턴을 소개하기 전에 상황을 하나 예시로 들겠습니다. 한 과일 매장은 상황에 따라 다른 가격 할인 정책을 적용하고 있는데, 매장을 열자마자 들어온 손님을 위한 '첫 손님 할인'과 저녁 시간대에 신선도가 떨어진 과일에 대한 '덜 신선한 과일 할인' 정책이 존재합니다.

 

이때, 과일의 가격을 계산하기 위하여 아래와 같이 Calculator 클래스를 만들고 메소드를 정의할 수 있습니다.

 

 

public class Calculator {

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

 

 

또한, Item 클래스는 아래와 같이 코드를 작성하였습니다.

 

 

public class Item {
    private static final int EVENING = 18;
    private final String name;
    private final double price;
    private final int time;

    public Item(final String name, final double price, final int time) {
        this.name = name;
        this.price = price;
        this.time = time;
    }

    public String getName() {
        return name;
    }

    public boolean isFresh() {
        return time < EVENING;
    }

    public double getPrice() {
        return price;
    }
}

 

 

여기서 시간은 '시'만을 의미하며, 18시 이상일 경우 덜 신선한 것으로 판단하였습니다.

 

 

public class CalculatorTest {

    @ParameterizedTest
    @DisplayName("과일의 총합이 잘 구해지는지 확인")
    @CsvSource(value = {"false:7300", "true:6000"}, delimiter = ':')
    void calculateIfFirstGuest(final boolean firstGuest, final int expected) {
        final Calculator calculator = new Calculator();
        final List<Item> items = Arrays.asList(new Item("사과", 2000.0, 18),
            new Item("딸기", 1500.0, 12), new Item("멜론", 4000.0, 10));

        assertThat(calculator.calculate(firstGuest, items)).isEqualTo(expected);
    }

}

 

 

파라미터 기반 테스트를 통하여 계산이 잘 되는 것을 확인하였습니다. 첫 고객이 아닐 경우, 사과에 대해 10% 할인 금액 1800원, 나머지는 1500, 4000원 그대로 계산되어 7300원이 되는 것을 알 수 있습니다. 첫 고객일 경우, 덜 신선한 과일과는 관계 없이 무조건 20퍼센트 할인이 적용되어 6000원이 됩니다.

 

 

우리가 위에서 작성하였던 calculate() 메소드 코드를 다시 한 번 봅시다.

 

 

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

 

 

앞으로 변화 없이 평생 할인 정책이 2가지만 존재한다면, 위 방식은 간단하므로 좋은 방식처럼 보일 수 있습니다. 하지만, 마지막 손님일 경우 50% 할인 정책이 추가되는 등의 추가 정책이 생긴다면 해당 메소드를 수정하기 어려워집니다. 따라서, 이런 문제를 해결하기 위한 방법 중의 하나로 가격 할인 정책을 별도 객체로 분리할 수 있습니다.

 

 

 

 

DiscountStrategy 인터페이스는 상품의 할인 금액 계산을 추상화하였고, 각각의 구현 클래스는 상황에 맞는 할인 계산 알고리즘을 제공합니다. 그리고 Calculator 클래스는 가격 합산 계산의 책임을 집니다.

 

이때, 가격 할인 알고리즘을 추상화하고 있는 DiscountStrategy를 전략이라고 부르고, 가격 계산 기능 자체의 책임을 갖고 있는 Calculator를 Context라고 부릅니다. 이렇게 특정 Context에서 알고리즘을 별도로 분리하는 설계 방법이 바로 전략 패턴입니다.

 

한 번 전략 패턴을 통해서 Calculator를 리팩토링해 봅시다.

 

 

public class Calculator {

    private DiscountStrategy discountStrategy;

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

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

 

 

다음과 같이 DiscountStrategy를 의존성 주입으로 초기화한 다음에, 전략 객체에 메소드를 호출하여 계산을 하도록 설계하는 것입니다. 복잡한 if문이 사라지고, 할인 정책을 변경하더라도 Calculator에서 수정할 코드는 없으므로 개방-폐쇄 원칙을 지키는 코드라고 볼 수 있습니다.

 

 

언제 전략 패턴을 사용하는 것이 좋을까?

일반적으로 if-else로 구성된 코드 블록이 비슷한 기능 혹은 비슷한 알고리즘을 수행하는 경우에 전략 패턴을 적용함으로써 코드를 확장시킬 수 있습니다. 이것은 제가 처음 소개한 예제에 해당합니다.

 

또는, 동일한 기능의 알고리즘을 변경해야 하는 경우에도 전략 패턴을 적용할 수 있습니다. 이 부분은 아래 사진을 보면서 이야기하겠습니다.

 

 

https://victorydntmd.tistory.com/292

 

 

예를 들어, 위와 같이 '이동'하는 메소드가 있는 Movable 인터페이스의 구현 클래스 Train과 Bus가 있다고 가정합시다. 이때, Train의 move()는 선로를 통해서 이동하고, Bus의 move()는 도로를 따라 이동합니다. 시간이 흘러서 선로를 따라 이동하는 Bus가 개발된다면, Bus의 move()를 수정해야하고 이것은 개방-폐쇄 원칙을 위반합니다. 또한, Train과 Bus 외의 다른 운송수단이 생긴다면 각자의 move()를 정의해야 하고, 또 수정 사항이 생긴다면 move()를 수정해야 합니다.

 

이러한 경우 '이동 전략' 인터페이스를 정의하고, 그것의 구현 클래스인 '선로를 통해서 이동' 클래스와 '도로를 따라 이동' 클래스를 만들어서 추상화를 진행하는 것이 바람직합니다.

 

https://victorydntmd.tistory.com/292

 

 

 

이제, 선로를 통해 이동하는 운송수단은 RailLoadStrategy를 상속받고, 도로를 따라 이동하는 운송수단은 LoadStrategy를 상속받음으로써 유연하게 변화에 대처할 수 있습니다.

 

 

정리

전략 패턴은 적용하기 쉬우면서도 정말 많이 쓰이는 디자인 패턴 중 하나이므로 꼭 잘 연습해서 익히는 것이 좋겠습니다.

 

다음 시간에는 템플릿 메소드 패턴을 알아 보겠습니다.

 

 

출처

개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴 - 최범균

 

 

[디자인패턴] 전략 패턴 ( Strategy Pattern )

전략 패턴 ( Strategy Pattern ) 객체들이 할 수 있는 행위 각각에 대해 전략 클래스를 생성하고, 유사한 행위들을 캡슐화 하는 인터페이스를 정의하여, 객체의 행위를 동적으로 바꾸고 싶은 경우

victorydntmd.tistory.com

 

댓글

추천 글