728x90
320x100
1. 디자인 패턴이란?
- 정의: 반복 출현하는 설계 문제를 문제-맥락-해법 템플릿으로 정리한 이름 있는 해결책
Someone has already solved your problems.
→ "누군가 당신의 문제를 이미 해결했다": 디자인 패턴은 정해지거나 만들어진 프레임워크 또는 모듈 같은 기능이 아닌, 선배 개발자들이 경험하며 쌓아 온 노하우라는 것.
- 패턴 ≠ 코드 스니펫
→ 설계 수준의 역할·책임·협력에 대한 약속(의도·트레이드오프 포함) - 효과
- 팀의 공유 언어
- 팀원간의 의사소통이 편리해진다. 또한 통일성을 줄 수 있음.
- e.g.) “여긴 Strategy 패턴을 사용하자” 한마디로 의도와 구조가 모두 전달됨.
- 재사용 가능한 구조 습득, 설계 품질(확장성/유지보수성/가독성) 상승
- 리뷰·온보딩 속도 ↑, 설계 토론 비용 ↓
- 팀의 공유 언어
- 경험
- 코드가 아니라 설계 지침
- 발명된 게 아니라 발견된 것 (여러 개발자가 쌓아온 경험)
- 변화에 대응하는 방법
2. 왜 필요한가?
The one constant in soft ware development is CHANGE.
- 좋은 OOP 설계 = 재사용성 + 확장성 + 유지보수성.
- 문제 상황:
- 요구사항이 바뀜 → 모든 코드가 바뀜 → 테스트/확장 비용, 시간 증가
- 상속으로 공통화 → 예외가 생기는 순간 빈 오버라이드/플래그 분기/중복 확산
- 결과: 복잡하고 쓸데없이 긴 코드(e.g. if-else/switch 지옥), 테스트 어려움, 결합도↑, 버그율↑
- ⇒ 변하는 것을 식별해 고정된 것과 구조적으로 분리
- OOP 개념을 잘 안다고 해도, 유연하게 만들수 있어야 한다.
- 결국 변하는 것과 변하지 않는 것을 분리하는 게 관건
- 즉, “디자인 패턴”은 사실 원칙을 잘 활용한 결과물이다.
3. Strategy Pattern
1) 문제 상황
- 객체의 동작(알고리즘) 이 자주 바뀐다.
- 상속으로 해결하려다 보면 하위클래스 폭발, 중복/불일치 발생, 공통 변경이 어렵다.
- 근본 원인
- “동작(알고리즘)”이 변한다는 사실을 클래스 계층 내부에 고정해둔 것.
- 변화하는 부분을 추상화/분리하지 않은 설계.
- 책 예시 요약 → 오리 게임
- 오리 시뮬레이션 기본 설계: 상속 기반, 처음엔 괜찮아 보임
- 새로운 요구사항(fly) 추가 → RubberDuck, DecoyDuck 같은 특수 케이스에서, 모든 오리가 날아버리는 문제 발생
- 오버라이딩으로 임시 해결 → 클래스가 늘어나며 유지보수 지옥
- 인터페이스 시도 → 코드 중복, 관리 어려움
- → 첫 번째 설계 원칙 : 변하는 것과 변하지 않는 것을 분리하라
2) 인터페이스
- 기존 설계 = 유연성 부족
- 목표:
- 각각 오리마다 Behavior(날기/울기)을 자유롭게 할당
Duck인스턴스마다 서로 다른 Behavior 구현체를 연결 가능해야 함.
- 런타임에도 교체 가능해야 한다.
- (예: ModelDuck은 처음엔 못 날다가, 나중에 로켓 추진 Behavior로 교체 가능)
Duck에 Behavior setter를 두어 동적 교체 가능하게 설계. - 각각 오리마다 Behavior(날기/울기)을 자유롭게 할당
- → 두 번째 설계 원칙 : 구현이 아닌 인터페이스(추상 타입) 에 맞춰서 프로그래밍하라
- 구조
- 동작을 나타내는 타입(Behavior) 을 부모 클래스에서 분리 → 독립 인터페이스로 정의
FlyBehavior,QuackBehavior= Behavior 인터페이스 → Strategy
- 오리는 구현 세부사항을 모른다.
- 단지 “
fly()메시지를 보낼 수 있는 객체”라는 사실(= 인터페이스 계약)만 알고 있음
- 단지 “
- 호출 시 → 인터페이스에 위임
- 동작을 나타내는 타입(Behavior) 을 부모 클래스에서 분리 → 독립 인터페이스로 정의
Strategy (인터페이스)
: 알고리즘 규격 (공통 계약)
Strategy 구현체 (Concrete Strategy)
: 알고리즘의 실제 구현 클래스
Context (ex. Duck)
: Strategy를 “has-a” 관계로 보유하고 실행 시 위임
Client
: Context에 어떤 Strategy 구현체를 사용할지 주입/교체

Program to an interface?
- 꼭 자바의
interface키워드여야 하는 건 아님 - 핵심은 “구체 타입에 묶이지 않고, 추상(supertype)에 맞춰 코딩”
- 추상 클래스도 가능
- 중요한 건 구현이 아닌 추상에 의존한다는 점
구현에 맞춘 경우
Dog d = new Dog();
d.bark();
- 변수 타입이 Dog → 코드가 Dog에 고정됨
추상에 맞춘 경우
Animal animal = new Dog();
animal.makeSound();
- 변수 타입이 Animal → Cat으로 교체해도 호출부는 수정할 필요 없음
런타임 바인딩
Animal a = getAnimal(); // 런타임에 어떤 동물일지 모름
a.makeSound();
- 객체 생성까지 하드코딩하지 않고, 팩토리/DI 등을 활용 → 결합도↓, 확장성↑
오리 게임 예시에서
- 두 인터페이스 정의
FlyBehavior { fly(); }QuackBehavior { quack(); }
- Behavior 구현 클래스
- 날기:
FlyWithWings: 날개로 날 수 있음FlyNoWay: 날 수 없음FlyRocketPowered: 로켓으로 날 수 있음
- 울기:
Quack: 오리 울음소리Squeak: 장난감 오리 소리MuteQuack: 무음
- 날기:
- 설계 효과
- 오리 외의 다른 객체도
FlyBehavior,QuackBehavior재사용 가능 - 새 동작(Behavior)을 추가해도 기존 Duck이나 기존 Behavior 구현을 수정할 필요 없음 → OCP 만족
- 상속 대신 Composition + Delegation 으로 유연성과 확장성 확보
- 오리 외의 다른 객체도
- Strategy Pattern으로 오리 게임 완성하기
- 구조 통합
Duck(Context)FlyBehavior,QuackBehavior필드를 가짐 →perform메서드에서 Behavior에 위임
- 구체 오리(
MallardDuck,ModelDuck등) : 생성자에서 기본 Behavior 세팅 - Behavior 인터페이스 + 구현 클래스: 필요 시 런타임에 교체 가능
FlyBehavior→FlyWithWings,FlyNoWay,FlyRocketPoweredQuackBehavior→Quack,Squeak,MuteQuack
- 예시
-
더보기
// -- 인터페이스 interface FlyBehavior {void fly();} interface QuackBehavior {void quack();} // -- FlyBehavior 구현 클래스 class FlyWithWings implements FlyBehavior { @Override public void fly() { System.out.println("I'm flying!!"); } } class FlyNoWay implements FlyBehavior { @Override public void fly() { System.out.println("I can't fly"); } } class FlyRocketPowered implements FlyBehavior { @Override public void fly() { System.out.println("I'm flying with a rocket!"); } } // -- QuackBehavior 구현 클래스 class Quack implements QuackBehavior { @Override public void quack() { System.out.println("Quack"); } } class MuteQuack implements QuackBehavior { @Override public void quack() { System.out.println("<< Silence >>"); } } class Squeak implements QuackBehavior { @Override public void quack() { System.out.println("Squeak"); } } // -- Context = 전략을 사용하는 주체 // 행동은 Duck이 직접 구현하지 않고 전략에 위임 abstract class Duck { private FlyBehavior flyBehavior; // 현재 장착된 '날기' 전략 private QuackBehavior quackBehavior; // 현재 장착된 '울기' 전략 public Duck() {} public abstract void display(); public void performFly() { if (flyBehavior == null) throw new IllegalStateException("flyBehavior not set"); flyBehavior.fly(); } public void performQuack() { if (quackBehavior == null) throw new IllegalStateException("quackBehavior not set"); quackBehavior.quack(); } public void swim() { System.out.println("All ducks float, even decoys!"); } // 런타임 교체(Setter) public void setFlyBehavior(FlyBehavior fb) { this.flyBehavior = fb; } public void setQuackBehavior(QuackBehavior qb) { this.quackBehavior = qb; } // 하위 클래스/팩토리에서 초기 장착용(선택) protected void initBehaviors(FlyBehavior fb, QuackBehavior qb) { this.flyBehavior = fb; this.quackBehavior = qb; } } // -- 서브클래스 class MallardDuck extends Duck { public MallardDuck() { initBehaviors(new FlyWithWings(), new Quack()); } @Override public void display() { System.out.println("I'm a real Mallard duck"); } } class ModelDuck extends Duck { public ModelDuck() { initBehaviors(new FlyNoWay(), new Quack()); } @Override public void display() { System.out.println("I'm a model duck"); } } // Demo = Strategy 패턴의 유연성 확인 // - MallardDuck: 고정된 날개 비행/꽥 // - ModelDuck: 런타임에 로켓 비행으로 교체 public class MiniDuckSimulator { public static void main(String[] args) { Duck mallard = new MallardDuck(); mallard.display(); mallard.performQuack(); // Quack mallard.performFly(); // I'm flying!! mallard.swim(); System.out.println(); Duck model = new ModelDuck(); model.display(); model.performFly(); // I can't fly // 런타임 교체 model.setFlyBehavior(new FlyRocketPowered()); model.performFly(); // I'm flying with a rocket! model.performQuack(); // Quack } }
-
- 구조 통합
4. HAS-A
- 기존:
Duck IS-A Flying/Quacking thing→ 상속으로 행동 구현 - 개선:
Duck HAS-A FlyBehavior, QuackBehavior→ 구성(Composition) 으로 행동 보유 - 장점
- 런타임 교체 가능
- 재사용 가능 (다른 클래스에서도 활용)
- 상속처럼 계층 구조에 묶이지 않음
- → 세 번째 설계 원칙 : 상속보다는 객체 조합을 선택하자.
5. 핵심 설계 원칙
지금까지 나왔던 핵심 설계 원칙을 모아보면,
- Identify the aspects of your application that vary and separate them from what stays the same.
(변하는 것과 변하지 않는 것을 분리하라)- 변하는 부분은 캡슐화해서 독립시킨다.
- 상속은 공통화에는 좋지만 예외 케이스가 생기면 유지보수성이 급격히 떨어짐.
- Program to an interface, not an implementation.
(구현이 아닌 인터페이스에 맞춰 프로그래밍)- 호출자는 “무엇을 할 수 있나(계약)”만 알고, “어떻게 하느냐(구현)”는 모른다.
- 다형성을 극대화.
- Favor composition over inheritance.
(상속보다는 객체 조합)- HAS-A 관계를 통해 조립/교체 가능하게 한다.
- 런타임 교체, 조합 다양화 가능.
- 결합도↓, 응집도↑
보조 원칙 (연계)
- 개방-폐쇄 원칙(OCP): 확장에는 열려 있고 변경에는 닫혀 있어야 한다. → 확장시에도 기존 코드 변경X
- 단일 책임 원칙(SRP): 한 모듈은 한 가지 이유로만 바뀌어야 한다. → 기능이 하나의 책임만
6. 구현 예시 (Java)
시나리오
- 결제 시스템을 만든다고 가정.
- 결제 방식이 여러 가지 있음 (카드, 무통장 입금, 카카오페이, 네이버페이 등)
- 결제 방법은 계속 추가/변경될 수 있음 → 변화 부분을 캡슐화해야 함.
코드
// Strategy 인터페이스
public interface PaymentStrategy {
void pay(int amount);
}
// 구체 전략: 카드 결제
public class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(int amount) {
System.out.println(amount + "원을 신용카드(" + cardNumber + ")로 결제했습니다.");
}
}
// 구체 전략: 페이팔 결제
public class PaypalPayment implements PaymentStrategy {
private String email;
public PaypalPayment(String email) {
this.email = email;
}
@Override
public void pay(int amount) {
System.out.println(amount + "원을 PayPal(" + email + ")로 결제했습니다.");
}
}
// Context 클래스: 결제 처리기
public class Order {
private PaymentStrategy paymentStrategy;
// 실행 중에 결제 전략을 바꿀 수 있음
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void checkout(int amount) {
if (paymentStrategy == null) {
throw new IllegalStateException("결제 방식이 선택되지 않았습니다.");
}
paymentStrategy.pay(amount);
}
}
// 실행 예시
public class Main {
public static void main(String[] args) {
Order order = new Order();
// 카드 결제
order.setPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456"));
order.checkout(10000);
// 실행 중에 전략을 교체
order.setPaymentStrategy(new PaypalPayment("user@example.com"));
order.checkout(20000);
}
}
7. 마무리 (정리)
- OO Principles (원칙)
- Encapsulate what varies (변하는 것 캡슐화)
- Favor composition over inheritance (상속보다 합성)
- Program to interfaces, not implementations (인터페이스에 맞춰 프로그래밍)
- 👉 이 3가지는 앞으로 모든 디자인 패턴에 반복적으로 적용될 핵심 원칙.
- OO Patterns (패턴)
- 지금까지 배운 것 = Strategy Pattern
- 핵심: 알고리즘을 캡슐화 + 교체 가능 + 클라이언트 독립
- Strategy 패턴은 설계 원칙을 실천한 결과물이다.
- 변하는 것(알고리즘)을 분리하고 → 인터페이스에 맞춰 프로그래밍 → 상속 대신 합성으로 유연성 확보.
- 효과
- 알고리즘을 자유롭게 교체 가능 (런타임까지 포함)
- 코드 중복, 조건문 지옥 방지
- OCP/SRP 등 객체지향 원칙 준수
- 팀 내 “전략(Strategy) 패턴”이라는 이름만으로 설계 의도를 공유 가능
- 핵심
- 디자인 패턴은 “새로운 문법이나 트릭”이 아니라, 좋은 설계 원칙을 반복 검증한 결과
- Strategy는 그중 가장 기본적이고 강력한 패턴으로, 이후 나올 다른 패턴들의 이해를 위한 기초가 된다.
참고
⌜Head First Design Patterns⌟ Eric Freeman 외, O’Reilly Media, 2004
[Chapter 1] Intro to Design Pattern (p.1~35)
* 책을 참고해 필요한 내용을 정리한 것이므로, 책의 내용과 다를 수 있습니다.
300x250
반응형
GitHub 댓글