268. 전략 (Strategy) - 알고리즘을 캡슐화하여 동적으로 교체 가능

핵심 인사이트 (3줄 요약)

  1. 본질: 전략(Strategy) 패턴은 실행 중에 수행할 행위(알고리즘)를 클래스로 캡슐화하고, 이들을 공통 인터페이스 아래 두어 동적으로 알고리즘을 교체(Swap)할 수 있게 하는 행동(Behavioral) 패턴이다.
  2. 가치: if-elseswitch 문의 지옥을 제거하고, 새로운 알고리즘을 추가할 때 기존 코드를 수정하지 않아도 되는 OCP(개방-폐쇄 원칙)를 완벽하게 달성하여 코드의 유지보수성과 테스트 용이성을 극대화한다.
  3. 융합: 상속을 통한 행위 정의의 한계(코드 중복, 부모 의존성)를 극복하기 위해 **"상속(Inheritance) 대신 위임(Composition)을 사용하라"**는 객체 지향의 가장 중요한 황금률을 구현한 대표적인 패턴이다.

Ⅰ. 개요 및 필요성 (Context & Necessity)

  • 개념: 전략 패턴은 동일한 목적을 달성하지만 구현 방식이 다른 여러 알고리즘(전략)을 각각 하나의 클래스로 묶어 분리(Encapsulate)하고, 런타임(실행 중)에 이들을 클라이언트 클래스에 갈아 끼울 수 있도록 하는 패턴이다.

  • 필요성: 내비게이션 앱을 개발한다고 가정하자. 처음에는 '자동차 경로 탐색' 알고리즘만 있었다. 이후 '도보 경로', '대중교통 경로', '자전거 경로'가 차례로 추가되었다. 만약 이를 하나의 Navigator 클래스 안에 수많은 if-else 문으로 욱여넣는다면, 코드는 금세 수천 줄이 되어 스파게티 코드가 되고 만다. 자전거 경로의 버그를 고치다가 자동차 경로까지 망가뜨리는 일이 빈번해진다.

  • 💡 비유: 리그 오브 레전드(LoL)나 오버워치 같은 게임에서 챔피언이 '무기'를 바꾸는 것과 같습니다. 검을 들면 '베기', 활을 들면 '쏘기'로 공격 방식(알고리즘)이 달라지지만, 챔피언 자체의 '공격한다()'라는 본질적인 행위 명령은 변하지 않습니다.

  • 등장 배경 및 발전 과정:

    1. 상속의 남용: 과거에는 알고리즘을 다양화하기 위해 부모 클래스를 상속받아 자식 클래스에서 메서드를 오버라이딩(Overriding)했다. (예: Robot 부모를 상속받은 WalkingRobot, FlyingRobot) 하지만 이는 기능이 조금만 섞여도 자식 클래스가 기하급수적으로 늘어나는 '클래스 폭발(Class Explosion)' 문제를 낳았다.
    2. 위임(Composition/Delegation)의 부상: "행위(전략)를 내가 직접 구현하거나 상속받지 말고, 행위를 전문으로 하는 다른 부품 객체에게 맡기자(위임)"는 철학이 대두되었다.
    3. 의존성 주입(DI)과의 결합: 현대에 와서는 Spring 프레임워크 등에서 런타임에 필요한 전략 객체를 외부에서 꽂아주는 의존성 주입(Dependency Injection) 메커니즘과 완벽히 결합하여 엔터프라이즈 아키텍처의 기본이 되었다.
  • 📢 섹션 요약 비유: 로봇 장난감을 살 때 날아다니는 로봇, 걸어다니는 로봇을 따로따로 여러 개 사는 것(상속)이 아니라, 몸통은 하나만 사고 등에 꽂는 '날개 팩', '다리 팩'(전략)만 갈아 끼워 다양한 로봇을 만드는 것(위임)과 같습니다.


Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)

구성 요소 (클래스 다이어그램)

요소명역할비유
Context (컨텍스트/문맥)Strategy 인터페이스를 멤버 변수로 가지고 있으며(위임), 실제 작업 실행을 이 변수에게 위임하는 주체. 동적으로 Strategy를 교체하는 Setter 메서드를 가진다.게임 캐릭터 (무기 슬롯을 가짐)
Strategy (전략 인터페이스)모든 구체적인 알고리즘들이 반드시 구현해야 하는 공통 인터페이스(또는 추상 클래스).'무기' 인터페이스 (공격하기 기능 강제)
ConcreteStrategyStrategy 인터페이스를 실제로 구현한 알고리즘 클래스들.활, 검, 마법 지팡이 (각각의 공격 방식 구현)

동작 메커니즘 (코드 뼈대 구조)

전략 패턴의 핵심은 Context 클래스 안에 복잡한 분기문(if-else)이 완전히 사라지고, 오직 인터페이스의 메서드 호출 한 줄만 남는다는 것이다.

  ┌─────────────────────────────────────────────────────────────┐
  │              전략 패턴의 구조와 인터페이스 위임 원리              │
  ├─────────────────────────────────────────────────────────────┤
  │                                                             │
  │  [Context]                          <<Interface>>           │
  │  Navigation ─────────위임(Has-a)──▶ RouteStrategy           │
  │  - routeStrategy                    + buildRoute(A, B)      │
  │  + setStrategy(s)                         △                 │
  │  + executeRoute()                         │                 │
  │       │                                   │                 │
  │       │(routeStrategy.buildRoute() 호출)   │                 │
  │       │                                   │                 │
  │       │          ┌────────────────────────┼─────────┐       │
  │       │          │                        │         │       │
  │       ▼          │                        │         │       │
  │  [Concrete Strategy 구현체들]              │         │       │
  │     ┌────────────┴───┐      ┌─────────────┴──┐      │       │
  │     │ AutoStrategy   │      │ WalkStrategy   │     ...      │
  │     │ + buildRoute() │      │ + buildRoute() │              │
  │     └────────────────┘      └────────────────┘              │
  │                                                             │
  └─────────────────────────────────────────────────────────────┘

[다이어그램 해설] Navigation 클래스는 자신이 어떤 수단으로 경로를 찾을지 알 필요가 없다. 그저 외부에서 누군가(Client)가 setStrategy(new WalkStrategy())로 전략을 꽂아주면, executeRoute()를 실행할 때 자신이 가진 routeStrategy 객체의 buildRoute()를 호출할 뿐이다. 훗날 '비행기 경로'가 추가되더라도 Context 코드는 1비트도 수정되지 않는다. OCP(Open-Closed Principle)가 완벽하게 지켜진 것이다.


상속(Inheritance) vs 위임(Composition)

왜 객체지향 설계자들은 상속보다 위임(Composition)을 선호하는가?

비교 항목상속 (Inheritance, Is-A)위임/컴포지션 (Composition, Has-A)
관계 정의컴파일 타임에 뼈대가 굳어짐런타임에 동적으로 변경 가능
결합도부모 클래스의 변경이 자식에게 연쇄적으로 영향을 미침 (강결합)인터페이스만 바라보므로 영향을 받지 않음 (느슨한 결합)
코드 재사용다중 상속 불가(Java/C#)로 인해 한계 발생필요한 여러 기능을 부품처럼 여러 개 조립 가능
디자인 패턴템플릿 메서드 패턴 (Template Method)전략 패턴 (Strategy)
  • 📢 섹션 요약 비유: 상속이 부모의 유전자를 물려받아 태어나서 죽을 때까지 바꿀 수 없는 '키'나 '얼굴 생김새'라면, 위임은 언제든 벗고 새로 입을 수 있는 '옷'이나 '신발'과 같습니다.

Ⅲ. 융합 비교 및 다각도 분석

1. 전략 패턴 vs 상태 패턴 (State Pattern) vs 커맨드 패턴 (Command Pattern)

이 세 패턴은 클래스 다이어그램이 거의 똑같이 생겼다. (Context가 인터페이스를 가지고 위임함) 그러나 **'목적'과 '교체 주체'**가 완전히 다르다.

패턴목적 (Intent)전략 교체/상태 변화의 주체비유
전략 (Strategy)어떤 작업을 어떻게 수행할지 알고리즘의 캡슐화와 교체**클라이언트(외부)**가 Context에 주입게임 시작 전/도중에 유저가 무기를 바꿈
상태 (State)객체의 내부 상태에 따라 객체의 행동이 스스로 달라지게**내부 로직(State 구현체)**이 스스로 다음 상태로 변경캐릭터 체력이 0이 되면 유저 개입 없이 알아서 '사망' 상태로 전이됨
커맨드 (Command)요청 자체를 객체화하여 저장, 큐잉, 취소(Undo) 할 수 있게 함클라이언트가 커맨드를 생성하여 Invoker에 전달버튼을 누르면 이전에 세팅된 매크로가 순서대로 나감
  ┌─────────────────────────────────────────────────────────────┐
  │         패턴 간 인텐트(Intent) 차이: 누가 뼈대를 바꾸는가?            │
  ├─────────────────────────────────────────────────────────────┤
  │                                                             │
  │  [Strategy Pattern]                                         │
  │  Client ──(결정)──▶ Context.setStrategy(new A())            │
  │                     │                                       │
  │                     └──▶ "클라이언트의 요구에 따라 무기가 바뀜"    │
  │                                                             │
  │  [State Pattern]                                            │
  │  Client ──(행동)──▶ Context.doAction()                      │
  │                     │                                       │
  │                     └──▶ State A 내부 로직이 끝난 후 스스로      │
  │                          Context.setState(new B()) 호출     │
  │                          "내부 조건에 의해 상태가 저절로 바뀜"     │
  └─────────────────────────────────────────────────────────────┘

[다이어그램 해설] 다이어그램의 구조는 같더라도, 전략 패턴에서는 Strategy 객체들이 서로의 존재를 모른 채 독립적으로 존재하며 클라이언트의 선택을 기다린다. 반면 상태 패턴에서는 State A가 자신의 임무를 마치면 State B로 넘어가도록 상태 전이 로직이 내부에 짜여 있는 경우가 많다.

과목 융합 관점

  • 소프트웨어 공학 (SE) / 아키텍처: 의존성 주입(DI, Dependency Injection) 컨테이너는 전략 패턴의 궁극적 진화형이다. 스프링(Spring)에서는 @Service 빈을 인터페이스로 선언하고, application.yml이나 @Profile에 따라 런타임에 다른 전략(구현체)을 주입하는 방식으로 아키텍처 결합도를 극도로 낮춘다.

  • 알고리즘 (Algorithm): 자바의 Collections.sort(List, Comparator) 메서드에서 Comparator가 바로 완벽한 전략 패턴이다. 정렬의 큰 뼈대는 sort()가 담당하지만, 구체적으로 "어떻게 비교할 것인가?"라는 알고리즘은 개발자가 Comparator 전략 객체로 주입한다.

  • 📢 섹션 요약 비유: 전략 패턴은 운전자가 "지금부터 에코 모드로 달려" 하고 버튼을 누르는 것이고, 상태 패턴은 차가 언덕길을 만나면 엔진이 알아서 "등판 모드"로 기어를 바꾸는 것과 같습니다.


Ⅳ. 실무 적용 및 기술사적 판단

실무 시나리오

  1. 시나리오 — 결제 시스템 연동과 분기문 폭발: E-Commerce 플랫폼에서 초기에 신용카드 결제만 지원하다가 카카오페이, 네이버페이, 페이팔, 비트코인이 매달 추가되고 있다. OrderService.pay() 메서드 내부에 if (type == "KAKAO") ... else if (type == "NAVER") ...가 500줄이 넘어가며, 개발자들은 새 결제 수단을 넣을 때마다 두려움에 떨고 있다.

    • 아키텍트의 해결책: 즉시 전략 패턴을 도입해야 한다. PaymentStrategy 인터페이스를 만들고 각 결제 수단을 구현체로 찢어낸다. 그리고 Map<String, PaymentStrategy> 형태의 전략 팩토리(Strategy Factory) 패턴을 결합하여, 결제 요청이 오면 팩토리에서 알맞은 전략 객체를 꺼내와 strategy.pay() 단 한 줄만 실행하도록 리팩토링한다.
  2. 시나리오 — 런타임 A/B 테스트 및 알고리즘 동적 전환: 상품 추천 서버에서 사용자 반응률을 높이기 위해, 기존 '인기순 추천' 알고리즘과 새로 개발한 '딥러닝 협업 필터링 추천' 알고리즘을 50:50으로 A/B 테스트하고 싶다. 서버 재시작 없이 이를 즉각 적용해야 한다.

    • 아키텍트의 해결책: 추천 로직을 RecommendStrategy 인터페이스로 뽑아낸다. AWS AppConfig나 Feature Flag(토글) 시스템과 연동하여, 사용자의 UUID 해시값이 짝수면 딥러닝 전략 객체를, 홀수면 인기순 전략 객체를 Context에 주입(Inject)하여 동적으로 알고리즘을 스위칭하게 만든다.

도입 체크리스트

  • 기술적: 클라이언트가 각 Strategy 간의 차이점을 명확히 이해하고 적절한 전략을 선택해서 주입할 수 있는 구조인가? (전략 패턴의 단점 중 하나는 클라이언트가 모든 전략을 알고 있어야 한다는 점이다. 이 부담을 덜기 위해 팩토리 패턴 결합을 고려해야 한다.)
  • 설계적: 분리해낸 전략들이 단순히 데이터 구조만 다르고 로직은 같다면 굳이 전략 패턴을 쓸 필요가 없다. 전략들 간에 로직(행위)의 뚜렷한 차이가 있는가?

안티패턴

  • 단일 전략을 위한 무의미한 인터페이스 분리: 미래에 확장될 가능성이 전혀 없고 오직 하나의 구현체만 존재하는데도, 무조건 인터페이스와 구현체로 쪼개는 과도한 설계(Over Engineering). 이는 코드 탐색(Navigating) 비용만 늘린다.

  • Context가 전략의 구체적 내용에 의존함: Context 클래스 안에서 if (strategy instanceof KakaoStrategy) { ... } 와 같은 타입 체크를 하는 행위. 이는 다형성을 파괴하고 전략 패턴을 무용지물로 만드는 최악의 안티패턴이다.

  • 📢 섹션 요약 비유: 플러그(Context)와 콘센트(Strategy 인터페이스) 규격만 맞춰두면 한국형 어댑터든 유럽형 어댑터든 고민 없이 꽂을 수 있어야 하는데, 플러그 안에서 "이게 한국산인가?" 확인하고 다르게 행동한다면 패턴을 잘못 쓴 것입니다.


Ⅴ. 기대효과 및 결론

정량/정성 기대효과

구분리팩토링 전 (if-else 지옥)전략 패턴 도입 후개선 효과
정량새 기능 추가 시 Context 클래스 라인 수 50줄 증가Context 라인 수 변동 0줄 (새 클래스 파일만 생성)OCP 준수로 인한 사이드 이펙트 발생률 0% 수렴
정량분기문에 얽혀있어 단위 테스트용 Mock 객체 생성 불가각 전략 객체별로 완벽한 단위 테스트 가능테스트 커버리지 90% 이상 도달 가능
정성여러 개발자가 동시 수정 시 Git Merge Conflict 폭발각각 다른 전략 파일만 작업하므로 충돌 없음개발 생산성 극대화 및 코드 리뷰 시간 단축

미래 전망

  • 함수형 프로그래밍(Functional Programming)과의 융합: Java 8의 람다(Lambda) 표현식과 일급 함수(First-class Function) 지원으로 인해, 더 이상 무거운 전략 '클래스'를 만들지 않고도 함수 자체를 변수처럼 넘기는 방식으로 전략 패턴이 극도로 경량화되고 있다. executeStrategy(() -> { ... }) 한 줄로 전략 객체 정의와 주입이 끝난다.
  • 클라우드 네이티브/MSA: 비즈니스 로직 단위의 캡슐화를 넘어, 인프라 계층에서도 사이드카(Sidecar) 패턴을 통해 보안/로깅/재시도 전략을 애플리케이션 외부에서 동적으로 교체하는 아키텍처로 철학이 전이되고 있다.

참고 표준

  • GoF (Gang of Four): Behavioral Patterns - Strategy
  • Java Collection Framework: java.util.Comparator
  • Spring Framework: org.springframework.web.servlet.ViewResolver 및 각종 리졸버 아키텍처

소프트웨어 설계에서 가장 확실한 상수는 "요구사항은 반드시 변한다"는 것이다. 전략 패턴은 그 변화가 가장 극심한 '알고리즘'과 '비즈니스 룰'을 단단한 뼈대(Context)에서 칼같이 도려내는 수술 기법이다. 기술사는 어떤 로직이 수시로 변할 것인가(가변성)를 정확히 진단하고, 그곳에 전략 패턴이라는 인터페이스 방화벽을 세워 미래의 변경으로부터 코어 시스템을 지켜내는 설계자 역할을 수행해야 한다.

  • 📢 섹션 요약 비유: 건물을 지을 때, 벽돌로 모두 막아버려 나중에 창문을 뚫기 힘들게 짓는 것(하드코딩)이 아니라, 언제든 창문을 떼고 에어컨을 달 수 있도록 '빈 슬롯'을 설계해 두는 건축의 지혜입니다.

📌 관련 개념 맵 (Knowledge Graph)

개념 명칭관계 및 시너지 설명
OCP (Open-Closed Principle)새로운 전략 클래스를 추가하는 것(Open)만으로 기존 Context 클래스의 수정(Closed)을 피할 수 있게 해주는 패턴의 이론적 근간.
상태 패턴 (State Pattern)전략 패턴과 구조는 쌍둥이처럼 같으나, 객체의 '상태 변화'에 따라 '내부 로직'이 스스로 인터페이스를 교체한다는 의도(Intent)가 다르다.
템플릿 메서드 (Template Method)알고리즘을 캡슐화하는 목적은 같으나, 상속(Inheritance)을 사용하여 일부만 자식에게 구현을 맡기는 구조적 한계를 갖는 라이벌 패턴.
팩토리 메서드 (Factory Method)클라이언트가 너무 많은 전략을 알아야 하는 부담을 줄이기 위해, 상황에 맞는 전략 객체를 대신 생성해 주는 시너지 패턴.
의존성 주입 (Dependency Injection)객체의 생성과 전략의 주입을 프레임워크(IoC 컨테이너)에 완벽히 맡겨 전략 패턴을 완성시키는 엔터프라이즈 표준 기술.

👶 어린이를 위한 3줄 비유 설명

  1. 게임기(Context)가 하나 있는데, 여기에는 게임팩(Strategy)을 꽂을 수 있는 슬롯이 있어요.
  2. 마리오 게임팩을 꽂으면 마리오 게임이 실행되고, 포켓몬 게임팩을 꽂으면 포켓몬이 실행되죠. 게임기를 분해해서 고치지 않아도 팩만 갈아 끼우면 되잖아요?
  3. 이렇게 뼈대(게임기)는 가만히 두고, 내가 하고 싶은 행동(게임팩)만 언제든지 쏙쏙 갈아 끼울 수 있게 프로그래밍하는 방법을 **'전략 패턴'**이라고 한답니다!