3. 전략 패턴 (Strategy)
행동을 인터페이스로 분리하여 실행 중에 자유롭게 교체하는 전략 패턴을 알아봅니다.
한 줄 요약
같은 일을 하는 여러 방법을 각각 클래스로 떼어 내고, 쓰는 쪽은 그중 하나를 골라 끼워서 쓰게 하는 패턴이에요. 방법을 바꾸고 싶으면 다른 것으로 갈아 끼우기만 하면 돼요.
문제: 방법이 늘 때마다 커지는 클래스
가게 계산대를 만든다고 해 볼게요. 처음에는 할인이 없었는데, 가게가 잘되면서 할인 방법이 생겨요.
- 할인 없음: 금액 그대로
- 비율 할인: 10%처럼 비율만큼 빼기
- 쿠폰 할인: 3000원처럼 정해진 금액만큼 빼기
가장 먼저 떠오르는 방법은 계산대가 할인 방법의 이름을 기억해 두고, ~에 따라 나누자로 계산을 나누는 거예요.
[계산대]를 설계하자:
'할인방식'을 [문자열]인 "없음"으로 정하자
'할인율'을 [숫자]인 0으로 정하자
'쿠폰금액'을 [숫자]인 0으로 정하자
<최종금액>을 만들자 ([숫자]인 '금액'):
'나'의 '할인방식'에 따라 나누자:
"없음" 인 경우:
'금액'을 돌려주자
"비율" 인 경우:
('금액' - '금액' * '나'의 '할인율' / 100)을 돌려주자
"쿠폰" 인 경우:
만약 ('금액'이 '나'의 '쿠폰금액'보다 작다) 라면:
0을 돌려주자
('금액' - '나'의 '쿠폰금액')을 돌려주자
'금액'을 돌려주자
'계산대1'을 [계산대]인 새로운 [계산대]()로 정하자
틀"할인 없음: {'계산대1'의 <최종금액>(20000)}원"을 출력하자
'계산대1'의 '할인방식'을 "비율"로 정하자
'계산대1'의 '할인율'을 10으로 정하자
틀"10% 할인: {'계산대1'의 <최종금액>(20000)}원"을 출력하자
'계산대1'의 '할인방식'을 "쿠폰"으로 정하자
'계산대1'의 '쿠폰금액'을 3000으로 정하자
틀"3000원 쿠폰: {'계산대1'의 <최종금액>(20000)}원"을 출력하자
실행 결과
할인 없음: 20000원
10% 할인: 18000원
3000원 쿠폰: 17000원
잘 돌아가요. 나누는 곳도 <최종금액> 한 곳뿐이라 상태 패턴에서 본 “모든 메서드에 같은 나누기가 반복되는” 문제도 없어요. 그런데 할인 방법이 늘어나면 다른 곳이 아파져요.
- 계산대가 모든 할인 방법을 알아야 해요. “두 개 사면 하나 공짜”, “회원 등급 할인”이 생길 때마다
[계산대]클래스를 열어서 고쳐야 해요. 계산대의 일은 “계산하기”인데, 할인 이벤트가 생길 때마다 계산대가 커져요. - 쓰지 않는 값이 쌓여요. 쿠폰을 쓰는 동안에도
'할인율'은 그대로 남아 있어요. 할인 방법이 다섯 개가 되면, 그중 하나만 쓰는데 값은 다섯 종류가 계산대에 붙어 있어요. - 짝이 안 맞아도 조용해요.
'할인방식'을"비율"로 바꾸고'할인율'정하기를 잊으면 할인이 0%로 계산돼요."퍼센트"처럼 없는 이름을 쓰면 마지막 줄 때문에 할인 없이 계산돼요. 둘 다 에러가 나지 않아요. - 할인 하나만 따로 시험하기 어려워요. 쿠폰 계산이 맞는지 보려면 계산대를 통째로 만들고, 방식 이름과 값을 맞춰 넣어야 해요.
해결책: 방법을 객체로 떼어 내기
전략 패턴은 “어떻게 할인하는가”를 계산대 밖으로 꺼내요.
- 모든 할인 방법이 지킬 약속, 곧 규정을 만들어요. 할인이라면 금액을 받아
<적용하기>를 할 줄 알아야 해요. - 할인 방법마다 클래스를 하나씩 만들어요. 그 방법에 필요한 값(할인율, 쿠폰 금액)은 그 클래스가 직접 들고 있어요.
- 계산대는
'할인'하나만 들고 있다가, 계산할 때 그 할인에게 맡겨요. - 할인 방법을 바꾸고 싶으면 쓰는 쪽이 다른 할인 객체를 골라 끼워요.
비유: 길찾기 앱
길찾기 앱에서 목적지는 그대로인데 도보, 자동차, 대중교통 중 하나를 고르면 경로가 달라져요. 앱은 “어떻게 갈지”를 버튼 하나로 갈아 끼울 뿐, 길찾기 화면 자체가 바뀌지는 않아요. 새 이동 수단(자전거)이 생겨도 화면은 그대로 두고 경로 계산 방법만 하나 더 넣으면 돼요. 이동 수단 하나하나가 바로 “전략”이에요.
누가 무엇을 하나요?
| 역할 | 이 예제에서 | 하는 일 |
|---|---|---|
| 전략 약속 | [할인] (규정) |
모든 할인 방법이 가져야 할 메서드를 정해요 |
| 구체적인 전략 | [할인없음], [비율할인], [쿠폰할인] |
할인을 실제로 계산하고, 필요한 값을 스스로 들고 있어요 |
| 맥락(쓰는 곳) | [계산대] |
전략 하나를 들고 있다가 계산을 맡겨요 |
| 고르는 쪽 | 프로그램 맨 아래 | 어떤 전략을 끼울지 정해요 |
하리로 만들기
같은 계산대를 전략 패턴으로 다시 만들어 볼게요. 코드 속 (참고: n단계)를 따라 읽어 보세요.
(참고: 1단계 — 모든 할인 방법이 지킬 약속)
[할인]을 규정하자:
<적용하기>가 있어야 한다 ([숫자]인 '금액')
(참고: 2단계 — 할인 방법마다 클래스 하나, 필요한 값은 자기가 들고 있어요)
[할인]을 따르는 [할인없음]을 설계하자:
<적용하기>를 만들자 ([숫자]인 '금액'):
'금액'을 돌려주자
[할인]을 따르는 [비율할인]을 설계하자:
'할인율'을 [숫자]인 0으로 정하자
처음 만들어질 때 ([숫자]인 '할인율') 다음과 같이 하자:
'나'의 '할인율'을 '할인율'로 정하자
<적용하기>를 만들자 ([숫자]인 '금액'):
('금액' - '금액' * '나'의 '할인율' / 100)을 돌려주자
[할인]을 따르는 [쿠폰할인]을 설계하자:
'쿠폰금액'을 [숫자]인 0으로 정하자
처음 만들어질 때 ([숫자]인 '쿠폰금액') 다음과 같이 하자:
'나'의 '쿠폰금액'을 '쿠폰금액'으로 정하자
<적용하기>를 만들자 ([숫자]인 '금액'):
만약 ('금액'이 '나'의 '쿠폰금액'보다 작다) 라면:
0을 돌려주자
('금액' - '나'의 '쿠폰금액')을 돌려주자
(참고: 3단계 — 계산대는 할인을 하나 들고 있다가 맡기기만 해요)
[계산대]를 설계하자:
'할인'을 [할인]인 새로운 [할인없음]()으로 정하여 숨기자
<할인바꾸기>를 만들자 ([할인]인 '새할인'):
'나'의 '할인'을 '새할인'으로 정하자
<최종금액>을 만들자 ([숫자]인 '금액'):
'나'의 '할인'의 <적용하기>('금액')을 돌려주자
'계산대1'을 [계산대]인 새로운 [계산대]()로 정하자
틀"할인 없음: {'계산대1'의 <최종금액>(20000)}원"을 출력하자
'계산대1'의 <할인바꾸기>(새로운 [비율할인](10))를 실행하자
틀"10% 할인: {'계산대1'의 <최종금액>(20000)}원"을 출력하자
'계산대1'의 <할인바꾸기>(새로운 [쿠폰할인](3000))를 실행하자
틀"3000원 쿠폰: {'계산대1'의 <최종금액>(20000)}원"을 출력하자
실행 결과
할인 없음: 20000원
10% 할인: 18000원
3000원 쿠폰: 17000원
결과는 앞의 코드와 같아요. 겉으로 하는 일은 그대로 두고 속 구조만 바꾼 리팩터링이에요.
단계별로 다시 보기
- 1단계 — 약속 만들기.
[할인]을 규정하자는 “할인이라면<적용하기>가 꼭 있어야 해”라는 약속이에요. 계산대는 할인이 어떤 클래스인지 몰라도, 이 약속만 믿고<적용하기>를 불러요. - 2단계 — 방법마다 클래스 하나.
'할인율'은[비율할인]안에만,'쿠폰금액'은[쿠폰할인]안에만 있어요. 쓰지 않는 값이 계산대에 쌓이지 않아요. 그리고새로운 [비율할인](10)처럼 만들 때 값을 꼭 넣어야 하니, 할인율을 잊는 일도 없어요. - 3단계 — 계산대는 맡기기만.
[계산대]의<최종금액>은 한 줄이에요. 할인 방법이 열 개가 되어도 이 줄은 그대로예요.
엉뚱한 것을 끼우면 어떻게 될까요? <할인바꾸기>는 [할인]인 '새할인'만 받기로 했으니, 앞의 코드처럼 할인 방법의 이름(글자)을 넘기면 바로 에러가 나요.
[할인]을 규정하자:
<적용하기>가 있어야 한다 ([숫자]인 '금액')
[할인]을 따르는 [할인없음]을 설계하자:
<적용하기>를 만들자 ([숫자]인 '금액'):
'금액'을 돌려주자
[계산대]를 설계하자:
'할인'을 [할인]인 새로운 [할인없음]()으로 정하여 숨기자
<할인바꾸기>를 만들자 ([할인]인 '새할인'):
'나'의 '할인'을 '새할인'으로 정하자
'계산대1'을 [계산대]인 새로운 [계산대]()로 정하자
'계산대1'의 <할인바꾸기>("쿠폰")을 실행하자
새 할인을 더해 보면
“무조건 1000원 할인” 이벤트가 생겼다고 해 볼게요.
- 앞의 방식:
[계산대]를 열어서"천원" 인 경우:를 넣어야 해요. 할인 금액이 바뀔 수 있다면 계산대에 새 값도 붙여야 해요. - 전략 패턴:
[할인]을 따르는 [천원할인]이라는 새 클래스 하나만 만들면 돼요.[계산대]는 한 글자도 고치지 않아요. 다른 파일, 다른 사람이 만든 할인도 약속만 지키면 그대로 끼울 수 있어요.
직접 해 보기
위 코드에[천원할인]을 더해 보세요. 금액에서 1000원을 빼되, 0원보다 작아지지는 않게요. 그리고'계산대1'의 <할인바꾸기>(새로운 [천원할인]())로 끼워서 결과를 확인해 보세요.[계산대]는 고칠 필요가 없을 거예요.
이미 써 본 전략 패턴: 함수 넘기기
사실 전략 패턴은 표준 라이브러리 2에서 이미 써 봤어요. <기준정렬>은 정렬하는 방법은 자기가 알고, “무엇을 기준으로 줄 세울지”만 함수로 받아요. 넘기는 함수가 바로 전략이에요.
[목록]에서 <기준정렬>을 가져오자
<짧은순>을 만들자 ('글'):
'글'의 '길이'를 돌려주자
<긴순>을 만들자 ('글'):
(0 - '글'의 '길이')를 돌려주자
'과일들'을 ["바나나", "귤", "사과"]로 정하자
<기준정렬>('과일들', <짧은순>)을 출력하자
<기준정렬>('과일들', <긴순>)을 출력하자
실행 결과
[귤, 사과, 바나나]
[바나나, 사과, 귤]
클래스로 만들까, 함수를 넘길까?
전략이 함수 하나로 끝나고 기억할 값도 없다면 함수를 넘기는 게 가장 간단해요. 할인율처럼 기억할 값이 있거나, 전략이 해야 할 일이 여러 개라면 규정과 클래스로 만드는 게 좋아요.
언제 쓰면 좋을까요?
- 같은 일을 하는 방법이 여러 가지이고, 앞으로 더 늘어날 것 같을 때
- 방법마다 필요한 값이 달라서 한 클래스에 모아 두면 안 쓰는 값이 쌓일 때
- 프로그램이 돌아가는 중에 방법을 바꿔 끼우고 싶을 때 (사용자 설정, 이벤트 기간 등)
- 방법을 만드는 사람과 쓰는 사람이 다를 때 (다른 파일, 다른 패키지)
좋은 점과 아쉬운 점
| 좋은 점 | 아쉬운 점 |
|---|---|
| 새 방법을 더해도 쓰는 쪽(계산대)을 고치지 않아요 | 클래스 수가 늘어나요 |
| 방법마다 필요한 값이 그 클래스 안에만 있어요 | 쓰는 쪽이 어떤 전략들이 있는지 알고 골라야 해요 |
| 방법 하나만 따로 만들고 시험하기 쉬워요 | 방법이 두세 개이고 늘 일이 없다면 따라 나누자가 더 간단해요 |
| 실행 중에 방법을 바꿔 끼울 수 있어요 |
너무 이른 패턴은 독이에요.
할인이 “없음”과 “10%” 두 가지뿐이고 바뀔 일도 없다면,만약한 번이 더 읽기 쉬워요. 방법이 늘어나기 시작하거나 방법마다 값이 붙기 시작할 때 전략 패턴으로 옮겨도 늦지 않아요.