Goal
- 전략 패턴에 대해 알아보고 이해한다
- 전략 패턴이 필요한 경우의 예시를 알아본다
- 문제를 파악하고, 디자인 원칙에 대해 알아본다
- 디자인 원칙에 입각한 전략패턴 이해하기
- 캡슐화된 행동 살펴보고 이해하기
- 클래스를 합치는 방법은 무엇인가?
전략 패턴 (Strategy Parttern)
객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행위 디자인 패턴 이다. 여기서 '전략'이란 일종의 알고리즘이 될 수 도 있으며, 기능이나 동작이 될 수도 있는 특정한 목표를 수행하기 위한 행동 계획을 말한다.
왜 or 언제 전략 패턴을 사용하는가?
어떤 일을 수행하는 알고리즘이 여러가지 일때, 동작들을 미리 전략으로 정의함으로써 손쉽게 전략을 교체할 수 있는, 알고리즘 변형이 빈번하게 필요한 경우에 적합한 패턴이기 때문이다
전략 패턴 사용 시 장점
- 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다
- 달라지는 부분을 찾아 나머지 코드에 영향을 주지 않도록 캡슐화한다 => OCP 개방폐쇄의 원칙
- 의도치 않게 발생하는 일을 줄이며 시스템 유연성 향상
as - is 오리 시뮬레이션 게임 관점에서 생각해보기
상황 : 오리 시뮬레이션 게임을 만드는 회사가 있다. 이 게임에는 1.헤엄도 치고, 2.꽥꽥 소리도 내는 등 매우 다양한 오리가 등장한다.
이 시스템을 처음 디자인 한 사람은 표준 객체지향 기법을 사용하여 Duck이라는 슈퍼클래스를 만들고, 그 클래스를 확장해서 서로 다른 종류의 오리를 만들었다.
요구 사항 : 오리들이 날 수 있도록 하라
해결 방안 1 : Duck 클래스에 fly() 메소드만 추가해서 모든 오리가 그 메소드를 상속 받게 하기
문제점 : Duck이라는 슈퍼클래스에 fly() 메소드를 추가하면서 일부 서브 클래스에 적합하지 않은 행동이 추가됨
=> 일부만 고쳤는데 전체에 고무오리가 날아다니는 오류 발생 => 코드를 재사용했지만 유지보수 관점에서는 좋지 않음
해결 방안 2 : 상속을 하여 아무것도 하지 않도록 오버라이드 하기
문제점 : 제품이 자주 업데이트가 되면 규격이 계속 바뀔 것이므로 상속을 활용하면 규격이 바뀔때마다 Duck의 서브클래스 fly()와 quack()메소드를 일일이 살펴보고 상황에 따라 오버라이드 해야함
해결 방안 3 : 인터페이스를 활용하여 행위를 따로 분리하기
문제점 : 모든 서브클래스에 날거나 꽥꽥거리는 기능이 있어야 하는 것은 아니므로 상속이 올바른 방법은 아니다. 서브클래스에서 Flyable, Quackable을 구현해서 (고무 오리가 날아다니는 것과 같은) 일부 문제점은 해결할 수 있지만, 코드를 재사용하지 않으므로 코드관리에 문제가 발생하며, 날 수 있는 오리 중에서도 날아다니는 방식이 서로 다를 수 있다면 서브클래스 모두 고쳐야하는 번거로움이 존재한다.
문제를 명확하게 파악하기
해결 방안으로 제시된 위의 3가지 경우는 문제점이 여전히 존재해보인다. 하지만 이 상황에서 어울릴만한 디자인 원칙이 있다.
디자인 원칙
01. 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
02. 바뀌는 부분은 따로 뽑아서 캡슐화한다. 그러면 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 고치거나 확장할 수 있다
=> OCP 개방폐쇄의 원칙
디자인원칙을 준수하는 4단계
1. 바뀌는 부분과 그렇지 않은 부분 분리하기
- 그대로 있는 부분 : Duck 클래스
- 바뀌는 부분 : fly(), quack()
- 클래스 집합(set)을 2개 생성 (fly관련, quack관련)
- 각 클래스 집합에는 각각의 행동을 구현한 것을 넣는다
2.오리의 행동을 디자인하기
- 목표 정하기
- Duck의 인스턴스 할당
- 특정 형식의 나는 행동으로 초기화하는 방법
- 오리의 행동을 동적으로 바꾸기
즉, Duck클래스에 행동과 관련된 setter 메소드를 포함해서 프로그램 실행중에도 MallardDuck의 나는 행동을 바꾸기
- 각 행동을 인터페이스로 사용하여 구현
- FlyBehavior 인터페이스의 fly()
- FlyBehavior를 구현한 FlyWithWings 클래스
- FlyBehavior를 구현한 FlyNoWay 클래스
이제부터 Duck의 행동은 별도의 클래스 안에 들어있다. 그러면 Duck 클래스에서는 그 행동을 구체적으로 구현할 필요가 없다.
즉, 실제 행동 구현(FlyBehavior, QuackBehavior를 구현하는 클래스에 코딩되어있는 구체적인 행동)은 Duck 서브클래스에 국한되지 않는다.
3.오리의 행동을 구현하기
- FlyBehavior, QuackBehavior 2개의 인터페이스에 맞춰서 구체적인 행동을 구현하는 클래스 만들기
위와 같이 디자인하면 다른 형식의 객체에서도 나는 형식과 꽥꽥거리는 행동을 재사용할 수 있다. 더 이상 Duck 클래스 안에 숨겨져 있지 않기 때문이다. 그리고 기존의 행동 클래스를 수정하거나 날아다니는 행동을 사용하는 Duck 클래스를 수정 없이 새로운 행동을 추가할 수 있다. => 상속을 사용시 부담을 떨쳐버리고 재사용의 장점을 누릴 수 있다.
4.오리의 행동 통합하기
가장 중요한 점은 나는 행동과 꽥꽥거리는 행동을 Duck클래스(또는 그 서브클래스)에서 정의한 메소드를 써서 구현하지 않고 다른 클래스에 위임하는 것이다.
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck() {}
public abstract void display();
public void performFly(){
flyBehavior.fly();
}
public void performQuack(){
quackBehavior.quack();
}
public void swim(){
System.out.println("모든 오리는 물에 뜹니다. 가짜 오리도 뜨죠");
}
}
- flyBehavior와 quackBehavior라는 인터페이스 형식의 인스턴스 변수를 추가
- 각 오리 객체에서는 실행 시 이 변수에 특정 행동 형식(FlyWithWings, Squack 등)의 레퍼런스를 다형적으로 설정
- 나는 행동과 꽥꽥거리는 행동은 인터페이스로 분리했으므로, Duck클래스와 모든 서브 클래스에서 fly(), quack()메소드 제거
- Duck클래스에 performFly(), performQuack()이라는 메소드 추가
- quackBehavior.quack();
- 꽥꽥거리는 행동을 직접 처리하는 대신, quackBehavior로 참조되는 객체에 그 행동을 위임한다
public class MaralldDuck extends Duck {
public MaralldDuck() {
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}
@Override
public void display() {
System.out.println("저는 물오리 인데용");
}
}
- MallardDuck은 Duck클래스에서 quackBehavior과 flyBehavior 인스턴스를 상속 받는다
- MallardDuck이 꽥꽥거리는 행동을 처리할 때는 Quack클래스를 사용하므로 performQauck()이 호출되면 꽥꽥거리는 행동은 Quack객체에게 위임된다. 결과적으로 꽥꽥 소리를 들을 수 있음, 그리고 FlyBehavior 형식으로는 FlyWithWings를 사용한다.
오리 코드 테스트
Duck 클래스
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck() {}
public abstract void display();
public void performFly(){
flyBehavior.fly();
}
public void performQuack(){
quackBehavior.quack();
}
public void swim(){
System.out.println("모든 오리는 물에 뜹니다. 가짜 오리도 뜨죠");
}
}
FlyBehavior 인터페이스
public interface FlyBehavior {
public void fly();
}
FlyWithWings, FlyNoWay 행동 구현 클래스
public class FlyWithWings implements FlyBehavior{
@Override
public void fly() {
System.out.println("날고 있어요");
}
}
public class FlyNoway implements FlyBehavior{
@Override
public void fly() {
System.out.println("저는 못 날아요");
}
}
QuackBehavior 인터페이스
public interface QuackBehavior {
public void quack();
}
Quack, MuteQuack, Squeak 행동 구현 클래스
public class Quack implements QuackBehavior{
@Override
public void quack() {
System.out.println("꽥");
}
}
public class MuteQuack implements QuackBehavior{
@Override
public void quack() {
System.out.println("조용");
}
}
public class Squack implements QuackBehavior{
@Override
public void quack() {
System.out.println("삑");
}
}
테스트 클래스
public class MiniDuckSimulator {
public static void main(String[] args){
Duck mallard = new MaralldDuck();
mallard.performQuack();
mallard.performFly();
}
}
결과
꽥
날고 있어요
동적으로 행동 지정하기
1. Duck 클래스에 아래 메소드 추가 - 언제든지 오리의 행동을 즉석에서 바꿀 수 있도록!
public void setFlyBehavior(FlyBehavior fb) {
this.flyBehavior = fb;
}
public void setQuackBehavior(QuackBehavior qb) {
this.quackBehavior = qb;
}
2. Duck의 서브클래스(ModelDuck.java)를 새로 만든다
public class ModelDuck extends Duck{
public ModelDuck() {
flyBehavior = new FlyNoway();
quackBehavior = new Quack();
}
@Override
public void display() {
System.out.println("나는 모형 오리");
}
}
-> 날지 못하는 오리
3. FlyBehavior 형식의 클래스(FlyRocketPowerd.java)를 새로 만든다
public class FlyRocketPowered implements FlyBehavior{
@Override
public void fly() {
System.out.println("로켓 추진으로 날아갑니다");
}
}
4. 테스트 클래스 ModelDuck 추가하고 로켓 추진 기능 부여
public class MiniDuckSimulator {
public static void main(String[] args){
Duck mallard = new MaralldDuck();
mallard.performQuack();
mallard.performFly();
Duck model = new ModelDuck();
model.performFly();
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();
}
}
결과
꽥
날고 있어요
저는 못 날아요
로켓 추진으로 날아갑니다
=> 실행 중에 오리의 행동을 바꾸고 싶으면 원하는 행동에 해당하는 Duck의 세타 메소드를 호출한다
캡슐화된 행동 살펴보기
클라이언트에서는 나는 행동과 꽥꽥거리는 행동을 캡슐화된 알고리즘으로 구현한다
두 클래스를 합치는 방법
"A에는 B가 있다". 각 오리에는 FlyBehavior와 QuackBehavior가 있으며, 각각 나는 행동과 꽥꽥거리는 행동을 위임받는다.
이런 식으로 두 클래스를 합치는 것을 구성(composition)을 이용한다라고 한다.
=> 오리 클래스 에서는 행동을 상속받는 대신, 올바른 행동 객체로 구성되어 행동을 부여받는다.
디자인 원칙
03. 상속보다는 구성을 사용한다
참고
헤드퍼스트 디자인 패턴
후 다이어그램 그리느라 시간이 더 소요됬지만 만족스럽다 !
이제 구성은 완벽히 이해가 되었다 히히
ㅎㅎㅎ
'JAVA > 디자인 패턴' 카테고리의 다른 글
[Factory Pattern] 팩토리 패턴 - 팩토리 메소드 (0) | 2023.07.09 |
---|---|
[Simple Factory] 간단한 팩토리 (0) | 2023.07.07 |
[Decorator Pattern] 데코레이터 패턴 (0) | 2023.07.04 |
[Proxy Pattern] 프록시 패턴 - 보호 프록시 (0) | 2023.07.04 |
[Proxy Pattern] 프록시 패턴 - 가상 프록시 (0) | 2023.07.03 |