헤드퍼스트 디자인패턴
- 본 책을 읽고 책의 내용을 간략하게 정리한 글입니다.
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 클래스에서는 행동을 상속받는 대신, 올바른 행동 객체로 구성되어 행동을 부여받는다.
- 구성을 활용하면 유연성을 크게 향상시킬 수 있다.
- 구성 요소로 사용하는 객체에서 올바른 행동 인터페이스를 구현하기만 하면 실행 시에 행동을 바꿀 수도 있다.
- 구성은 여러 디자인 패턴에서 쓰인다.
첫 번째 디자인 패턴: 전략 패턴
- 위의 패턴이 바로 전략 패턴이다.
- 전략 패턴
- 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해준다.
- 전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리하여 독립적으로 변경할 수 있다.
핵심 정리
- 훌륭한 객체지향 디자인은 재사용성, 확장성, 관리의 용이성을 갖출 수 있어야 한다.
- 패턴은 훌륭한 객체지향 디자인 품질을 갖추고 있는 시스템을 만드는 방법을 제공한다.
- 대부분의 패턴은 시스템의 일부분을 나머지 부분과 무관하게 변경하는 방법을 제공한다.
- 많은 경우에 시스템에서 바뀌는 부분을 골래내어 캡슐화해야 한다.
헤드퍼스트 디자인패턴 개정판, 에릭 프리먼, 엘리자베스 롭슨, 케이시 시에라, 버트 베이츠 저, 한빛미디어 출판