brunch

You can make anything
by writing

C.S.Lewis

by 서준수 Jan 05. 2022

[리팩터링] 전략 패턴으로 조건문 간소화

전략 패턴이란?

어떤 로직에서 알고리즘을 캡슐화하여 분리하는 디자인 패턴이다. 여기서 말하는 알고리즘은 어렵고 거창한 것이 아니다. 예를 들어 커피 가격을 관리하는 프로그램이 있다고 하자. 그러면 아메리카노를 주문할 때 얼음을 추가하면 아이스 아메리카노의 가격을 가져와야 한다. 아이스 아메리카노의 가격은 얼음 + 아메리카노 가격의 합이라는 단순한 계산도 알고리즘이 될 수 있다.


장점

이런 상황에서 전략 패턴이 주는 장점은 바로 런타임 중 알고리즘을 선택적으로 교체할 수 있다는 것이다. 커피 주문 시 아이스 라떼를 주문하는 경우도 있을 것이다. 이때는 아이스 라떼 가격을 가져오는 알고리즘으로 주문을 교체하면 된다. 이게 말로만 들어서는 잘 이해가 되지 않을 수 있다. 아래에서 코드로 살펴볼 때 이러한 상황을 생각하고 보면 도움이 될 것이다.


또 하나의 장점은 필요할 때 새로운 전략(알고리즘)을 쉽게 추가할 수 있다는 것이다. 카페에서 신메뉴를 출시하여 가격을 책정해야 하는 경우 컨텍스트 클래스라 부르는 호스트 클래스의 변경 없이 새로운 전략 클래스를 추가할 수 있다.


단점

단순한 로직으로 구성된 경우 전략 패턴을 적용하는 것은 오히려 과도한 리팩터링이 될 수 있다. 또한 해당 패턴에 익숙하지 않다면 오히려 컨텍스트 클래스와 전략 클래스 사이의 관계가 복잡하게 느껴질 것이다.


전략 패턴의 장단점을 고려하면 복잡하거나 긴 조건문으로 로직이 구성되어 있는 경우 이를 간소화하는데 도움이 될 수 있을 것이라고 추측할 수 있다. 따라서 복잡한 조건 로직이 있는 경우 이를 리팩터링 하기 위한 방법으로 전략 패턴을 선택하는 것이 하나의 방법이 될 수 있다.


예제를 보자!

Code : 전략 패턴 미적용, 전략 패턴 적용

예제는 앞서 가정했던 커피 가격 정보를 가져오는 프로그램을 작성해 볼 것이다. 이 상황에서 전략 패턴이 최선의 패턴이라기보다는 전략 패턴이 어떤 것인지 이해하기 위해 그냥 적용해 본 것임에 유의한다. (커피 가격은 데코레이션 패턴이 더 낫지 않을까?) 또한 전략 패턴 그 자체에 대한 것보다는 이 패턴을 사용해서 조건문을 간소화하는 것에 포커스를 맞춘다.


커피 종류는 아주 많을 텐데 여기서는 (아메리카노, 아이스 아메리카노, 아이스 라떼, 아이스 바닐라라떼) 네 가지 종류만 취급한다. 그렇지 않으면 알고리즘이 너무 많아지기 때문이다. 메뉴 종류는 레시피에 따라 결정된다. 예를 들어 얼음, 우유, 바닐라 시럽 모두 없으면 아메리카노이다. 여기에 얼음만 추가하면 아이스 아메리카노이다. 여기에 추가적으로 사이즈 선택과 샷을 추가할 수 있다.


먼저 전략 패턴을 적용하기 전 코드를 살펴보자.

Order 클래스 멤버 변수를 보면 가격 결정을 위한 재료인 ice, milk, vanillaSyrup과 추가 옵션인 shot, size 변수가 있다. 멤버 함수에는 주문한 내용에 따라 가격을 결정하는 price() 함수와 샷과 사이즈 추가 시 발생하는 비용 계산을 위한 shot(), size() 함수가 있다. 또한 주문한 메뉴명을 보여주는 show() 함수가 있다.

이 예제를 보면 price() 함수 내에 조건에 따른 여러 계산법이 존재한다. 바로 이 부분이 전략 패턴을 적용해서 간소화할 수 있는 곳이다.


만약 처음 코드를 보는 사람이 if문을 보면 아마 정신이 아득해질 것이다. 코드가 어려워서가 아니다. 무슨 의미인지 알 수 없기 때문이다. 'ice, milk, vanillaSyrup가 모두 false일 때 왜 저런 식의 결과를 리턴하는 걸까?'라는 의문이 들 수밖에 없다. 바꿔보자!


전략 패턴을 적용하기 위한 순서는 다음과 같다.

1. PriceStrategy 클래스를 만든다.
2. price() 함수의 내용을 PriceStrategy 클래스로 옮긴다.
3. Order 클래스의 price() 함수는 PriceStrategy 클래스의 price() 함수에 계산을 위임한다.
4. Order 클래스 내에서 PriceStrategy 객체를 하드 코딩하지 않고 외부에서 생성할 수 있도록 한다.
5. PriceStrategy 클래스의 price() 함수 계산을 각 메뉴에 맞는 서브 클래스를 만들어서 옮긴다.


1. PriceStrategy 클래스를 만든다.

2. price() 함수의 내용을 PriceStrategy 클래스로 옮긴다.

Order 클래스에 있는 price() 함수를 그대로 옮긴다. price() 함수 내에서 호출하는 shot(), size() 함수도 옮긴다.


3. Order 클래스의 price() 함수는 PriceStrategy 클래스의 price() 함수에 계산을 위임한다.

Order 클래스의 price() 함수를 위와 같이 수정하여 계산을 위임한다. 이때 계산에 필요한 변수만 price() 함수의 파라미터로 일일이 보낼 수도 있다. 여기서는 Order 객체 참조를 전달하여 필요한 멤버에 접근할 수 있도록 했다. 따라서 현재 private 변수에 접근할 수 있도록 getter를 추가해 준다.


또한 PriceStrategy 클래스의 price() 함수는 price(Order order)과 같이 되어야 한다.

최종적으로 PriceStrategy 클래스는 위와 같이 된다. 실행도 정상적으로 된다.


4. Order 클래스 내에서 PriceStrategy 객체를 하드 코딩하지 않고 외부에서 생성할 수 있도록 한다.

Order 클래스 생성자에서 생성하던 PriceStrategy 객체는 각 메뉴에 맞는 Order 객체를 생성할 때 생성할 수 있도록 변경한다. 여기까지 하면 큰 덩어리의 전략 패턴은 적용된 것처럼 보인다. 하지만 PriceStrategy 클래스 내의 price() 함수는 여전히 복잡한 조건문으로 이루어져 있다. 이것을 리팩터링 해야 한다.


5. PriceStrategy 클래스의 price() 함수 계산을 각 메뉴에 맞는 서브 클래스를 만들어서 옮긴다.

하나의 예를 들면 아메리카노의 가격 계산을 하는 식을 위와 같이 PriceStrategy 클래스의 서브 클래스인 PriceStrategyAmericano 클래스에서 하도록 변경한다. 각 메뉴에 맞는 서브 클래스를 모두 생성한다.


그러면 PriceStrategy 클래스 최종적으로 위와 같이 깔끔하게 정리된다.


최종적으로 Order 클래스에서도 메뉴에 맞는 서브 클래스를 생성하도록 변경한다.


전략 패턴을 적용한 Order 클래스는 최종적으로 다음과 같다. 복잡한 조건문이 사라졌고 어떤 메뉴의 가격을 가져오는 것인지 명확히 알 수 있게 되었다. 신메뉴가 추가되면 PriceStrategy 클래스의 서브 클래스를 추가하면 된다. 테스트 또한 추가된 클래스만 하면 되고 테스트 코드를 작성할 때 해당 클래스만 가져와서 사용할 수 있어서 편리한 점도 있다.


Ref.) 패턴을 활용한 리팩터링 (조슈아 케리에브스키 저, 인사이트)

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari