Development Project

[ 헤드퍼스트 디자인패턴 - 11/06 ] Chap1. 디자인 패턴 소개와 전략패턴 본문

Web Tech/Design Pattern

[ 헤드퍼스트 디자인패턴 - 11/06 ] Chap1. 디자인 패턴 소개와 전략패턴

나를 위한 시간 2022. 11. 6. 20:17

헤드퍼스트 디자인패턴 개정판을 읽은 후 터득한 내용을 정리해 올린 포스팅임을 밝힙니다.


 

 오리 시뮬레이션 게임, SimUduck

해당 시뮬레이션 게임의 초기 디자인은, Duck이라는 슈퍼클래스를 상속받는 여러 종류의 오리클래스로 구성됨

ex) MallardDuck(청둥오리), RedheadDuck(머리가 빨간 오리), RubberDuck(러버덕 - 고무오리) ...

 

 

▶ 오리에게 날 수 있도록 하려면?

1st 접근) 슈퍼클래스인 Duck에 fly() 메소드를 추가

모든 오리들에게 fly라는 기능이 추가되어버리기 때문에, 날지못하는 오리도 날수 있게됨.

몇 오리들은 날지못하게 바꾸기 위해서는 오버라이드가 필수적임.

 

2nd 접근) Duck에 fly() 메소드를 추가하고, 오버라이드를 통해 다른 행동을 하도록 변경

1. 서브클래스에서 코드가 중복된다.

날수있는 오리도 여러마리, 날지못하는 오리도 여러마리가 될 수 있음

즉, 날수있는 메소드로 Duck을 설정해두면, 날수없는오리들은 오버라이드를 받아서 날지못하도록 바꿔주어야하므로 비효율적임

 

2. 실행 시에 특징을 바꾸기 힘들다.

날 수 있었던 오리가 실행도중 날 수 없도록, 날 수 없었던 오리가 실행도중 날 수 있도록 바꾸기 위해서는 코드를 또다시 작성해주어야 하므로 좋지않다.

 

3. 모든 오리의 행동을 알기 힘들다.

오리마다 행동을 각기 다르게 오버라이드 할 수 있어서, 한눈에 파악하기 쉽지않다.

 

4. 코드를 변경했을 때 다른 오리들에게 원치 않은 영향을 끼칠 수 있다.

Duck의 fly()메소드를 날수있는 코드로 설정해두고 수많은 오리들을 그에 맞춰서 바꾸었는데, Duck의 fly()를 날수없는 코드로 바꾸어버린다면 굉장히 많은 시간을 써서 수정해야 할 수 있다.

 

 

3rd 접근) Flyable이라는 인터페이스를 설계하여, 해당 인터페이스에 fly() 메소드를 정의

날 수 있는 오리에게만 인터페이스를 구현하여 사용하면되므로 효율적이라 생각할 수 있지만, 날아가는 동작을 바꿔야한다면 여전히 수많은 코드를 수정해야하므로 비효율적.

 

 

즉, 한가지 행동을 바꿀 때마다 그 행동이 정의된 서브클래스를 모두 찾아 고치는 것은 굉장히 비효율적이다.

Design Principle 1. 변경가능 부분과 변경불가 부분을 분리한다.

< 오리의 날아가는 행동들은 여러가지이고 변경가능한 부분이므로,  날아가는 행동들을 모은 클래스 집합을 별도로 생성 >

 


 

클래스 집합은 슈퍼클래스의 인스턴스에 행동을 할당할 수 있을텐데, 동적으로 행동을 바꾸려면?

Design Principle 2. 인터페이스에 맞춰 코드를 작성한다.

< 오리의 날아가는 행동들은 Duck클래스가 아닌 특정행동만을 목적으로 하는 클래스의 집합으로 생성하고 여기서 구현 >

// 인터페이스로 구현할때의 이점

/* 1. 기본형태
d를 Dog 형식으로 선언하면, 
Dog의 구체적 표현에 맞게 코딩해야함.*/
Dog d = new Dog();
d.bark();

/* 2. 다형성의 활용 
Animal이라는 인터페이스와 상위형식에 맞춰 코딩한다면, 
다형성을 활용할 수 있음.*/
Animal animal = new Dog();
animal.makeSound();

/* 3. 바람직한 방법
new Dog()처럼 상위형식의 인스턴스를 만드는 과정을 
직접 코드로 만들지 않고, 구체적으로 구현된 객체를 실행해 대입*/
a = getAnimal();
a.makeSound();

 

4th 접근) 디자인 원칙 1,2  적용

1. 다른 형식의 객체에서도 날아가거나 울음소리를 내는 행동들을 재사용할 수 있다.

☞ 이미 FlyBehavior나 QuackBehavior이라는 인터페이스의 각각 fly(), quack()메소드로 관련된 구체적 행동들이 구현되어있는 클래스들이 있기때문!

 

2. 슈퍼클래스인 Duck을 건드리지 않고도, 새로운 행동을 추가할 수 있다.

☞ FlyBehavior나 QuackBehavior이라는 인터페이스와 이를 상속받은 클래스들은 슈퍼클래스와 별개의 클래스이므로, 독립적이다!

 

 

 

즉, 결과는 아래와 같게된다.


 

※  오리의 큰 행동을 나타낸 인터페이스와 구체적인 행동을 담은 클래스

FlyBehavior [인터페이스]
fly()
QuackBehavior [인터페이스]
quack()
FlyWithWings [클래스]
fly() { // 날아가는 방법 }
FlyNoWay [클래스]
fly() { } // 날수없도록
Quack [클래스]
fly() { // 꽥꽥 소리 }
Squeak [클래스]
fly() { // 삑삑 소리 }
MuteQuack [클래스]
fly() { } // 울수 없도록

=> 위와 같이 날아가는 방법을 담은 FlyBehavior 인터페이스를 상속받은, FlyWithWings 클래스, FlyNoWay 클래스를 생성할 수 있다. 울음소리의 경우도 마찬가지.

 

 

※  행동을 담은 슈퍼클래스 Duck

Duck [슈퍼 클래스]

// 행동 인터페이스 형식으로 미리 선언
FlyBehavior flyBehavior
QuackBehavior quackBehavior

// 오리 메소드
performFly()
performQuack()
swim()
. . .
=> Duck 슈퍼클래스에 행동 인터페이스 형식의 인스턴스를 추가함으로써, 각 오리객체에서는 실행시 이 변수에 특정행동형식의 클래스를 다형적으로 설정이 가능함! 

실제 코드로 표현하면 아래와 같다. FlyBehavior에 관한 것만 적어뒀는데, QuackBehavior이나 다른 행동인터페이스가 와도 같은 방식으로 활용 가능하다.

  • Duck.java
    행동인터페이스 형식의 레퍼런스 변수로 선언하여 가져온 뒤, 날거나 우는 등의 행동은 행동클래스에게로 위임한다.
    실행중에 동적으로 행동을 지정하기 위해, 세터 메소드를 선언한다.
  • FlyBehavior.java , FlyWithWings.java , FlyNoWay.java
    행동인터페이스인 FlyBehavior는 fly()를 가지고, 이를 행동구현클래스에서 실제 정의를 내린다.
  • MallardDuck.java - 청둥오리
    구체적인 나는 동작을 이미 정의된 행동묶음(행동 인터페이스를 implements받은 클래스들)에서 가져온다.
  • DuckSimulator.java
    위에서 선언한 아이들을 실행하기 위한 예시코드
    RubberDuck은 따로 선언하지 않았지만, MallardDuck과 같은 형식의 클래스이고, FlyNoWay로 정의되어있다 가정함
// Duck.java - 오리 (슈퍼클래스)
public abstract class Duck{
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;
    
    // 기타 코드. . .
    
    // 날기- 행동클래스에 위임
    public void performFly(){
    	flyBehavior.fly();
    }
    
    // 날아가는 방식을 바꾸기 위한 세터메소드
    public void setFlyBehavior(FlyBehavior fb){
        flyBehavior = fb;
    }
}

//---------------------------------------------------------------

//FlyBehavior.java - 날아가는 행동 인터페이스
public interface FlyBehavior{
    public void fly();
}
// FlyWithWings.java - 날개로 날아가는 구체적 행동을 담은 클래스
public class FlyWithWings implements FlyBehavior{
   public void fly(){
       System.out.println("날개로 날아요!");
   }
}
// FlyNoWay.java - 날 수 없다는 행동을 담은 클래스
public class FlyNoWay implements FlyBehavior{
   public void fly(){
       System.out.println("못날아요..");
   }
}

//---------------------------------------------------------------

// MallardDuck.java - 청둥오리 (오리를 상속받음)
public class MallardDuck extends Duck{
    // 청둥오리를 생성할때 (해당 생성자로 들어올때)
    public MallardDuck() {
        // 날아가는 동작은 날개로 날아가록 하겠다는 의미
        flyBehavior = new FlyWithWings();
    }
    // 기타 코드. . .
}

//---------------------------------------------------------------

// DuckSimulator.java
public class DuckSimulator{
    public static void main(String[] args){
        Duck mallard = new MallardDuck();
        mallard.performFly();
        
        Duck rubber = new RubberDuck(); 
        rubber.performFly(); // 못날아요.. 를 출력
        rubber.setFlyBehavior(new FlyWithWings());
        rubber.performFly(); // 날개로 날아요! 를 출력
    }   
}

 


 

 캡슐화된 행동 살펴보기

클라이언트( 슈퍼클래스 Duck을 포함한 여러 오리들 )에서는 날아가는 행동이나 울음소리를 내는 행동들 등등을 캡슐화된 알고리즘으로 구현하게 된다. 

즉, 각 오리에는 FlyBehavior, QuackBehavior를 가지므로 오리에는 FlyBehavior, QuackBehavior가 있다 라고 표현가능함

이렇게 두 클래스를 합치는 것을 구성(composition)이라 한다.

 

"A는 B이다" 보다 "A에는 B가 있다"가 효율적일 수 있다.

Design Principle 3. 상속보다는 구성을 활용한다.

< 구성을 활용하면 유연성을 크게 향상시킬 수 있다! >

 

 

첫 번째 디자인 패턴 : 전략 패턴

위에서 언급한 총 3가지의 디자인 원칙은 전략패턴이다.

알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해준다.

전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경 할 수 있다,

 

 

Comments