헤드퍼스트 디자인패턴 Chapter1

헤드퍼스트 디자인패턴

  • 본 책을 읽고 책의 내용을 간략하게 정리한 글입니다.

Chapter 1. 디자인 패턴 소개와 전략 패턴

오리 시뮬레이션 게임, SimUduck

  • 오리 시뮬레이션 게임에서는 헤엄도 치고 꽥꽥 소리도 내는 다양한 오리가 등장한다.
  • 이 시스템을 처음 디자인한 사람은 표준 객체지향 기법을 사용하여 Duck 이라는 슈퍼클래스를 만든 다음, 그 클래스를 확장해서 서로 다른 종류의 오리를 만들었다.
    |          Duck          |
    | ---------------------- |
    | quack()                |
    | swim()                 |
    | display()              |
    | // 기타 오리 관련 메서드 |
    
    |     MallardDuck     |
    | ------------------- |
    | display() {         |
    | // 적당한 모양 표시 } |
    
    |     RedheadDuck     |
    | ------------------- |
    | display() {         |
    | // 적당한 모양 표시 } |
    

오리 시뮬레이션 게임 차별화하기

  • 만약 오리가 날 수 있도록 기능을 추가해달라는 요청이 들어온다면?
    • 단, 몇몇 서브 클래스 오리들만 날 수 있도록 해야 한다.
    • 상속을 생각하여 오버라이드하는 게 방법일까?
      |               RubberDuck                 |
      | ---------------------------------------- |
      | quack() { // 삑삑 }                       |
      | display() { // 고무 오리 }                 |
      | fly() { // 아무 동작하지 않도록 오버라이드 } |
      
      |                 DecoyDuck                   |
      | ------------------------------------------- |
      | quack() { // 아무 동작하지 않도록 오버라이드 } |
      | display() { // 나무 오리 }                   |
      | fly() { // 아무 동작하지 않도록 오버라이드 }   |
      

인터페이스 설계하기

  • 앞으로 주기적으로 제품을 업데이트한다고 결정된다면 상속은 옳은 방법이 아니게 된다.
    • 상속을 계속 활용한다면 규격이 바뀔 때마다 Duck의 서브클래스의 메서드를 일일이 살펴보고 상황에 따라 오버라이드해야 하기 때문이다.
  • 특정 형식의 오리만 날거나 소리낼 수 있도록 하는 깔끔한 방법이 필요하다.

소프트웨어 개발 불변의 진리

  • 소프트웨어 개발에서 절대로 바뀌지 않는 진리는 변화이다.
  • 아무리 디자인을 잘한 애플리케이션이라고 하더라도 시간이 지남에 따라 변화하고 성장해야 한다.

문제를 명확하게 파악하기

디자인 원칙 1

  • 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
  • 코드에 새로운 요구 사항이 있을 때마다 바뀌는 부분이 있다면 분리해야 한다.
  • “바뀌는 부분은 따로 뽑아서 캡슐화한다. 그러면 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 고치거나 확장할 수 있다.”
    • 이 개념은 매우 간단하지만 다른 모든 디자인 패턴의 기반을 이루는 원칙이다.
    • 모든 패턴은 ‘시스템의 일부분을 다른 부분과 독립적으로 변화시킬 수 있는’ 방법을 제공한다.

바뀌는 부분과 그렇지 않은 부분 분리하기

  • 우선 fly()quack() 문제를 제외하면 Duck 클래스는 잘 작동하며, 나머지 부분은 자주 바뀌거나 달라지지 않으므로 Duck 클래스는 그대로 둔다.
  • 변화하는 부분에 해당하는 행동을 구현한 것을 담을 클래스 집합을 생성한다.
    • 각 클래스 집합에는 각각의 행동을 구현한 것을 모두 담는다.
    • 예를 들어 quack() 행동에 대하여 꽥꽥거리는 행동, 삑삑거리는 행동, 아무것도 하지 않는 행동을 구현하는 클래스를 만드는 식이 되겠다.

오리의 행동을 디자인하는 방법

  • 행동을 구현하는 클래스 집합은 최대한 유연하게, 그리고 Duck의 인스턴스에 행동을 할당할 수 있어야 한다.
  • 행동을 동적으로 바꿀 수 있다면 더 좋을 것이다.

디자인 원칙 2

  • 구현보다는 인터페이스에 맞춰서 프로그래밍한다.
    • 각 행동은 인터페이스(예: FlyBehavior, QuackBehavior)로 표현하고 이런 인터페이스를 사용해서 행동을 구현하도록 한다.
인터페이스에 맞춰서 프로그래밍한다 라는 말은 상위 형식에 맞춰서 프로그래밍한다 라는 말입니다
  • 인터페이스에 맞춰서 프로그래밍하라는 말이 반드시 자바의 인터페이스를 사용하라는 뜻은 아니다.
  • 핵심은 실제 실행 시에 쓰이는 객체가 코드에 고정되지 않도록 상위 형식(supertype)에 맞춰 프로그래밍해서 다형성을 활용해야 한다는 점이다.
  • 상위 형식에 맞춰서 프로그래밍하라는 원칙은 변수를 선언할 때 보통 추상 클래스나 인터페이스같은 상위 형식으로 선언해야 한다.
    • 객체를 변수에 대입할 때 상위 형식을 구체적으로 구현한 형식이라면 어떤 객체든 넣을 수 있기 때문이다.
    • 변수를 선언하는 클래스에서 실제 객체의 형식을 몰라도 된다는 뜻으로 생각하면 된다.
  • 구현에 맞춰서 프로그래밍한다면 다음과 같이 할 수 있다.
    // 변수 d를 Dog 형식(Animal를 확장한 구상 클래스)으로 선언하면 구체적인 구현에 맞춰 코딩해야 한다.
    Dog d = new Dog();
    d.bark();
    
  • 하지만 인터페이스와 상위 형식에 맞춰서 프로그래밍한다면 다음과 같이 할 수 있다.
    // Dog라는 것을 알고는 있지만 다형성을 활용해서 Animal의 레퍼런스를 써도 된다.
    Animal animal = new Dog();
    animal.makeSound();
    
  • 더 바람직한 방법은 상위 형식의 인스턴스를 만드는 과정을 직접 코드로 만드는 대신 구체적으로 구현된 객체를 실행 시 대입하는 것이다.
    // Animal의 하위 형식 가운데 어떤 형식인지는 모른다. 단지 makeSound()에 올바른 반응만 할 수 있으면 된다.
    a = getAnimal();
    a.makeSound();
    

오리의 행동을 구현하는 방법

  • 날 수 있는 클래스는 무조건 FlyBehavior 인터페이스를 구현해야 한다.
    | <<Interface>> |
    |  FlyBehavior  |
    | ------------- |
    | fly()         |
    
    |   FlyWithWings   |
    | ---------------- |
    | // 나는 방법 구현 |
    
    |      FlyNoWay       |
    | ------------------- |
    | // 아무것도 하지 않음 |
    | // 날 수 없음!       |
    
  • 꽥꽥거리는 것과 관련된 행동도 마찬가지이다.
    | <<Interface>>  |
    |  QuackBehavior |
    | -------------- |
    | quack()        |
    
    |      Quack       |
    | ---------------- |
    | // 꽥꽥 소리를 냄 |
    
    |      Squeak      |
    | ---------------- |
    | // 삑삑 소리를 냄 |
    
    |      MuteQuack      |
    | ------------------- |
    | // 아무것도 하지 않음 |
    
  • 이런 식으로 디자인하면 다른 형식의 객체에서도 나는 행동과 소리내는 행동을 재사용할 수 있다.
  • 기존의 행동 클래스를 수정하거나 행동을 하는 Duck 클래스를 전혀 건드리지 않고도 새로운 행동을 추가할 수도 있다.
    • 따라서 상속을 사용할 때 느낀 부담을 떨쳐 버리고 재사용의 장점을 그대로 누릴 수 있다.

오리 행동 통합하기

  1. Duck 클래스에 flyBehaviorquackBehavior 라는 인터페이스 형식의 인스턴스 변수를 추가한다.
    • 각 오리 객체에서는 실행 시에 이 변수에 특정 행동 형식(FlyWithWings, Squeak 등)의 레퍼런스를 다형적으로 설정한다.
    • Duck 클래스와 모든 서브클래스에서 fly()quack() 메서드를 제거한다.
    • Duck 클래스에 fly()quack() 대신 performFly()performQuack()이라는 메서드를 넣는다.
      |            Duck             |
      | --------------------------- |
      | FlyBehavior flyBehavior     |
      | QuackBehavior quackBehavior |
      | --------------------------- |
      | performQuack()              |
      | swim()                      |
      | display()                   |
      | performFly()                |
      | // 기타 오리 관련 메서드      |
      
  2. performQuack() 메서드를 구현하자.
    • 꽥꽥거리는 행동을 하고 싶을 땐 quackBehavior에 의해 참조되는 객체에서 꽥꽥거리도록 하면 된다.
    • 객체의 종류에 신경 쓸 필요 없이 quack()을 실행할 줄 알면 된다.
public abstract class Duck {
	FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;
    // 기타 코드
  
  public void performQuack() {
      quackBehavior.quack();
  }
}
  1. flyBehaviorquackBehavior 인스턴스 변수 설정 방법을 생각해보자.
public class MallardDuck extends Duck {
    
    // MallardDuck에서는 꽥꽥 소리를 내고 하늘을 날 수 있는 오리를 구현할 수 있다.
    public MallardDuck() {
        quackBehavior = new Quack();
        flyBehavior = new FlyWithWings();
    }
    
    @Override
    public void display() {
      System.out.println("물오리입니다.");	
    }
}

특정 구현에 맞춰서 프로그래밍하면 안된다고 했는데?

  • MallardDuck 예제에서 생성자 파트를 보면 Quack 이라는 구현되어 있는 구상 클래스의 인스턴스를 만들었다.
    • Quack이나 FlyWithWings 같은 행동 클래스의 인스턴스를 만들어서 행동 레퍼런스 변수에 대입함으로써 행동을 구상 클래스로 설정하고 있긴 하지만, 실행 시에 쉽게 변경 가능하다.
    • 인스턴스 변수를 유연하게 초기화하는 방법을 쓰고 있으므로 이 코드는 상당히 유연하다고 할 수 있다.
    • quackBehavior 인스턴스 변수는 인터페이스 형식에, 실행 시에 동적으로 QuackBehavior를 구현한 다른 클래스를 할당할 수 있다.

동적으로 행동 지정하기

  • 오리의 행동을 생성자에서 인스턴스를 만드는 방식이 아닌 Duck의 서브클래스에서 Setter 메서드로 설정할 수 있도록 수정해보자.
// 아래 두 메서드를 호출하면 언제든지 행동을 즉석에서 바꿀 수 있다.
public void setFlyBehavior(FlyBehavior fb) {
    flyBehavior = fb;
}

public void setQuackBehavior(QuackBehavior qb) {
    quackBehavior = qb;
}

두 클래스를 합치는 방법

  • “A에는 B가 있다” 관계
    • 각 오리에는 FlyBehaviorQuackBehavior가 있으며 각각 나는 행동과 소리내는 행동을 위임받는다.
    • 이렇게 두 클래스를 합치는 것을 구성(composition)을 이용한다고 한다.

디자인 원칙 3

  • 상속보다는 구성을 활용한다.
  • 위 프로젝트의 Duck 클래스에서는 행동을 상속받는 대신, 올바른 행동 객체로 구성되어 행동을 부여받는다.
  • 구성을 활용하면 유연성을 크게 향상시킬 수 있다.
  • 구성 요소로 사용하는 객체에서 올바른 행동 인터페이스를 구현하기만 하면 실행 시에 행동을 바꿀 수도 있다.
  • 구성은 여러 디자인 패턴에서 쓰인다.

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

  • 위의 패턴이 바로 전략 패턴이다.
  • 전략 패턴
    • 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해준다.
    • 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리하여 독립적으로 변경할 수 있다.

핵심 정리

  • 훌륭한 객체지향 디자인은 재사용성, 확장성, 관리의 용이성을 갖출 수 있어야 한다.
  • 패턴은 훌륭한 객체지향 디자인 품질을 갖추고 있는 시스템을 만드는 방법을 제공한다.
  • 대부분의 패턴은 시스템의 일부분을 나머지 부분과 무관하게 변경하는 방법을 제공한다.
  • 많은 경우에 시스템에서 바뀌는 부분을 골래내어 캡슐화해야 한다.

헤드퍼스트 디자인패턴 개정판, 에릭 프리먼, 엘리자베스 롭슨, 케이시 시에라, 버트 베이츠 저, 한빛미디어 출판