핵심: 자주 바뀌는 행동을 별도 객체로 분리하고, 필요할 때 교체해서 사용한다.
『헤드 퍼스트 디자인 패턴』 Chapter 1에서는 오리 시뮬레이션 예제를 통해 전략 패턴(Strategy Pattern)을 설명한다.
이 장에서 중요한 것은 패턴의 정의를 외우는 것이 아니라,
기존 설계에서 무엇을 변경하기 어려웠고, 구조를 바꾸자 무엇이 쉬워졌는가?
를 이해하는 것이다.
1. 문제의 시작
처음에는 여러 종류의 오리가 Duck 클래스를 상속받도록 설계한다.
그런데 새로운 요구사항으로 Duck에 fly()를 추가하면 문제가 생긴다.
class Duck {
void fly() {
System.out.println("날아요");
}
}
모든 하위 클래스가 fly()를 상속받기 때문에 고무 오리처럼 날면 안 되는 오리까지 날게 된다.
고무 오리에서 fly()를 오버라이드할 수도 있지만, 날지 못하는 오리가 계속 늘어난다면 예외 처리도 계속 증가한다.
Flyable 인터페이스를 만들어 날 수 있는 오리만 구현하도록 해도 문제는 남는다.
여러 오리가 같은 방식으로 난다면 각 클래스에서 같은 코드를 반복해서 작성해야 하기 때문이다.
즉,
인터페이스를 사용한다고 구현 코드의 중복까지 자동으로 해결되는 것은 아니다.
2. 해결 방법: 행동을 객체로 분리한다
문제를 해결하기 위해 질문을 바꾼다.
"어떤 오리인가?"와 "어떻게 나는가?"를 따로 관리하면 어떨까?
비행 방식을 Duck에서 분리한다.
interface FlyBehavior {
void fly();
}
각 비행 방식을 별도의 클래스로 만든다.
class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("날개로 날아요");
}
}
class FlyNoWay implements FlyBehavior {
public void fly() {
System.out.println("날 수 없어요");
}
}
그리고 Duck은 직접 날지 않고 FlyBehavior 객체에게 비행을 맡긴다.
class Duck {
private FlyBehavior flyBehavior;
Duck(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
void performFly() {
flyBehavior.fly();
}
void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
}
사용할 때는 다음처럼 행동을 연결한다.
Duck duck = new Duck(new FlyNoWay());
duck.performFly();
duck.setFlyBehavior(new FlyWithWings());
duck.performFly();
Duck 객체 자체는 그대로지만, 연결된 행동 객체가 바뀌면서 비행 방식도 달라진다.
3. 여기서 배우는 설계 원칙
① 바뀌는 부분을 분리한다
오리 예제에서 자주 바뀌는 것은 비행 방식과 소리 내는 방식이다.
따라서 이런 행동을 Duck에서 분리해 별도로 관리한다.
이렇게 하면 비행 방식을 수정할 때 오리 클래스 전체를 건드릴 필요가 줄어든다.
② 구현보다 인터페이스에 맞춰 프로그래밍한다
FlyBehavior behavior = new FlyWithWings();
사용하는 쪽에서는 구체 클래스인 FlyWithWings보다 FlyBehavior라는 상위 타입을 기준으로 행동을 다룬다.
덕분에 다음과 같이 다른 구현으로 교체할 수 있다.
behavior = new FlyNoWay();
실제로 실행되는 fly()는 현재 참조하고 있는 객체에 따라 결정된다.
③ 상속보다 구성을 활용한다
MallardDuck extends Duck은 상속 관계다.
반면,
class Duck {
FlyBehavior flyBehavior;
}
는 Duck이 행동 객체를 가지고 있는 구성(Composition) 관계다.
그리고
flyBehavior.fly();
를 통해 실제 처리를 행동 객체에게 **위임(Delegation)**한다.
"상속보다 구성을 활용하라"는 말은 상속을 없애라는 뜻이 아니다.
오리 종류에는 상속을 사용하고, 변화가 많은 행동은 별도의 객체로 분리하자는 의미에 가깝다.
4. 전략 패턴이란?
전략 패턴은
같은 역할을 수행하는 여러 알고리즘을 각각 캡슐화하고, 필요에 따라 서로 교체해서 사용할 수 있도록 만드는 패턴
이다.
여기서 알고리즘은 복잡한 수학 공식만 의미하지 않는다.
오리 예제에서는
- 날개로 날기
- 날지 않기
- 로켓으로 날기
같은 행동 방식이 하나의 전략이 된다.
구조를 정리하면 다음과 같다.
Duck
└─ FlyBehavior
├─ FlyWithWings
├─ FlyNoWay
└─ FlyRocketPowered
Duck은 어떤 방식으로 나는지 직접 알 필요가 없다.
현재 가지고 있는 FlyBehavior 객체에게 요청하면 된다.
5. 전략 패턴을 적용하면 무엇이 좋아질까?
예를 들어 새로운 로켓 비행이 필요하다면 기존 Duck을 수정하기보다 새로운 전략을 추가할 수 있다.
class FlyRocketPowered implements FlyBehavior {
public void fly() {
System.out.println("로켓으로 날아요");
}
}
그리고 필요한 오리에게 연결한다.
duck.setFlyBehavior(new FlyRocketPowered());
즉, 행동을 추가하거나 변경할 때 변경 위치와 영향 범위를 더 명확하게 만들 수 있다.
다만 새로운 전략을 만든다고 모든 객체가 자동으로 바뀌는 것은 아니다.
어떤 객체가 새로운 전략을 사용할지는 별도로 지정해야 한다.
6. 자주 헷갈리는 부분
전략 패턴은 인터페이스를 만드는 패턴인가?
아니다.
인터페이스는 수단일 뿐이다.
핵심은
행동을 별도 객체로 분리하고 → 객체가 그 행동을 가지고 → 실행을 행동 객체에게 위임하는 구조다.
실행 중에 행동을 바꿔야만 전략 패턴인가?
아니다.
실행 중 교체할 수도 있지만, 객체 생성 시 전략을 하나 선택해서 계속 사용해도 된다.
날지 못하는데 왜 FlyBehavior인가?
FlyBehavior는 반드시 "날아간다"가 아니라 비행 요청을 어떻게 처리할 것인가를 의미한다.
따라서 "날지 않는다" 역시 하나의 행동 방식으로 표현할 수 있다.
7. 핵심 정리
Chapter 1에서 기억해야 할 내용은 세 가지다.
1. 바뀌는 부분을 분리한다.
2. 구현보다 인터페이스에 맞춰 프로그래밍한다.
3. 상속보다 구성을 활용한다.
전략 패턴의 핵심도 결국 같다.
변화가 많은 행동을 객체로 분리하고, 사용하는 객체가 그 행동을 조합해서 사용할 수 있게 만든다.
오리 예제에서는 오리의 종류와 비행 방식을 분리함으로써 새로운 행동을 추가하거나 기존 행동을 변경하기 쉬운 구조를 만들었다.
디자인 패턴을 공부할 때는 패턴 이름부터 외우기보다 먼저 다음 질문을 해보는 것이 좋다.
"이 프로그램에서 앞으로 자주 바뀔 부분은 무엇인가?"
그 부분을 찾아 분리하는 것이 전략 패턴을 이해하는 출발점이다.
'Design Pattern' 카테고리의 다른 글
| 데코레이터 패턴, 날씨 API 예제로 이해하기 (0) | 2026.10.05 |
|---|---|
| 옵저버 패턴으로 회원 탈퇴 후 남는 데이터 정리하기 (0) | 2026.09.30 |
| [디자인 패턴] 데코레이터 패턴, 기존 객체에 기능 덧붙이기 (0) | 2026.09.29 |
| [디자인 패턴] 옵저버 패턴, 값이 바뀌면 여러 객체에 알리기 (0) | 2026.09.28 |
| 객체지향이란 무엇인가 ? + 프로젝트 분석 (0) | 2026.09.09 |