| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- DB
- 트러블슈팅
- 백엔드
- 로드밸런서
- Effective Java
- 스케줄러
- 자바
- 디자인 패턴
- 이펙티브 자바
- 토스
- 추상클래스
- Til
- Spring
- 코드카타
- 프로그래머스
- spring boot
- 빌더 패턴
- 김영한
- Spring Batch
- java
- redis
- 계산기
- 프록시 패턴
- 템플릿 메서드 패턴
- lv1
- GoF 23
- 배치
- 성능 개선
- 스프링 배치
- 스프링
- Today
- Total
김코딩
전략 패턴(Strategy Pattern) 본문
디자인 패턴 시리즈의 아홉 번째 주제는 전략 패턴(Strategy Pattern)이다. 이번부터는 행위 패턴으로 넘어간다.
구조 패턴이 "객체들을 어떻게 조합할까?"였다면, 행위 패턴은 "객체들이 어떻게 협력하고 책임을 분담할까?"에 대한 해답이다.
예를 들어, 내비게이션 앱을 생각해보자. 목적지까지 가는 방법은 여러 가지다.
- 자동차로 갈 수도 있고
- 대중교통을 이용할 수도 있고
- 도보로 갈 수도 있다.
상황에 따라 최적의 경로 찾기 전략을 선택해야 한다. 전략 패턴은 이처럼 여러 알고리즘 중 하나를 상황에 맞게 선택할 수 있게 해주는 패턴이다.
이번 글에서는 전략 패턴이 무엇인지, 왜 필요한지, 어떻게 구현하는지 알아보자.
전략 패턴의 정의
전략 패턴(Strategy Pattern)은 알고리즘군을 정의하고 각각을 캡슐화하여, 이들을 상호 교환 가능하게 만드는 패턴이다.
전략 패턴을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.
핵심 특징:
- 알고리즘을 캡슐화한다
- 실행 중에 알고리즘을 선택할 수 있다
- 알고리즘 사용 코드와 분리된다
- if-else 지옥을 제거한다
현실 비유:
- 내비게이션 경로 찾기 (자동차/대중교통/도보)
- 결제 수단 선택 (카드/현금/포인트)
- 압축 방식 선택 (ZIP/RAR/7Z)
왜 전략 패턴이 필요할까?
온라인 쇼핑몰에서 결제 시스템을 개발한다고 가정해보자.
// 문제가 있는 코드
public class PaymentService {
public void processPayment(String paymentType, int amount) {
if (paymentType.equals("CARD")) {
System.out.println("신용카드로 " + amount + "원 결제");
System.out.println("카드 승인 요청...");
System.out.println("영수증 발행");
} else if (paymentType.equals("CASH")) {
System.out.println("현금으로 " + amount + "원 결제");
System.out.println("현금 영수증 발행");
} else if (paymentType.equals("POINT")) {
System.out.println("포인트로 " + amount + "원 결제");
System.out.println("포인트 차감");
System.out.println("포인트 내역 저장");
} else if (paymentType.equals("KAKAO")) {
System.out.println("카카오페이로 " + amount + "원 결제");
System.out.println("카카오 API 호출...");
System.out.println("결제 승인");
} else if (paymentType.equals("NAVER")) {
System.out.println("네이버페이로 " + amount + "원 결제");
System.out.println("네이버 API 호출...");
System.out.println("결제 승인");
}
// 새로운 결제 수단이 추가될 때마다 if-else 추가...
}
}
// 사용
public class Main {
public static void main(String[] args) {
PaymentService service = new PaymentService();
service.processPayment("CARD", 10000);
service.processPayment("KAKAO", 20000);
}
}
문제점:
- if-else 지옥: 결제 수단이 늘어날수록 코드가 길어짐
- OCP 위반: 새로운 결제 수단 추가 시 기존 코드 수정 필요
- SRP 위반: 하나의 클래스가 모든 결제 로직을 처리
- 테스트 어려움: 특정 결제 수단만 테스트하기 어려움
- 유지보수 악몽: 결제 로직 변경 시 전체 메서드 수정
전략 패턴은 "각 결제 방법을 독립적인 전략으로 분리하자"로 이 문제를 해결한다.
순수 Java로 구현하기
1단계: 전략 인터페이스 정의
// 결제 전략 인터페이스
public interface PaymentStrategy {
void pay(int amount);
}
2단계: 구체적인 전략 구현
// 신용카드 결제 전략
public class CardPaymentStrategy implements PaymentStrategy {
private String cardNumber;
public CardPaymentStrategy(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(int amount) {
System.out.println("💳 신용카드로 " + amount + "원 결제");
System.out.println("카드번호: " + cardNumber);
System.out.println("카드 승인 완료\n");
}
}
// 현금 결제 전략
public class CashPaymentStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("💵 현금으로 " + amount + "원 결제");
System.out.println("현금 영수증 발행\n");
}
}
// 포인트 결제 전략
public class PointPaymentStrategy implements PaymentStrategy {
private String memberId;
public PointPaymentStrategy(String memberId) {
this.memberId = memberId;
}
@Override
public void pay(int amount) {
System.out.println("⭐ 포인트로 " + amount + "원 결제");
System.out.println("회원ID: " + memberId);
System.out.println("포인트 차감 완료\n");
}
}
// 카카오페이 결제 전략
public class KakaoPayStrategy implements PaymentStrategy {
private String kakaoId;
public KakaoPayStrategy(String kakaoId) {
this.kakaoId = kakaoId;
}
@Override
public void pay(int amount) {
System.out.println("💬 카카오페이로 " + amount + "원 결제");
System.out.println("카카오ID: " + kakaoId);
System.out.println("카카오 API 호출 완료\n");
}
}
3단계: 컨텍스트 클래스 (전략 사용)
// 결제 처리 클래스 (컨텍스트)
public class PaymentService {
private PaymentStrategy paymentStrategy;
// 전략을 선택할 수 있음
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
// 선택된 전략으로 결제 처리
public void processPayment(int amount) {
if (paymentStrategy == null) {
throw new IllegalStateException("결제 수단을 선택해주세요");
}
paymentStrategy.pay(amount);
}
}
4단계: 사용 예시
public class Main {
public static void main(String[] args) {
PaymentService paymentService = new PaymentService();
// 신용카드로 결제
System.out.println("=== 고객 1 ===");
paymentService.setPaymentStrategy(new CardPaymentStrategy("1234-5678-9012"));
paymentService.processPayment(15000);
// 포인트로 결제
System.out.println("=== 고객 2 ===");
paymentService.setPaymentStrategy(new PointPaymentStrategy("user123"));
paymentService.processPayment(5000);
// 카카오페이로 결제
System.out.println("=== 고객 3 ===");
paymentService.setPaymentStrategy(new KakaoPayStrategy("kakao_user"));
paymentService.processPayment(25000);
// 런타임에 전략 변경 가능
System.out.println("=== 고객 3 결제 수단 변경 ===");
paymentService.setPaymentStrategy(new CashPaymentStrategy());
paymentService.processPayment(10000);
}
}

장점:
- if-else 제거됨
- 새로운 결제 수단 추가 시 기존 코드 수정 불필요
- 각 전략을 독립적으로 테스트 가능
- 런타임에 전략 변경 가능
실전 예제: 할인 정책
// 할인 전략 인터페이스
public interface DiscountStrategy {
int calculateDiscount(int price);
}
// 정액 할인
public class FixedDiscountStrategy implements DiscountStrategy {
private int discountAmount;
public FixedDiscountStrategy(int discountAmount) {
this.discountAmount = discountAmount;
}
@Override
public int calculateDiscount(int price) {
return Math.max(0, price - discountAmount);
}
}
// 정률 할인
public class PercentDiscountStrategy implements DiscountStrategy {
private int discountPercent;
public PercentDiscountStrategy(int discountPercent) {
this.discountPercent = discountPercent;
}
@Override
public int calculateDiscount(int price) {
return price * (100 - discountPercent) / 100;
}
}
// 할인 없음
public class NoDiscountStrategy implements DiscountStrategy {
@Override
public int calculateDiscount(int price) {
return price;
}
}
// VIP 할인 (정률 + 추가 할인)
public class VIPDiscountStrategy implements DiscountStrategy {
@Override
public int calculateDiscount(int price) {
int percentDiscount = price * 80 / 100; // 20% 할인
return Math.max(0, percentDiscount - 5000); // 추가 5000원 할인
}
}
// 주문 처리
public class Order {
private int price;
private DiscountStrategy discountStrategy;
public Order(int price) {
this.price = price;
this.discountStrategy = new NoDiscountStrategy(); // 기본값
}
public void setDiscountStrategy(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
public int calculateFinalPrice() {
return discountStrategy.calculateDiscount(price);
}
public void printOrder() {
System.out.println("원가: " + price + "원");
System.out.println("최종가: " + calculateFinalPrice() + "원\n");
}
}
// 사용
public class Main {
public static void main(String[] args) {
// 일반 고객
Order order1 = new Order(50000);
order1.setDiscountStrategy(new NoDiscountStrategy());
System.out.println("=== 일반 고객 ===");
order1.printOrder();
// 신규 고객 (10% 할인)
Order order2 = new Order(50000);
order2.setDiscountStrategy(new PercentDiscountStrategy(10));
System.out.println("=== 신규 고객 (10% 할인) ===");
order2.printOrder();
// 프로모션 (5000원 할인)
Order order3 = new Order(50000);
order3.setDiscountStrategy(new FixedDiscountStrategy(5000));
System.out.println("=== 프로모션 (5000원 할인) ===");
order3.printOrder();
// VIP 고객
Order order4 = new Order(50000);
order4.setDiscountStrategy(new VIPDiscountStrategy());
System.out.println("=== VIP 고객 ===");
order4.printOrder();
}
}
전략 패턴의 장점
- OCP 준수: 새로운 전략 추가 시 기존 코드 수정 불필요
- SRP 준수: 각 전략이 독립적인 책임
- if-else 제거: 깔끔한 코드
- 테스트 용이: 각 전략을 독립적으로 테스트
- 유연성: 런타임에 전략 변경 가능
- 재사용성: 전략을 여러 곳에서 재사용
전략 패턴의 단점
- 클래스 증가: 전략마다 클래스 생성 필요
- 클라이언트 인지: 클라이언트가 전략의 차이를 알아야 함
- 컨텍스트 복잡도: 전략이 많아지면 선택 로직이 복잡해질 수 있음
Spring에서의 전략 패턴
Spring에서 전략 패턴은 매우 자주 사용된다.
// 결제 전략 인터페이스
public interface PaymentStrategy {
boolean pay(int amount);
String getPaymentType();
}
// 구체적인 전략들
@Component
public class CardPaymentStrategy implements PaymentStrategy {
@Override
public boolean pay(int amount) {
System.out.println("카드 결제: " + amount);
return true;
}
@Override
public String getPaymentType() {
return "CARD";
}
}
@Component
public class KakaoPayStrategy implements PaymentStrategy {
@Override
public boolean pay(int amount) {
System.out.println("카카오페이 결제: " + amount);
return true;
}
@Override
public String getPaymentType() {
return "KAKAO";
}
}
// 전략 팩토리 (전략 선택)
@Component
public class PaymentStrategyFactory {
private final Map<String, PaymentStrategy> strategies;
// Spring이 모든 PaymentStrategy 구현체를 주입
public PaymentStrategyFactory(List<PaymentStrategy> strategyList) {
this.strategies = strategyList.stream()
.collect(Collectors.toMap(
PaymentStrategy::getPaymentType,
strategy -> strategy
));
}
public PaymentStrategy getStrategy(String paymentType) {
PaymentStrategy strategy = strategies.get(paymentType);
if (strategy == null) {
throw new IllegalArgumentException("지원하지 않는 결제 수단: " + paymentType);
}
return strategy;
}
}
// 서비스에서 사용
@Service
public class OrderService {
private final PaymentStrategyFactory strategyFactory;
public OrderService(PaymentStrategyFactory strategyFactory) {
this.strategyFactory = strategyFactory;
}
public void processOrder(String paymentType, int amount) {
PaymentStrategy strategy = strategyFactory.getStrategy(paymentType);
boolean success = strategy.pay(amount);
if (success) {
System.out.println("주문 완료!");
}
}
}
실무에서 언제 사용할까?
사용하면 좋은 경우:
- 같은 목적의 여러 알고리즘이 있을 때
- if-else나 switch-case가 많을 때
- 런타임에 알고리즘을 선택해야 할 때
- 알고리즘이 자주 변경되거나 추가될 때
사용하지 않아도 되는 경우:
- 알고리즘이 하나뿐일 때
- 알고리즘이 거의 변하지 않을 때
- 간단한 if-else로 충분할 때
핵심 원칙:
"여러 알고리즘 중 하나를 선택해야 한다면 전략 패턴을 고려하라"
전략 패턴 vs 팩토리 메서드 패턴
둘 다 런타임에 선택한다는 공통점이 있지만, 목적이 다르다.
| 구분 | 전략 패턴 | 팩토리 메서드 패턴 |
| 패턴 분류 | 행위 패턴 | 생성 패턴 |
| 목적 | 알고리즘(행동) 실행 방법을 선택 | 객체 생성 방법을 선택 |
| 초점 | “어떻게 할까?” | “무엇을 만들까?” |
| 변경 가능성 | 언제든지 전략(알고리즘) 교체 가능 | 생성 후에는 객체 타입이 고정 |
| 사용 시점 | 알고리즘 선택이 중요할 때 | 객체 생성이 복잡할 때 |
| 핵심 역할 | 여러 알고리즘을 캡슐화하여 교체 가능 | 객체 생성을 서브클래스에 위임 |
코드로 비교
팩토리 메서드 패턴:
// 목적: 어떤 결제 객체를 생성할까?
public class PaymentFactory {
public Payment createPayment(String type) {
if (type.equals("CARD")) {
return new CardPayment(); // 객체 생성
} else if (type.equals("KAKAO")) {
return new KakaoPayment(); // 객체 생성
}
return null;
}
}
// 사용
Payment payment = factory.createPayment("CARD");
payment.pay(10000);
// payment는 한 번 생성되면 타입 변경 불가
전략 패턴:
// 목적: 어떤 결제 방식으로 처리할까?
public class PaymentService {
private PaymentStrategy strategy;
public void setStrategy(PaymentStrategy strategy) {
this.strategy = strategy; // 전략 교체
}
public void pay(int amount) {
strategy.pay(amount); // 전략 실행
}
}
// 사용
PaymentService service = new PaymentService();
service.setStrategy(new CardStrategy());
service.pay(10000);
// 같은 객체로 전략 변경 가능
service.setStrategy(new KakaoStrategy());
service.pay(20000);
실무에서는 함께 사용
두 패턴을 조합하면 더욱 강력하다.
// 팩토리로 전략 객체를 생성
@Component
public class PaymentStrategyFactory {
public PaymentStrategy createStrategy(String type) {
// 팩토리 메서드로 전략 생성
switch (type) {
case "CARD": return new CardPaymentStrategy();
case "KAKAO": return new KakaoPaymentStrategy();
case "POINT": return new PointPaymentStrategy();
default: throw new IllegalArgumentException("Unknown type");
}
}
}
// 서비스에서 사용
@Service
public class OrderService {
private final PaymentStrategyFactory factory;
private final PaymentService paymentService;
public void checkout(String paymentType, int amount) {
// 1. 팩토리로 전략 생성
PaymentStrategy strategy = factory.createStrategy(paymentType);
// 2. 전략 패턴으로 실행
paymentService.setStrategy(strategy);
paymentService.process(amount);
}
}
핵심:
- 팩토리: 객체를 어떻게 만들까?
- 전략: 알고리즘을 어떻게 실행할까?
결론
전략 패턴은 알고리즘을 캡슐화하여 유연하게 교체할 수 있게 하는 패턴이다.
기억해야 할 포인트:
- 알고리즘을 캡슐화한다
- if-else 지옥을 제거한다
- 런타임에 전략 변경 가능
- Spring에서 매우 자주 사용된다
- 새로운 전략 추가가 쉽다
다음 글에서는 알고리즘의 골격을 정의하는 템플릿 메서드 패턴을 알아볼 것이다.
다음 글 예고: 템플릿 메서드 패턴 - 알고리즘의 뼈대를 정의하라
'디자인 패턴' 카테고리의 다른 글
| 옵저버 패턴(Observer Pattern) (1) | 2026.01.14 |
|---|---|
| 템플릿 메서드 패턴(Template Method Pattern) (1) | 2026.01.14 |
| 프록시 패턴(Proxy Pattern) (0) | 2026.01.13 |
| 데코레이터 패턴(Decorator Pattern) (0) | 2026.01.13 |
| 어댑터 패턴(Adapter Pattern) (0) | 2026.01.13 |