볼트라고 하는 이름의 강아지와 키티라는 이름의 고양이를 키운다고 상상해보자. 볼트, 키티와 함께 재미있는 시간을 보내는 세계를 프로그램으로 표현한다면 다음과 같은 2개의 클래스가 필요할 것이다.
Dog.java
public Class Dog {
public void playWithOwner() {
System.out.println("귀염둥이 이리온...";
System.out.println("멍! 멍!");
System.out.println("꼬리 살랑 살랑~");
System.out.println("잘했어");
}
}
Cat.java
public Class Cat {
public void playWithOwner() {
System.out.println("귀염둥이 이리온...";
System.out.println("야옹~ 야옹~");
System.out.println("꼬리 살랑 살랑~");
System.out.println("잘했어");
}
}
위의 예제 코드의 playWithOwner() 메서드를 보면 4번째 줄만 빼고는 모두 동일한 것을 볼 수 있다. 따라서 위 코드를 보고 있으면 객체 지향의 4대 특성 가운데 상속을 통해 동일한 부분(중복)은 상위 클래스로, 달라지는 부분만 하위 클래스로 분할하고 싶은 객체 지향 설계에 대한 욕구가 자극될 것이다. 이에 따라 코드를 개선해보자.
상위 클래스를 포함하는 Animal.java
package templateMethodPattern;
public abstract class Animal {
// 템플릿 메서드
public void playWithOwner() {
System.out.println("귀염둥이 이리온...";
play();
runSomething();
System.out.println("잘했어");
}
// 추상 메서드
abstract void play();
// Hook(갈고리) 메서드
void runSomthing() {
System.out.println("꼬리 살랑 살랑~");
}
}
하위 클래스를 포함하는 Dog.java
package templateMethodPattern;
public Class Dog extends Animal {
@Override
// 추상 메서드 오버라이딩
void play() {
System.out.println("멍! 멍!");
}
@Override
// Hook(갈고리) 메서드 오버라이딩
void runSomthing() {
System.out.println("멍! 멍!~ 꼬리 살랑 살랑~");
}
}
하위 클래스를 포함하는 Cat.java
package templateMethodPattern;
public Class Cat extends Animal {
@Override
// 추상 메서드 오버라이딩
void play() {
System.out.println("야옹~ 야옹~");
}
@Override
// Hook(갈고리) 메서드 오버라이딩
void runSomthing() {
System.out.println("야옹~ 야옹~ 꼬리 살랑 살랑~");
}
}
Driver.java
package templateMethodPattern;
public Class Driver {
public static void main(String[] args) {
Animal bolt = new Dog();
Animal kitty = new Cat();
bolt.playWithOwner();
System.out.println();
System.out.println();
kitty.playWithOwner();
}
}
상위 클래스인 Animal에는 템플릿(견본)을 제공하는 playWithOwner() 메서드와 하위 클래스에게 구현을 강제하는 play() 추상 메서드, 하위 클래스가 선택적으로 오버라이딩할 수 있는 runSomething() 메서드가 있다. 하위 클래스인 Dog와 Cat은 상위 클래스인 Animal에서 구현을 강제하고 있는 play() 추상 메서드를 반드시 구현해야 한다. runSomething() 메서드는 선택적으로 오버라이딩할 수 있다. 이처럼 상위 클래스에 공통 로직을 수행하는 템플릿 메서드와 하위 클래스에 오버라이딩을 강제하는 추상 메서드 또는 선택적으로 오버라이딩할 수 있는 훅(Hook) 메서드를 두는 패턴을 템플릿 메서드 패턴이라고 한다.
템플릿 메서드 패턴을 한 문장으로 정리해보자.
"상위 클래스의 견본 메서드에서 하위 클래스가 오버라이딩한 메서드를 호출하는 패턴"
클래스 다이어그램을 보면 템플릿 메서드 패턴이 의존 역전 원칙(DIP)
을 활용하고 있음을 알 수 있다.
팩터리는 공장을 의미한다. 공장은 물건을 생산하는데 객체 지향에서 팩터리는 객체를 생성한다. 결국 팩터리 메서드는 객체를 생성 반환하는 메서드를 말한다. 여기에 패턴이 붙으면 하위 클래스에서 팩터리 메서드를 오버라이딩해서 객체를 반환하게 하는 것을 의미한다.
위에서 살펴본 템플릿 메서드 패턴에서 귀여운 애완 동물들과 노는 코드를 작성해봤는데 여기에 상상력을 더해 볼트와 키티가 각자 가지고 놀고 싶어하는 장난감을 가져오는 모습을 상상해보자. 볼트는 강아지 장난감을 물고 올 것이고 키티는 고양이 장난감을 물고 올 것이다. 이를 코드로 구현해 보자.
추상 클래스를 나타내는 Animal.java
package factoryMethodPattern;
public abstract class Animal {
// 추상 팩터리 메서드
abstract AnimalToy getToy();
}
추상 클래스를 나타내는 AnimalToy.java
package factoryMethodPattern;
// 팩터리 메서드가 생성할 객체의 상위 클래스
public abstract class AnimalToy {
abstract void identify();
}
Dog.java
package factoryMethodPattern;
public Class Dog extends Animal {
// 추상 팩터리 메서드 오버라이딩
@Override
AnimalToy getToy() {
return new DogToy();
}
}
DogToy.java
package factoryMethodPattern;
// 팩터리 메서드가 생성할 객체
public Class DogToy extends AnimalToy {
public void identify() {
System.out.println("나는 테니스공! 강아지의 친구!");
}
}
Cat.java
package factoryMethodPattern;
public Class Cat extends Animal {
// 추상 팩터리 메서드 오버라이딩
@Override
AnimalToy getToy() {
return new CatToy();
}
}
CatToy.java
package factoryMethodPattern;
// 팩터리 메서드가 생성할 객체
public Class CatToy extends AnimalToy {
public void identify() {
System.out.println("나는 캣타워! 고양이의 친구!");
}
}
Driver.java
package factoryMethodPattern;
public Class Driver {
public static void main(String[] args) {
// 팩터리 메서드를 보유한 객체들 생성
Animal bolt = new Dog();
Animal kitty = new Cat();
// 팩터리 메서드가 반환하는 객체들
AnimalToy boltBall = bolt.getToy();
AnimalToy kittyTower = kitty.getToy();
// 팩터리 메서드가 반환한 객체들을 사용
boltBall.identify();
kittyTower.identify();
}
}
팩터리 메서드 패턴을 한 문장으로 정의하면 다음과 같다.
"오버라이드된 메서드가 객체를 반환하는 패턴"
클래스 다이어그램을 살펴보면 팩터리 메서드 패턴이 "의존 역전 원칙(DIP)"
을 활용하고 있음을 알 수 있다.
참고