팩토리 패턴 (Factory Pattern)
팩토리 패턴은 추상 팩토리 패턴과 팩토리 메소드 패턴이 존재한다. 이 두가지 패턴은 팩토리 패턴으로 묶이지만 서로 다른 디자인 패턴이다. 팩토리 메소드 패턴은 객체를 생성할 때 필요한 인터페이스를 만들고, 어떤 클래스의 인스턴스를 만들지는 서브 클래스에서 결정한다. 즉, 인스턴스를 생성하는 과정을 서브 클래스에 위임하는 것이다.
코드에 적용해보기
Sandwich sandwich = new HamSandwich();
앞선 디자인 패턴 글에서 위와 같은 new 연산자를 통한 인스턴스 생성(구상 클래스의 인스턴스) 과정을 많이 보았다. 인터페이스를 이용하여 코드를 유연하게 만들었지만 구상 클래스의 인스턴스를 만들어야 했다는 것은 여전하다. 햄 샌드위치, 베이컨 샌드위치 등 각각에 해당하는 인스턴스를 조건에 맞춰 생성해야 한다.
Sandwich sandwich
if(Ham) {
sandwich = new HamSandwich();
} else if (Bacon) {
sandwich = new BaconSandwich();
}
...
이 상황에서 특정 샌드위치를 추가하거나 삭제해야 한다면 어떨까. 확장에는 닫혀있고 변경에는 열려있는 코드가 된다. 하지만 인스턴스 생성을 안 할 방법은 없다.
public Sandwich orderSandwich(String type) {
Sandwich sandwich;
if(type.equals("Ham")) {
sandwich = new HamSandwich();
} else if (type.equals("Bacon")) {
sandwich = new BaconSandwich();
}
sandwich.prepare();
sandwich.make();
sandwich.cut();
sandwich.wrap();
return sandwich;
}
전략패턴을 소개하면서 "애플리케이션에서 달라지는 부분과 달라지지 않는 부분을 분리한다" 라는 말을 했었다. 여기서도 찾아보자. 샌드위치 종류에 맞춰 인스턴스를 생성하는 구간이 달라지는 부분이다. 이제 전략패턴에서도 그랬듯이 캡슐화를 하면 된다. 객체를 생성하는 부분만을 orderSandwich() 메소드에서 따로 뽑아 객체만을 만드는 일을 전담시키려고 한다. 이를 팩토리라고 부른다. SimpleSandwichFactory를 만들고 이제 orderSandwich() 메소드는 새로 만든 객체의 클라이언트가 된다. 한마디로 팩토리한테 샌드위치 하나 만들어 달라고 부탁한다.
public class SimpleSandwichFactory {
public Sandwich createSandwich(String type) {
Sandwich sandwich = null;
if (type.equals("Ham")) {
sandwich = new HamSandwich();
} else if (type.equals("Bacon")) {
sandwich = new BaconSandwich();
}
return sandwich;
}
}
이제 클라이언트가 샌드위치 객체 인스턴스를 만들 때 createSandwich() 메소드를 호출하면 된다. 사실 샌드위치를 만드는 과정을 팩토리로 옮겼을 뿐 이전 코드와 별반 다를 게 없어보일 수 있다. 어쨌든 샌드위치가 추가되면 또 다시 기존 코드를 수정해야 하니까. 그러나 샌드위치 인스턴스를 이곳 저곳에서 쓴다면 얘기가 달라진다. orderSandwich() 말고도 샌드위치 인스턴스를 받아 가격을 계산하거나, 레시피를 설명하는 등 클래스가 있다면 이 팩토리를 유용하게 사용할 수 있다. 수정이 생겨도 이 팩토리만 수정하게 된다.
public class SandwichShop {
SimpleSandwichFactory factory;
public SandwichShop(SimpleSandwichFactory factory) {
this.factory = factory;
}
public Sandwich orderSandwich(String type) {
Sandwich sandwich;
sandwich = factory.createSandwich(type);
sandwich.prepare();
sandwich.make();
sandwich.cut();
sandwich.wrap();
return sandwich;
}
}
샌드위치숍 클래스에서는 SimpleSandwichFactory를 인스턴스 변수로 선언하고 생성자에 팩토리 객체가 전달된다. orderSandwich() 메소드에서는 팩토리를 이용하여 샌드위치 객체를 만든다. 이제는 new 연산자를 이용한 객체 생성이 아닌 팩토리 객체에 있는 createSandwich() 메소드를 사용하면 된다. 구상 클래스의 인스턴스를 더 이상 샌드위치숍에서 담당할 필요가 없어졌다.
굳이 Simple이라는 단어를 붙인 이유는 간단한 팩토리라는 용어가 있기 때문이다. 디자인 패턴이라기 보다는 프로그래밍에서 자주 쓰이는 단어같은 개념이다. 하지만 간단한 팩토리 패턴은 정확히는 패턴이 아니다.
팩토리를 사용하는 클라이언트 SandwichShop 클래스는 SimpleSandwichFactory로부터 Sandwich 인스턴스를 받게 된다. 여기서 팩토리가 유일하게 구상 Sandwich 클래스를 직접 참조하는 부분이다. Sandwich 클래스는 햄 샌드위치, 베이컨 샌드위치 등 구상 클래스에서 상속 받아 쓰도록 추상 클래스로 만든다. 이제 구상 클래스에서 각 샌드위치들은 Sandwich 추상 클래스를 확장한다.
이제 간단한 팩토리가 아닌 진짜 팩토리 패턴을 살펴보자.
샌드위치숍을 각지에 지점을 내려고 한다. 지역마다 선호하는 스타일이 있기에 서울 샌드위치, 부산 샌드위치, 제주 샌드위치 등 특색에 맞춰 만들어야 한다. SimpleSandwichFactory를 삭제하고 이제 SeoulSandwichFactory, BusanSandwichFactory, JejuSandwichFactory를 만들고 SandwichShop에서 이용하게 할 것이다.
SeoulSandwichFactory seoulSandwichFactory = new SeoulSandwichFactory();
SandwichShop sandwichShop = new SandwichShop(seoulSandwichFactory);
sandwichShop.orderSandwich("Ham");
서울 스타일의 샌드위치를 만드는 팩토리를 생성하고, 샌드위치숍을 생성하면서 seoulSandwichFactory를 인자로 넣는다. 이제 서울 스타일의 햄 샌드위치를 만들 수 있다. 하지만 지역의 특색을 살린다 한들 체인점이기 때문에 만드는 방식, 자르는 방식 등이 달라서는 안된다. 이러한 문제를 해결하려면 SandwichShop과 샌드위치 생성 코드를 하나로 묶어주는 프레임워크를 만들어야 한다.
public abstract class SandwichShop {
public Sandwich orderSandwich(String type) {
Sandwich sandwich;
sandwich = createSandwich(type);
sandwich.prepare();
sandwich.make();
sandwich.cut();
sandwich.wrap();
return sandwich;
}
abstract Sandwich createSandwich(String type);
}
createSandwich() 메소드를 다시 넣었다. 대신 추상 메소드로 선언하고 지역별 스타일에 맞게 서브 클래스를 만들면 된다. 또 SandwichShop 클래스도 추상 클래스가 되었다. 이젠 팩토리 객체 대신 createSandwich() 메소드를 사용한다.
서브 클래스들은 createSandwich() 메소드를 상속 받지만 order의 경우 SandwichShop에서 정의한 대로 사용한다. 샌드위치를 준비하고 자르고 포장하는 것은 동일해야 하기 때문이다. 어떤 스타일의 샌드위치를 만드느냐만 다르기 때문에 이에 해당하는 메소드만 상속 받아 확장한다.
public Sandwich createSandwich(String type) {
Sandwich sandwich = null;
if (type.equals("Ham")) {
sandwich = new SeoulHamSandwich();
} else if (type.equals("Bacon")) {
sandwich = new SeoulBaconSandwich();
}
return sandwich;
}
각 구상 클래스는 위와 같은 모습일 것이다. SandwichShop에서는 어떤 샌드위치를 만드는지에는 관심이 없어졌다. 그냥 어떤 스타일의 어떤 종류의 샌드위치를 만들던 준비하고 만들고 자르고 포장하는 일에만 관심이 있다. 즉, 서브 클래스가 어떤 샌드위치를 만들지 결정하게 위임한 것이다. SandwichShop과 Sandwich는 완전히 분리되었다.
public class SeoulSandwichShop extends SandwichShop {
Sandwich createSandwich(String item) {
if (item.equals("Ham")) {
return new SeoulHamSandwich();
} else if (item.equals("Bacon")) {
return new SeoulBaconSandwich();
} else return null;
}
// order 메소드는 자동으로 상속받음
}
Sandwich의 구상 클래스 가운데 어떤 인스턴스를 만들어 반환할지는 SandwichShop의 서브 클래스(SeoulSandwichShop)에 의해 결정된다. 구상 클래스의 인스턴스를 만드는 일을 하나의 객체가 하다가 이제 여러 서브 클래스가 처리하는 방식으로 바뀌었다.
public abstract class SandwichShop {
public Sandwich orderSandwich(String type) {
Sandwich sandwich;
sandwich = createSandwich(type);
sandwich.prepare();
sandwich.make();
sandwich.cut();
sandwich.wrap();
return sandwich;
}
protected abstract Sandwich createSandwich(String type);
}
위 코드에서 createSandwich() 메소드를 팩토리 메소드라고도 하는데 이는 클라이언트(슈퍼 클래스의 orderSandwich() 메소드)가 실제로 생성되는 인스턴스가 무엇인지 알 수 없게 만드는 역할을 하기도 한다. 한마디로 어떤 샌드위치의 인스턴스가 만들어지는지 관심을 두지 않게 해준다.
abstract public class Sandwich {
String name;
String bread;
String sauce;
List<String> toppings = new ArrayList<String>();
public String getName() {
return name;
}
public void prepare() {
System.out.println("준비하는중 " + name);
}
public void make() {
System.out.println("만드는중 " + name);
}
public void cut() {
System.out.println("자르는중 " + name);
}
public void wrap() {
System.out.println("포장하는중 " + name);
}
public String toString() {
StringBuffer display = new StringBuffer();
display.append("---- " + name + " 레시피 ----\n");
display.append(bread + "\n");
display.append(sauce + "\n");
for (String topping : toppings) {
display.append(topping + "\n");
}
return display.toString();
}
}
public class SeoulHamSandwich extends Sandwich {
public SeoulHamSandwich() {
name = "서울 스타일의 햄 샌드위치";
bread = "서울 스타일의 빵";
sauce = "서울 스타일의 소스";
toppings.add("서울 스타일의 토핑!");
}
}
public class SandwichTest {
public static void main(String[] args) {
SandwichShop sandwichShop = new SeoulSandwichShop();
Sandwich sandwich = sandwichShop.orderSandwich("Ham");
System.out.println(sandwich);
// 아래와 같은 결과 출력
// 준비하는중 서울 스타일의 햄 샌드위치
// 만드는중 서울 스타일의 햄 샌드위치
// 자르는중 서울 스타일의 햄 샌드위치
// 포장하는중 서울 스타일의 햄 샌드위치
// ---- 서울 스타일의 햄 샌드위치 레시피 ----
// 서울 스타일의 빵
// 서울 스타일의 소스
// 서울 스타일의 토핑!
}
}
모든 팩토리 패턴은 객체 생성을 캡슐화한다. 팩토리 메소드 패턴은 서브 클래스에서 어떤 인스턴스를 만들지 결정함으로써 객체 생성을 캡슐화한다. 번역을 생산자라고 했지만 Creator라고 보는 게 더 이해가 된다. 즉, 추상 크리에이터 클래스는 서브 클래스에서 객체를 생산하려고 구현하는 팩토리 메소드(createSandwich() 메소드)를 정의한다. 생산자 클래스를 상속 받은 클래스들을 Concrete creator라고도 부른다.
제품 클래스에서는 Sandwich 클래스 즉, 팩토리에서는 제품을 Sandwich를 만든다. 이렇게 생산자 클래스와 제품 클래스는 병렬 구조를 이룬다. 두 개의 큰 클래스 모두 추상 클래스이고 그 클래스를 확장하는 구상 클래스가 있다. 구체적인 구현은 구상 클래스에서 한다는 공통점이 있다. SandwichShop 클래스의 각 구상 클래스에 있는 createSandwich() 메소드(팩토리 메소드)에는 각 지역의 샌드위치 만드는 법이 캡슐화되어 있다.
Creator에는 Product를 이용하여 일을 하는 메소드들이 모두 구현되어 있지만, Product를 만들어 주는 팩토리 메소드는 추상 메소드로 정의되어 있다. 이를 ConcreteCreator들이 상속 받아서 실제 Product를 생상하는 팩토리 메소드를 구현한다. 즉, 구상 클래스의 인스턴스를 만드는 일을 ConcreteCreator들이 책임진다.
어찌보면 간단한 팩토리와 팩토리 메소드 패턴이 비슷해보이지만 간단한 팩토리는 임시 방편에 불과하고 팩토리 메소드 패턴은 여러번 재사용 가능한 프레임워크를 만들 수 있다. Product를 그때 그때 유연하게 바꿀 수 있다는 점에서 팩토리 메소드 패턴이 더 확장에는 열린 방법이라고 할 수 있다.
* 아래의 자료들을 참고하였습니다.
에릭 프리먼 · 엘리자베스 롭슨 · 케이시 시에라 · 버트 베이츠, 『헤드퍼스트 디자인패턴 개정판』, 서환수 옮김, 한빛미디어(2022), p144-169.
'Programming' 카테고리의 다른 글
디자인패턴 시리즈 6. 템플릿 메소드 패턴 (Template Method Pattern) (2) | 2023.01.17 |
---|---|
디자인패턴 시리즈 5. 커맨드 패턴 (Command Pattern) (0) | 2023.01.17 |
디자인패턴 시리즈 3. 데코레이터 패턴 (Decorator Pattern) (0) | 2023.01.13 |
디자인패턴 시리즈 2. 싱글톤 패턴 (Singleton Pattern) (0) | 2023.01.12 |
디자인패턴 시리즈 1. 전략 패턴 (Strategy Pattern) (2) | 2023.01.11 |
댓글