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 연산자를 사용하지 않는다.
•
구상 클래스에서 유도된 클래스를 만들지 않는다.
•
베이스 클래스에 이미 구현되어 있는 메소드를 오버라이드하지 않는다.
추상 팩토리 패턴
•
구상 클래스에 의존하지 않고도 서로 연관되거나 의존적인 객체로 이루어진 객체군을 생산하는 인터페이스를 제공한다.
•
구상 클래스는 서브 클래스에서 만든다.