이번 글에서는 팩토리 패턴에 대해 알아보고자 합니다. 이 글은 Head First Design Pattern 책의 147p ~ 206p를 읽고 이를 정리한 글입니다.
팩토리 패턴은 가장 흔히 사용하는 패턴입니다. 그렇기 때문에 어떤 것이 팩토리 패턴이고 어떻게 사용하는지 알아보겠습니다.
Factory Pattern을 알아보기 전, 팩토리에 대한 개념부터 먼저 알아보겠습니다. 아 그리고 오늘부터는 존댓말로 작성하겠습니다. ^^
개발을 하다보면 상황에 따라 다른 서브클래스를 생성해야만 하는 상황이 발생합니다. 예를 들면 손님이 어떤 메뉴를 선택 하거나 게임 유저가 하고싶은 챔피언을 선택 할 경우, 우리는 선택에 따라 그에 알맞는 인스턴스를 생성해줘야만 합니다.
제가 게임을 할때 특정 챔피언을 선택한다고 가정해보고 게임상의 코드를 작성해 보겠습니다. 전 뽀삐를 픽했습니다.
Champion champion = new Poppy();
뽀삐를 선택할 경우, champion이라는 변수에는 Poppy
라는 Champion의 서브클래스가 인스턴스로 생겼음을 알 수 있습니다. 하지만 리그오브레전드는 150종 이상의 챔피언이 있는데 이럴 경우에는 어떻게 사용자의 선택에 따라 챔피언을 동적으로 생성해줄 수 있을까요?
다음과 같은 코드를 구현하여 150종 이상의 챔피언을 선택할 수 있도록 할 수 있습니다.
Champion champion;
if (poppy) {
champion = new Poppy();
} else if(sylas) {
champion = new Sylas();
}
.... 이하 생략
하지만 이것이 최선일까요? 새로운 챔피언을 개발할 경우에는 어떻게 할 것인가요? 다음과 같은 코드를 새로운 챔피언을 선택하는 상황에서 계속해 추가해줄 것인가요?
else if(new_champion) {
champion = new NewChampion();
}
팩토리는 이러한 부분을 해결하기 위해 관용적으로 사용하고 있는 개념입니다. 객체를 생성하는 부분을 따로 분리하여 새로운 객체로 생성 합니다.
팩토리 적용 순서
Factory
라는 이름을 붙여줍니다.적용하는 내용에 알맞게 한 번 수정해보겠습니다.
public class ChampionFactory {
public Champion selectChampion(String type) {
Champion champion;
if (poppy) {
champion = new Poppy();
} else if(sylas) {
champion = new Sylas();
}
.... 이하 생략
return champion;
}
}
ChampionFactory
를 사용하는 클래스가 많아질수록 구현을 변경해야 하는 경우 이곳저곳 들어가 수정할 필요없이 Factory 클래스만 수정해주면 되는 이점이 존재합니다.
자 이제 우리는 팩토리에 대해 이해했습니다! 이제는 본 주제인 팩토리 패턴에 대해 알아볼 시간입니다.
팩토리 메소드 패턴은 객체를 생성하기 위한 인터페이스를 정의하는데 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하는 패턴입니다.
쉽게 말하자면 인스턴스는 내가 만드는 것이 아닌 팩토리에서 만드는 것입니다.
이를 통해 개발자는 수정 사항이나 추가 구현 사항이 발생할 경우 다른 놈들은 신경쓰지 않고 팩토리만 조지면 된다는 이점을 얻을 수 있습니다.
그럼 예시를 통해 조금 더 확실히 알아보겠습니다.
피자나라 치킨공주를 운영하는 박모씨, 치킨 공주의 매출이 저조하며 사람들의 수요가 피자로 몰려 피자 전문점
피자나라
로 리뉴얼 하려고 한다. 박모씨는 음식점 사장이기 전에 개발일을 한 사람이다. 따라서 조금이나마 비용 절감을 위해 시스템을 나와 함께 개발할 예정인데 어떤 것들을 고려하며 시스템을 개발할 것인가?
가장 먼저 고려해야 될 요소는 기존 피자집에서 무엇을 팔았느냐일 것입니다. 기존에 운영하고 있는 시스템에서 일부만을 고치면(사실 치킨만 빼면 되겠지만) 피자 전문점으로 거듭날 수 있습니다. 피자 전문점으로 태어나기 위해서는 어떤 것들이 필요할지 알아보겠습니다.
좀 더 구체적으로 알 수 있게 박씨에게 물어본 결과 다음과 같은 답변을 받게 되었습니다.
우리는 프랜차이즈고 피자는 지역별로 지들 맘대로 특산품을 토핑으로 깔아서 팔아서 스페셜로도 팔아요.. 다 좋은데 모든 매장에서 파는 피자 종류에 대해서는 건들지 않도록 시스템을 만들었으면 좋겠습니다.
모든 내용을 요약하자면 다음과 같습니다.
그렇다면 이제 고민을 해봅시다.. 피자를 주문하는 사람은 다음과 같이 주문을 할 것입니다.
주문 과정까지 알아보았으니 이제 박씨의 요구사항과 주문까지 확실하게 커버할 수 있는 아키텍쳐 및 시스템 코드를 들고 돌아오겠습니다.
앞서 말한 팩토리의 정의를 상기시켜보겠습니다.
팩토리는 이러한 부분을 해결하기 위해 관용적으로 사용하고 있는 개념입니다. 객체를 생성하는 부분을 따로 분리하여 새로운 객체로 생성 합니다.
이 때, 객체를 생성하는 부분을 따로 분리하여 새로운 객체로 생성하는데 우리는 이것을 팩토리 클래스로 부른다고 했습니다. 현재 이것저것 생성을 해야되는데 공통적인 부분이 있는지 확인해볼까요?
공통적으로 생성되는 클래스
프랜차이즈이기 때문에 매점이 있을 것이고 매점별로 또 판매되는 피자가 있을 것입니다.
그렇다면 일단 기본적으로 두 가지 팩토리 클래스를 생성하고 시작해보겠습니다.
이게... 구조가 왜 이렇죠...?
구조는 다음을 고려해서 설계하였습니다.
이 때, 결과적으로 피자를 먹기 위해서는 무조건 매점에서 주문을 해야 하기 때문에 매점과 피자는 포함관계라는 것을 알 수 있습니다. 따라서 매점은 최소 1개 이상의 피자를 포함하고 있습니다.
이를 이용하기 위해서는 피자를 만들 때 동일한 과정으로 피자를 만들어야 되기 때문에 다음과 같이 StoreFactory 내부에서 orderPizza() 메소드를 통해 과정을 공통화 할 계획입니다.
그럼 코드를 볼까요?
public abstract class Pizza {
private String name;
private int price;
public Pizza(String name, int price) {
this.name = name;
this.price = price;
}
public int getPrice() {
return price;
}
public String getName() {
return name;
}
public void printPrice(){
System.out.println("Pizza Price is " + getPrice());
}
public void prepare(){
System.out.println(getName() + " Pizza Prepare....");
}
public void bake(){
System.out.println("Baking ~ 20 minutes");
}
public void cut(){
System.out.println("Cut! ~ 1 minutes");
}
public void box(){
System.out.println("Boxing ~ 1 minutes");
}
}
public class BasicPizza extends Pizza {
public BasicPizza(String name, int price) {
super(name, price);
}
}
public class CheezePizza extends Pizza {
public CheezePizza(String name, int price) {
super(name, price);
}
}
public abstract class PizzaStore {
public Pizza createPizza(String type) {
if ("potato".equalsIgnoreCase(type)) {
return new PotatoPizza("Potato", 14000);
} else if ("gimochi".equalsIgnoreCase(type)) {
return new GimochiPizza("Gimochi", 12000);
} else if ("cheeze".equalsIgnoreCase(type)) {
return new CheezePizza("Cheeze", 8000);
} else if ("special".equalsIgnoreCase(type)) {
return new SpecialPizza("Special", 30000);
} else {
return new BasicPizza("Basic", 7000);
}
}
public void orderPizza(String type) {
Pizza pizza = createPizza(type);
System.out.println("------------------Order---------------");
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
pizza.printPrice();
System.out.println("------------------End---------------");
}
}
public class SeoulStore extends PizzaStore{
}
public class IncheonStore extends PizzaStore{
}
public class main {
public static void main(String args[]) {
BusanStore busanStore = new BusanStore();
busanStore.orderPizza("potato");
busanStore.orderPizza("special");
}
}
결과는 다음과 같이 출력됩니다.
이게 왜 팩토리 메소드 패턴인지 궁금하신 분들이 있으실 것 같아 설명해 드리겠습니다. 앞서 본 클래스 다이어그램에서 매장과 피자를 나누어 보시면 더 편할 것 같습니다.
치즈피자, 스페셜피자 등은 모두 피자에서 파생된 것이기 때문에 피자는 공통적인 성질을 추상 클래스로 묶어 설계하였습니다.
이것들만 있으면 피자를 만들수도 있고, 어떤 피자인지 얼마인지를 알 수 있기 때문에 다음과 같은 구조로 설계하였습니다.
매장 또한 서울점, 경기점,... XX점 으로 모든 매장은 매장으로 공통화 시킬 수 있습니다. 매장이 하는 일들을 잠시 생각해 볼까요?
따라서 다음과 같이 공통으로 묶을 수 있는 성질들을 하나로 모아 한 곳에서 생성을 관리하는 팩토리 클래스를 이용했는데 이 때, 팩토리 클래스와 팩토리 클래스가 서로 관련이 있어 매장에서 피자 팩토리 클래스를 호출해 피자를 주문하는 구조를 만든 것입니다.
그럼 이 구조에서 어떤 부분을 추가하여야 각 지역만의 스타일을 가진 피자를 추가로 만들 수 있을까요? 제가 생각한 가장 간단한 답변은 다음과 같습니다.
public abstract class PizzaStore {
public Pizza createPizza(String type) {
if ("potato".equalsIgnoreCase(type)) {
return new PotatoPizza("Potato", 14000);
} else if ("gimochi".equalsIgnoreCase(type)) {
return new GimochiPizza("Gimochi", 12000);
} else if ("cheeze".equalsIgnoreCase(type)) {
return new CheezePizza("Cheeze", 8000);
} else if ("special".equalsIgnoreCase(type)) {
return new SpecialPizza("Special", 30000);
} else if ("basic".equalsIgnoreCase(type)) {
return new BasicPizza("Basic", 7000);
}
return null;
}
public void orderPizza(String type) {
Pizza pizza = createPizza(type);
if(Objects.isNull(pizza)) {
System.out.println("Sorry. There is no Pizza who you want.");
} else {
System.out.println("------------------Order---------------");
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
pizza.printPrice();
System.out.println("------------------End---------------");
}
}
}
public class SeoulStore extends PizzaStore {
@Override
public Pizza createPizza(String type) {
Pizza pizza = super.createPizza(type);
if ("seoul".equalsIgnoreCase(type)) {
pizza = new SeoulPizza("Seoul", 20000);
}
return pizza;
}
@Override
public void orderPizza(String type) {
Pizza pizza = this.createPizza(type);
System.out.println("------------------Order---------------");
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
pizza.printPrice();
System.out.println("------------------End---------------");
}
}
public class main {
public static void main(String args[]) {
SeoulStore seoulStore = new SeoulStore();
seoulStore.orderPizza("seoul");
}
}
음... 오버라이딩을 통해 super 클래스 메소드를 참고하여 null일 경우, 서울점에서만 만들 수 있는 피자를 추가로 구현할 수 있도록 구성해 보았습니다.
하지만 이게 과연 우리가 팩토리 메서드 패턴을 이용하는 방법일까요? 그렇다면 결국 서울 매점 또한 팩토리 클래스에서 담당해야할 주문과 인스턴스 생성에 대한 권한을 가지게 됩니다.
따라서 각 지점을 팩토리 클래스로 임명할 경우, 이런 방식으로 수정하는 방법 또한 한 가지 방법이라고 생각합니다. 하지만 우린 언제나 더 좋은 방법을 찾을 필요가 있기 때문에 조금 더 고민해보는 시간을 가져보겠습니다.
어떻게 수정하는 것이 좋을까요??
제 생각엔 다음과 같은 구조가 괜찮을 것 같습니다.
createPizza()
메소드를 추상 메소드로 변경하고 매장별로 스타일에 알맞게 구현합니다.orderPizza()
메소드는 PizzaStore 클래스에서 관리합니다.앞서 작성했던 코드는 PizzaStore 클래스와 각 매장을 모두 팩토리 메소드로 이용하겠다는 생각이었으나, 변경된 구조에서는 PizzaStore에서는 주문 관련 메소드만 구현을 진행하고 피자를 만드는 부분을 각 매장으로 이동시켰습니다.
이를 통해 매장은 프랜차이즈 특성상 공통적인 주문 및 조리 방식은 존재하나, 매점 특성을 살려 개성있는 피자를 판매할 수 있게 되었습니다.
결론을 보기 이전에 우리는 은연중에 팩토리 메서드 패턴을 적용했습니다. 과연 어떤 부분에서 적용했는지 알아볼까요?
public void orderPizza(String type) {
Pizza pizza = createPizza(type);
System.out.println("------------------Order---------------");
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
pizza.printPrice();
System.out.println("------------------End---------------");
}
다음과 같이 메소드 내에서 팩토리 클래스를 이용해 생성하는 부분을 클래스/메소드로 전환하는 패턴을 팩토리 메소드 패턴이라 부릅니다.
우리는 무의식적으로 두 가지 팩토리 클래스를 이용하여 팩토리 메소드 패턴을 구현해봤네요 ㅎㅎ..
팩토리 패턴을 이용하면 생산하는 부분을 분리하여 관리할 수 있다는 장점이 존재합니다.
단순히 팩토리 클래스를 적용해 팩토리 패턴을 이용하실 경우, 객체 생성을 캡슐화한 이점 이외에는 큰 장점을 못 느끼실 겁니다. 하지만 지금 보신 팩토리 메서드 패턴은 메소드 내부에서 팩토리 클래스를 통해 좀 더 유연한 구현을 가능하게 만들어주는 추가적인 이점을 주기 때문에 해당 패턴을 잘 이해하시고 적용하셨으면 좋겠습니다.
이번 시간에 이해가 잘 안되셨을 수도 있습니다. 하지만 다음 시간에 팩토리 메소드 패턴과 유사한 추상화 팩토리 패턴에 대해 알아보는 시간을 가지기 때문에 이해가 안되신 분들은 다음 글을 추가로 봐주시면 감사하겠습니다.