하리Hari

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%로 계산돼요. "퍼센트"처럼 없는 이름을 쓰면 마지막 줄 때문에 할인 없이 계산돼요. 둘 다 에러가 나지 않아요.
  • 할인 하나만 따로 시험하기 어려워요. 쿠폰 계산이 맞는지 보려면 계산대를 통째로 만들고, 방식 이름과 값을 맞춰 넣어야 해요.

해결책: 방법을 객체로 떼어 내기

전략 패턴은 “어떻게 할인하는가”를 계산대 밖으로 꺼내요.

  1. 모든 할인 방법이 지킬 약속, 곧 규정을 만들어요. 할인이라면 금액을 받아 <적용하기>를 할 줄 알아야 해요.
  2. 할인 방법마다 클래스를 하나씩 만들어요. 그 방법에 필요한 값(할인율, 쿠폰 금액)은 그 클래스가 직접 들고 있어요.
  3. 계산대는 '할인' 하나만 들고 있다가, 계산할 때 그 할인에게 맡겨요.
  4. 할인 방법을 바꾸고 싶으면 쓰는 쪽이 다른 할인 객체를 골라 끼워요.

비유: 길찾기 앱

길찾기 앱에서 목적지는 그대로인데 도보, 자동차, 대중교통 중 하나를 고르면 경로가 달라져요. 앱은 “어떻게 갈지”를 버튼 하나로 갈아 끼울 뿐, 길찾기 화면 자체가 바뀌지는 않아요. 새 이동 수단(자전거)이 생겨도 화면은 그대로 두고 경로 계산 방법만 하나 더 넣으면 돼요. 이동 수단 하나하나가 바로 “전략”이에요.

누가 무엇을 하나요?

역할 이 예제에서 하는 일
전략 약속 [할인] (규정) 모든 할인 방법이 가져야 할 메서드를 정해요
구체적인 전략 [할인없음], [비율할인], [쿠폰할인] 할인을 실제로 계산하고, 필요한 값을 스스로 들고 있어요
맥락(쓰는 곳) [계산대] 전략 하나를 들고 있다가 계산을 맡겨요
고르는 쪽 프로그램 맨 아래 어떤 전략을 끼울지 정해요

하리로 만들기

같은 계산대를 전략 패턴으로 다시 만들어 볼게요. 코드 속 (참고: 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%” 두 가지뿐이고 바뀔 일도 없다면, 만약 한 번이 더 읽기 쉬워요. 방법이 늘어나기 시작하거나 방법마다 값이 붙기 시작할 때 전략 패턴으로 옮겨도 늦지 않아요.

다른 패턴과의 관계

  • 상태 패턴과 모양이 같아요. 둘 다 “규정을 따르는 객체를 갈아 끼우는” 구조예요. 차이는 누가 갈아 끼우느냐예요. 전략은 쓰는 쪽이 골라 끼우고 전략끼리는 서로 몰라요. 상태는 상태가 스스로 다음 상태로 넘어가요.
  • 팩토리 패턴과 함께 쓰기 좋아요. “VIP면 비율 할인, 쿠폰이 있으면 쿠폰 할인”처럼 어떤 전략을 만들지 정하는 일을 팩토리에 맡기면, 고르는 코드도 한곳에 모여요.
  • 데코레이터 패턴과 비교해 보세요. 전략은 일의 속(방법)을 바꾸고, 데코레이터는 일을 겉에서 감싸 기능을 더해요.
놀이터
결과
코드를 작성하고 '실행' 버튼을 눌러보세요.