반응형
객체지향 프로그래밍에서 상속만으로는 해결할 수 없는 복잡한 요구사항들이 있습니다. 오늘은 이런 문제를 해결해주는 **전략 패턴(Strategy Pattern)**에 대해 알아보겠습니다.
🦆 문제 상황: 오리 시뮬레이터의 딜레마
SimUDuck이라는 오리 시뮬레이터를 개발한다고 가정해봅시다. 처음에는 단순히 상속을 통해 다양한 오리 종류를 구현했지만, 새로운 요구사항이 추가되면서 문제가 발생했습니다.
상속의 한계점
// 기존 설계의 문제점
public class Duck {
public void fly() { /* 모든 오리가 날 수 있다고 가정 */ }
public void quack() { /* 모든 오리가 꽥꽥거린다고 가정 */ }
}
public class RubberDuck extends Duck {
// 문제: 고무오리는 날 수 없고, 삑삑거린다!
@Override
public void fly() { /* 비워두거나 예외처리? */ }
@Override
public void quack() { /* 삑삑 소리로 변경 */ }
}
문제점들:
- 규격이 바뀔 때마다 모든 서브클래스의 메소드를 확인하고 수정해야 함
- 모든 오리가 같은 행동을 하지 않는데도 강제로 상속받아야 함
- 코드 재사용성이 떨어짐
💡 해결책: 전략 패턴 적용하기
전략 패턴을 적용하기 위해 세 가지 핵심 디자인 원칙을 활용합니다.
디자인 원칙 1: 변하는 부분과 변하지 않는 부분 분리하기
// 변하는 부분을 인터페이스로 분리
public interface FlyBehavior {
void fly();
}
public interface QuackBehavior {
void quack();
}
디자인 원칙 2: 구현보다는 인터페이스에 맞춰 프로그래밍하기
// 구체적인 행동 구현체들
public class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("날개로 날고 있어요!");
}
}
public class FlyNoWay implements FlyBehavior {
public void fly() {
System.out.println("날지 못합니다.");
}
}
public class FlyRocketPowered implements FlyBehavior {
public void fly() {
System.out.println("로켓 추진으로 날아갑니다!");
}
}
public class Quack implements QuackBehavior {
public void quack() {
System.out.println("꽥");
}
}
public class Squeak implements QuackBehavior {
public void quack() {
System.out.println("삑");
}
}
public class MuteQuack implements QuackBehavior {
public void quack() {
System.out.println("조용");
}
}
디자인 원칙 3: 상속보다는 구성(Composition) 활용하기
public abstract class Duck {
// 행동을 위임할 객체들
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public void performFly() {
flyBehavior.fly(); // 행동을 위임
}
public void performQuack() {
quackBehavior.quack(); // 행동을 위임
}
// 런타임에 행동을 변경할 수 있는 세터 메소드
public void setFlyBehavior(FlyBehavior fb) {
flyBehavior = fb;
}
public void setQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}
public abstract void display();
}
🔧 구체적인 구현
이제 다양한 오리들을 구현해봅시다.
public class MallardDuck extends Duck {
public MallardDuck() {
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}
public void display() {
System.out.println("저는 물오리입니다.");
}
}
public class ModelDuck extends Duck {
public ModelDuck() {
flyBehavior = new FlyNoWay();
quackBehavior = new MuteQuack();
}
public void display() {
System.out.println("저는 모형 오리입니다.");
}
}
🎯 실제 사용 예제
public class MiniDuckSimulator {
public static void main(String[] args) {
// 물오리 생성 및 테스트
Duck mallard = new MallardDuck();
mallard.performFly(); // "날개로 날고 있어요!"
mallard.performQuack(); // "꽥"
// 모형 오리 생성 및 테스트
Duck model = new ModelDuck();
model.performFly(); // "날지 못합니다."
model.performQuack(); // "조용"
// 런타임에 행동 변경!
model.setFlyBehavior(new FlyRocketPowered());
model.performFly(); // "로켓 추진으로 날아갑니다!"
}
}
🚀 확장성 예제: 오리 호출기
Duck 클래스를 상속받지 않고도 꽥꽥거리는 기능을 사용할 수 있습니다.
public class DuckCaller {
QuackBehavior quackBehavior;
public void call(QuackBehavior qb) {
quackBehavior = qb;
qb.quack();
}
}
// 사용 예제
DuckCaller duckCaller = new DuckCaller();
duckCaller.call(new Quack()); // "꽥"
duckCaller.call(new Squeak()); // "삑"
📚 전략 패턴의 정의
**전략 패턴(Strategy Pattern)**은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해줍니다. 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있습니다.
✨ 전략 패턴의 장점
1. 유연성 향상
- 런타임에 객체의 행동을 변경할 수 있습니다
- 새로운 알고리즘을 쉽게 추가할 수 있습니다
2. 코드 재사용성
- 같은 행동을 여러 클래스에서 재사용할 수 있습니다
- 중복 코드를 제거할 수 있습니다
3. 관리의 용이성
- 각 알고리즘이 독립적으로 존재하여 수정이 쉽습니다
- 새로운 요구사항에 대한 대응이 빠릅니다
4. 개방-폐쇄 원칙 준수
- 기존 코드 수정 없이 새로운 전략을 추가할 수 있습니다
🎯 핵심 포인트
- 변화하는 부분을 캡슐화하라: 자주 변경되는 알고리즘들을 별도의 클래스로 분리
- 인터페이스를 활용하라: 구체적인 구현보다는 추상화에 의존
- 구성을 활용하라: 상속보다는 객체 합성을 통해 유연성 확보
- 런타임 변경 가능: 필요에 따라 실행 중에 전략을 바꿀 수 있음
💭 마무리
전략 패턴은 단순해 보이지만 매우 강력한 디자인 패턴입니다. 특히 다양한 알고리즘이나 정책이 필요한 상황에서 코드의 유연성과 확장성을 크게 향상시킬 수 있습니다.
객체지향 설계에서 "변화"를 다루는 것은 항상 중요한 과제입니다. 전략 패턴을 통해 변화에 유연하게 대응할 수 있는 설계를 만들어보세요!
다음 포스팅에서는 옵저버 패턴(Observer Pattern)에 대해 알아보겠습니다. 많은 관심 부탁드립니다! 🙌
반응형