헤드퍼스트 디자인패턴
- 본 책을 읽고 책의 내용을 간략하게 정리한 글입니다.
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 클래스를 전혀 건드리지 않고도 새로운 행동을 추가할 수도 있다.
- 따라서 상속을 사용할 때 느낀 부담을 떨쳐 버리고 재사용의 장점을 그대로 누릴 수 있다.
오리 행동 통합하기
- Duck 클래스에
flyBehavior
와quackBehavior
라는 인터페이스 형식의 인스턴스 변수를 추가한다.- 각 오리 객체에서는 실행 시에 이 변수에 특정 행동 형식(
FlyWithWings
,Squeak
등)의 레퍼런스를 다형적으로 설정한다. - Duck 클래스와 모든 서브클래스에서
fly()
와quack()
메서드를 제거한다. - Duck 클래스에
fly()
와quack()
대신performFly()
와performQuack()
이라는 메서드를 넣는다.| Duck | | --------------------------- | | FlyBehavior flyBehavior | | QuackBehavior quackBehavior | | --------------------------- | | performQuack() | | swim() | | display() | | performFly() | | // 기타 오리 관련 메서드 |
- 각 오리 객체에서는 실행 시에 이 변수에 특정 행동 형식(
performQuack()
메서드를 구현하자.- 꽥꽥거리는 행동을 하고 싶을 땐
quackBehavior
에 의해 참조되는 객체에서 꽥꽥거리도록 하면 된다. - 객체의 종류에 신경 쓸 필요 없이
quack()
을 실행할 줄 알면 된다.
- 꽥꽥거리는 행동을 하고 싶을 땐
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
// 기타 코드
public void performQuack() {
quackBehavior.quack();
}
}
flyBehavior
와quackBehavior
인스턴스 변수 설정 방법을 생각해보자.
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가 있다” 관계
- 각 오리에는
FlyBehavior
와QuackBehavior
가 있으며 각각 나는 행동과 소리내는 행동을 위임받는다. - 이렇게 두 클래스를 합치는 것을 구성(composition)을 이용한다고 한다.
- 각 오리에는
디자인 원칙 3
- 상속보다는 구성을 활용한다.
- 위 프로젝트의 Duck 클래스에서는 행동을 상속받는 대신, 올바른 행동 객체로 구성되어 행동을 부여받는다.
- 구성을 활용하면 유연성을 크게 향상시킬 수 있다.
- 구성 요소로 사용하는 객체에서 올바른 행동 인터페이스를 구현하기만 하면 실행 시에 행동을 바꿀 수도 있다.
- 구성은 여러 디자인 패턴에서 쓰인다.
첫 번째 디자인 패턴: 전략 패턴
- 위의 패턴이 바로 전략 패턴이다.
- 전략 패턴
- 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해준다.
- 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리하여 독립적으로 변경할 수 있다.
핵심 정리
- 훌륭한 객체지향 디자인은 재사용성, 확장성, 관리의 용이성을 갖출 수 있어야 한다.
- 패턴은 훌륭한 객체지향 디자인 품질을 갖추고 있는 시스템을 만드는 방법을 제공한다.
- 대부분의 패턴은 시스템의 일부분을 나머지 부분과 무관하게 변경하는 방법을 제공한다.
- 많은 경우에 시스템에서 바뀌는 부분을 골래내어 캡슐화해야 한다.
헤드퍼스트 디자인패턴 개정판, 에릭 프리먼, 엘리자베스 롭슨, 케이시 시에라, 버트 베이츠 저, 한빛미디어 출판