////
Search
Duplicate
🌖

Chapter 04. Factory

new 연산자가 눈에 띈다면 구체적이라는 용어를 떠올리자.

⇒ 당연히 추상적인 클래스를 사용하는 것이 아니라 구상 클래스를 사용하게 된다.
Duck duck = new MallardDuck(); 타입은 인터페이스로 선언했으나 결국 구상 클래스의 인스턴스를 만들게 된다.
new 연산자를 사용하면 구상 클래스의 인스턴스가 생성되고 해당 인스턴스의 주소를 갖게 된다. (Java의 경우)
만약 특정 인터페이스 타입의 구상 클래스가 여러개 있다면 런타임 상황에 해당 타입의 객체를 생성할 때, 특정 조건에 따라 생성하게 된다.
Duck duck; if (picnic) { duck = new MallardDuck(); { else if (hunting) { duck = new DecoyDuck(); } else if (inBathTub) { duck = new RubberDuck(); }
Java
복사
⇒ 해당 객체가 어떤 구상 클래스의 객체인지 결정되는 것은 런타임 시점이다. 우리는 컴파일 시점에서 duck이라는 변수가 어떤 구상 클래스의 인스턴스 주소를 가리키는지 알 수 없다.
만약 새로운 오리 종류가 추가된다면? 위의 순서에 영향이 갈 수 있는 부분을 체크하고 새로운 코드를 추가하거나 기존 코드를 제거해야 한다. 변경사항에 유연하지 못하게 되는 것이다.

그렇다면 new에는 어떤 문제가 있는 걸까?

사실 new에는 문제가 없다. 다만 변경 가능성이 문제가 있는 것이다. 변화하는 무언가 때문에 우리는 new를 조심해서 사용해야 한다.
인터페이스에 맞춰서 프로그래밍하면 변경 가능성에 대처할 수 있다. 인터페이스를 바탕으로 만들어진 코드는 어떤 클래스든 특정 인터페이스의 조건만 만족하면 되기 때문이다.
⇒ 다형성!
반대로 구상 클래스를 많이 사용하게 된다면 새로운 구상 클래스가 추가될 때마다 코드를 고쳐야 하므로 수많은 문제가 발생할 수 있다.
⇒ 변경에 닫혀있는 코드가 되는 것이다!
새로운 구상 형식을 써서 확장해야할 가능성이 있다면 어떻게 해서든 변경에 열려있게 만들어야 한다.
바뀌는 부분을 찾아내서 바뀌지 않는 부분과 분리해야 한다는 원칙을 사용해보자. 우린 이것을 벌써 3번이나 했다.

최첨단 피자 코드 만들기 ⇒ 바뀌는 부분을 찾아내자.

public Pizza orderPizza(String type) { Pizza pizza = new Pizza(); if (type.equals("cheese")) { pizza = new CheesePizza(); } else if (type.equals("greek")) { pizza = new GreekPizza(); } else if (type.equals("pepperoni")) { pizza = new PepperoniPizza(); } pizza.prepare(); pizza.bake(); pizza.cut(); pizza.box(); return pizza; }
Java
복사
요구사항에 변경이 생길 수 있는 부분을 생각해서 바뀌는 부분과 바뀌지 않는 부분을 구분해보자.

피자 코드 추가하기 ⇒ 만약 신메뉴 요구사항이 발생한다면?

public Pizza orderPizza(String type) { Pizza pizza = new Pizza(); // 바뀌는 부분 // if (type.equals("cheese")) { pizza = new CheesePizza(); } else if (type.equals("pepperoni")) { pizza = new PepperoniPizza(); } else if (type.equals("clam")) { pizza = new ClamPizza(); } else if (type.equals("veggie")) { pizza = new VeggiePizza(); } // 바뀌지 않는 부분 // pizza.prepare(); pizza.bake(); pizza.cut(); pizza.box(); return pizza; }
Java
복사
위 메소드 orderPizza에서 가장 문제가 되는 부분은 인스턴스를 만드는 구상 클래스를 선택하는 부분이다.
이 부분에 변경이 생기는 경우, 코드를 변경하게 된다는 것이다. 이제 어떤 부분이 바뀌는 부분이고 어떤 부분이 바뀌지 않는 부분인지 알았으니 캡슐화를 해보자.

객체 생성 부분 캡슐화하기

객체 생성 부분을 orderPizza에서 뽑아내야 한다.
우선 객체 생성 코드만 따로 SimplePizzaFactory라는 객체로 빼내보자.
public class SimplePizzaFactory { public Pizza createPizza(String type) { Pizza pizza = null; if (type.equals("cheese")) { pizza = new CheesePizza(); } else if (type.equals("pepperoni")) { pizza = new PepperoniPizza(); } else if (type.equals("clam")) { pizza = new ClamPizza(); } else if (type.equals("veggie")) { pizza = new VeggiePizza(); } return pizza; } }
Java
복사

클라이언트 코드 수정하기

public class PizzaShop { private final SimplePizzaFactory factory; public PizzaShop(SimplePizzaFactory factory) { this.factory = factory; } public Pizza orderPizza(String type) { Pizza pizza; pizza = factory.createPizza(type); pizza.prepare(); pizza.bake(); pizza.cut(); pizza.box(); return pizza; } }
Java
복사

간단한 팩토리의 정의

디자인 패턴이라기 보다는 프로그래밍에서 자주 쓰이는 관용구에 속한다.
간단한 팩토리를 팩토리 패턴이라고 부르는 사람도 있으나 이는 엄밀히 말해서 패턴은 아니다.
팩토리를 사용하므로써 객체 생성에 대한 책임을 팩토리에게 위임하였다. 즉 Pizza 클래스의 구상 클래스들에 더 이상 PizzaShop이 의존하지 않게되었다는 것!
즉 특정 타입의 객체를 사용하는 객체가 해당 타입의 구상 클래스의 변경에 영향을 받지 않는다는 것!!

다양한 팩토리 만들기

이제 다양한 팩토리를 만들어보자. 더이상 대중들이 단순한 피자가 아닌 뉴욕 스타일의 피자, 시카고 스타일의 피자를 원하게 되었다.
그렇다면 우리는 SimplePizzaFactory 외에도 NYPizzaFactory, ChicagoPizzaFactory 등의 팩토리들을 만들 수 있다.

피자 가게 프레임워크 만들기

createPizza() 메소드를 PizzaStore에 다시 넣고 해당 메소드를 추상 메소드로 선언하고 각 지역별 스타일에 맞게 서브 클래스를 만들면 된다.
public abstract class PizzaStore { public Pizza orderPizza(String type) { Pizza pizza; pizza = createPizza(type); ... } abstract Pizza createPizza(String type); }
Java
복사
public abstract class NYStylePizzaStore { abstract Pizza createPizza(String type) { if (type.equals("cheese")) { pizza = new NYStyleCheesePizza(); } else if (type.equals("pepperoni")) { pizza = new NYStylePepperoniPizza(); } else if (type.equals("clam")) { pizza = new NYStyleClamPizza(); } else if (type.equals("veggie")) { pizza = new NYStyleVeggiePizza(); } } }
Java
복사
public abstract class ChicagoStylePizzaStore { abstract Pizza createPizza(String type) { if (type.equals("cheese")) { pizza = new ChicagoStyleCheesePizza(); } else if (type.equals("pepperoni")) { pizza = new ChicagoStylePepperoniPizza(); } else if (type.equals("clam")) { pizza = new ChicagoStyleClamPizza(); } else if (type.equals("veggie")) { pizza = new ChicagoStyleVeggiePizza(); } } }
Java
복사
위 코드를 보면 피자의 종류는 어떤 서브 클래스를 선택했느냐에 따라서 달라진다.
변경이 발생할 가능성이 적은 부분은 구현하여 최상위 추상 클래스에 배치해두고 변경되어야할 부분만 하위 클래스에서 재정의하여 객체 생성 시 결정되도록하는 것이다.

팩토리 메소드 패턴 살펴보기

모든 팩토리 패턴은 객체 생성을 캡슐화한다. 팩토리 메소드 패턴은 서브 클래스에서 어떤 클래스를 만들지 결정하게 함으로써 객체 생성을 캡슐화했다.
팩토리 메소드 패턴에서는 객체를 생성할 때 필요한 인터페이스를 만들고 어떤 클래스의 인스턴스를 만들지는 서브클래스에서 결정하게 한다.
팩토리 메소드 패턴을 사용하면 클래스 인스턴스 만드는 책임을 서브클래스에게 위임하게 된다.

객체 의존성 살펴보기

기존 코드는 PizzaStore에서 수많은 구상 클래스에 의존했어야 했다. 따라서 구상 클래스가 추가될때마다 createPizza 메소드에 if문이 하나 더 늘어나게 되었을 것이다.
팩토리 메소드 패턴을 이용해서 구상 클래스 의존성을 줄였다. 이를 의존성 역전의 원칙이라고 부른다. 이 원칙은 다음과 같이 정의가능하다.
추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.

의존성 역전 원칙을 지키는 방법

변수에 구상 클래스의 참조값을 저장하지 않는다. 즉 new 연산자를 사용하지 않는다.
구상 클래스에서 유도된 클래스를 만들지 않는다.
베이스 클래스에 이미 구현되어 있는 메소드를 오버라이드하지 않는다.

추상 팩토리 패턴

구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 객체군을 생산하는 인터페이스를 제공한다.
구상 클래스는 서브 클래스에서 만든다.