CHAPTER 01. 디자인 패턴 소개와 전략 패턴

1. 오리 시뮬레이션 게임을 만든다면

  • 초기 기획단계에서는 모든 오리들은 꽥꽥 소리를 낼 수 있고 수영만 할 수 있고 겉모습만 달랐다. 그래서 객체지향 기법을 사용하여 모든 오리가 가지고 있어야 하는 공통된 기능을 정의한 Duck이라는 슈퍼클래스를 만든 다음 이를 상속받는 서브클래스를 만들어 오리의 겉모습을 각자 다르게 구현했다.

  • 그런데 갑자기 나는 기능도 추가되어야 한다면 어떻게 할까?
  • 슈퍼클래스인 Duckfly() 메서드를 추가로 구현하면 상속받고 있는 모든 서브클래스에 나는 기능을 추가할 수 있다.

  • 그런데 나중에 보니까 일부 서브클래스에서 문제가 발견되었다. 왜냐면 서브클래스에는 고무로 만든 장난감 오리도 있었는데 그 클래스에도 날아다니는 기능이 추가된 것이었다. 고무 오리가 날아다니는 기능은 의도하지 않은 기능이기 때문에 고쳐야 한다.
public class Duck {
    quack(); // 꽥꽥
    swim(); // 수영
    fly(); // 날기
}

// 살아 있는 오리
public class MallardDuck extends Duck {
    display();
}

// 고무 오리
public class RubberDuck extends Duck {
    display();
}
  • 그러면 고무 오리 클래스에서 fly() 메서드를 아무 것도 하지 않도록 오버라이드 해서 문제를 해결할 수 있을 것이다.
  • 하지만 만약 게임이 주기적으로 업데이트 된다면 그 때마다 fly() 메서드에 새로운 기능이 추가될 수 있고, 그러면 그 때마다 fly()를 아무 것도 하지 않게 오버라이드 한 서브클래스의 fly() 메서드도 수정해 줘야 할 것이다. 생각만 해도 너무 손이 많이 간다.
  • Flyable이라는 인터페이스를 만들어 서브클래스에서 구현하게 하면 고무 오리가 날아다니는 문제를 해결할 수 있겠지만 Flyable을 구현하는 클래스마다 각자 구현해야 하기 때문에 중복되는 코드가 많이 생기고 만약 날아가는 동작에 수정사항이 생긴다면 Flyable을 구현하는 모든 서브클래스의 코드를 수정해 줘야 한다. 이것 역시 비효율적이다.

2. 디자인 원칙 1) 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.

  • 이를 효율적으로 설계하기 위해서 첫 번째 디자인 원칙인 캡슐화를 적용한다. 캡슐화는 애플리케이션에서 달라지는 부분을 찾아내고 달라지지 않는 부분과 분리하는 것이다. 그러면 코드를 변경할 일이 생기면 캡슐화된 부분만 변경하면 돼기 때문에 다른 부분에 미치는 영향을 최소화 시킬 수 있다.

  • 현재 오리의 기능 중에서 꽥꽥 소리를 내는 quack()과 나는 기능인 fly()는 서브클래스의 종류에 따라 다르게 구현되어야 하기 때문에 Duck 슈퍼클래스에서 quack()fly()를 꺼내서 각각 행동을 나타낼 클래스 집합(set)을 새로 만들어야 한다.

3. 디자인 원칙 2) 구현보다는 인터페이스에 맞춰서 프로그래밍한다.

  • 나는 행동과 꽥꽥거리는 행동을 구현하는 클래스 집합은 최대한 유연하게 만들어야 한다. 그리고 Duck의 인스턴스에 행동을 할당할 수 있어야 한다. 인스턴스가 생성된 후에 행동을 동적으로 바꿀 수 있으면 더 좋을 것이다.
  • 이를 효율적으로 구현하기 위해 슈퍼클래스에 모든 행동을 구현하기 보다는 특정 행동만을 목적으로 하는 클래스의 집합을 만들어 행동 인터페이스를 구현한다.
public interface FlyBehaior {
    fly();
}

public class FlyWithWings implements FlyBehavior {
    fly();
    // 나는 방법을 구현
}

public class FlyNoWay implements FlyBehavior {
    fly();
    // 아무것도 하지 않음
    // 날 수 없다.
}
  • 이런 식으로 디자인하면 특정 행동들이 Duck 클래스에 국한되지 않고 해당 기능들이 필요한 서브클래스에 추가해서 사용할 수 있다. 상속을 쓸 때 떠안게 되는 부담을 전부 떨쳐 버리고도 재사용의 장점을 그대로 누릴 수 있다.

4. 오리 행동 통합하기

  • Duck 클래스에서 flyBehaviorquackBehavior라는 인터페이스 형식의 인스턴스 변수를 추가하고 각 서브클래스에서도 fly()quack()을 제거한다. Duck 클래스에서 fly()quack() 메서드를 제거한 뒤 대신 performFly()performQuack()이라는 메서드를 넣는다.
public abstract class Duck {
    
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;
    
    public void performQuack() {
        // 꽥꽥거리는 행동을 직접 처리하는 대신 quackBehavior로 참조되는 객체에 그 행동을 위임한다.
        quackBehavior.quack(); 
    }
    
    public void performFly() {
        flyBehavior.fly();
    }
}
  • 서브클래스
public class MallardDuck extends Duck {
    
    public MallarDuck() {
        // 슈퍼클래스에서 정의된 인스턴스 변수에 실제 생성된 객체를 할당 
        quackBehavior = new Quack();
        flyBehavior = new FlyWithWings();
    }
    
    public void display() {
        System.out.println("저는 물오리입니다.");
    }
}
  • 슈퍼클래스를 상속받은 서브클래스에서 각각 필요한 인스턴스를 할당해 사용할 수 있다. 재사용성이 증대된다.

5. 디자인 원칙 3) 상속보다는 구성을 활용한다.

  • 각 오리에는 FlyBehaviorQuackBehavior가 있으며 각각 나는 행동과 꽥꽥거리는 행동을 위임받는다. 이런 식으로 두 클래스를 합치는 것을 ‘구성(composition)을 이용한다’라고 부른다.
  • 구성을 활용해서 시스템을 만들면 유연성을 크게 향상시킬 수 있다. 단순히 알고리즘군을 별도의 클래스 집합으로 캡슐화할 수 있으며 구성 요소로 사용하는 객체에서 올바른 행동 인터페이스를 구현하기만 하면 실행 시에 행동을 바꿀 수도 있다.

전략 패턴

  • 지금까지 학습한 디자인 패턴을 전략 패턴(Strategy Pattern)이라고 한다. 전략 패턴은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해 준다. 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경할 수 있다.

디자인 패턴을 알아야 하는 이유

  • 개발자끼리 공통으로 아는 전문 용어를 사용하면 다른 개발자와 더 쉽게 대화할 수 있고, 패턴을 아직 모르는 사람들에게는 패턴을 배우고 싶은 생각이 들도록 자극을 줄 수 있다. 또한 자질구레한 객체 수준에서의 생각이 아닌, 패턴 수준에서 생각할 수 있기에 아키텍처를 생각하는 수준도 끌어 올려 준다.

마지막 정리

객체지향 기초

  • 추상화
  • 캡슐화
  • 다형성
  • 상속

객체지향 원칙

  • 바뀌는 부분은 캡슐화한다.
  • 상속보다는 구성을 활용한다.
  • 구현보다는 인터페이스에 맞춰서 프로그래밍한다.

객체지향 패턴

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


참고