본문 바로가기

OOP, FP/디자인패턴

헤드퍼스트 디자인 패턴: 4-1. 팩토리 메소드 패턴


지난 포스팅에선 객체를 감싸서 새로운 임무를 부여하는 데코레이터 패턴에 대해 알아보았다.

이번에는 객체를 생성하는 과정을 분리하여 불필요한 의존성을 없애는 팩토리 패턴에 대해 복습한다.


구상 객체와 new 키워드


Duck duck = new MallardDuct();

Duck이라는 인터페이스를 써서 코드를 유연하게 만드려고 했는데,

대입하는 쪽을 보니 MallardDuck이라는 구상 클래스의 인스턴스를 생성해서 대입했다.


여기서 구상클래스가 여러개 있을 경우 어쩔 수 없이 아래와 같은 코드를 만든다.

Duck duck;


if(picnic) {

duck = new MallardDuck()

} else if(hunting) {

duck = new DecoyDuck();

} else if(inBathTub) {

duck = new RubberDuck();

}


이 코드를 보면 오리를 나타내는 일련의 클래스들이 있긴 하지만,

컴파일시에는 어떤 것의 인스턴스를 만들어야 하는지 알 수 없다.

즉, 만들어지는 인스턴스의 형식은 실행시에 주어진 조건에 따라 결정된다.


이러한 코드가 있다는 것은, 무언가 변경하거나 확장할 때 코드를 다시 확인하고

추가 또는 제거해야한다는 것을 뜻한다. 결국 관리 및 갱신이 어려워지고 오류의 가능성도 높아진다.


그렇다고 new 키워드 자체에 문제가 있는 것은 아니다.

진정한 문제는 변화가 불러오는 상황에 있다. 어떻게 변화에 대비해야할까?


그 답은 두개의 디자인 원칙에 있다. 세번째 원칙 "구현보다 인터페이스에 맞춰 프로그래밍하라"을 기억해보자

인퍼테이스에 맞춰서 코딩을 하면 시스템에서 일어날 수 있는 여러 변화를 이겨낼 수 있다.


다형성 덕분에 어떤 클래스든 특정 인터페이스만 구현하면 해당 인스턴스의 타입으로 사용할 수 있다.

반대로 코드에서 구상 클래스를 많이 사용하면 새로운 구상 클래스가 추가될 때마다 코드를 고쳐야한다.

즉, 변화와 확장에 대해 닫혀있는 코드가 된다.

확장이 필요할 때 어떻게 해서든 확장에 열려있게 해야한다.


확장에 대해서 열린 코드를 만들기 위해서는 첫번째 원칙 "바뀌는 부분을 캡슐화하라"를 기억해야한다.


간단한 팩토리(Simple Factory)

객체 생성을 처리하는 클래스를 팩토리라고 부른다.

보통 자바에서 new는 구상 객체를 뜻한다.

이는 new를 사용하면 구상 클래스의 인스턴스를 만든다는 의미이다.

우리는 인터페이스가 아닌 특정 구현에 의존하면 안된다고 공부했었다.

나중에 코드를 수정해야할 가능성아 높아지고, 유연성이 떨어지니까!


Ex)

public class SimpleUnitFactory {


public Unit createUnit(String unitNm) {


Unit unit = null;


if(unitNm.equals(unit.getName("Marine")) {

unit = new Marine();

} else if(unitNm.equals(unit.getName("Firebat")) {

unit = new Firebat();

} else if(unitNm.equals(unit.getName("Vessel")) {

unit = new vessel();

}   .......

}

}


위와 같이 객체 생성을 팩토리가 담당하게 하면 아래와 같은 특징이 생긴다.


(1) SimpleUnitFactory를 많은 클라이언트가 사용하는 경우.

- 여기서는 createUnit() 메소드만 정의했지만, 유닛 객체를 받아서

  유닛의 요구 미네랄양이라던가 유닛에 대한 정보 등을 찾아서 활용하는 Barracks와 같은 클래스와 같은곳에서

  이 팩토리를 사용할 수 있다.


(2) 따라서 유닛을 생산하는 작업을 한 클래스에 캡슐화시켜 놓으면 구현을 변경해야 하는 경우 여기저기 다 들어가 고칠 필요 없이 이 팩토리 클래스 하나만 고치면 된다.


* 정적 팩토리란 간단한 팩토리를 정적 메소드로 정의하는 기법이다. 이를 이용하면 팩토리의 인스턴스를 만들지 않아도 된다.

하지만 서브클래스를 만들어서 객체 생성 메소드의 행동을 변경할 수 없다.



팩토리 메소드 패턴

객체를 생성하기 위한 인터페이스를 정의한다. 이 때 어떠한 클래스의 인스턴스를 만들지는

서브클래스에 의해 결정된다. 즉, 클래스의 인스턴스를 만드는 일을 서브클래스에게 맡긴다.


구상 형식의 인스턴스를 만드는 작업을 캡슐화하는 팩토리 패턴의 종류는 여러가지가 있다. 

"팩토리 메소드 패턴에서는 어떤 클래스의 인스턴스를 만들지는 서브클래스에 의해 결정된다."


위에서 결정한다 라는 표현을 사용한 이유는 실제로 서브클래스가 어떤 인스턴스를 만들지 결정하는 의사판단을 하는게 아니라,

어떤 서브클래스를 사용하는지에 따라 생산되는 객체 인스턴스가 결정되기 때문이다.

서브클래스가 실행중에 어떤 클래스의 인스턴스를 만들지 결정하는 것이 아니라,

생산자 클래스 자체가 사전 지식이 전혀 없는 상태에서 만들어지기 때문이다.



(1) Product: 제품

제품 클래스를 추상화시킨 인터페이스.

생산될 수 있는 제품들은 모두 이 인터페이스를 구현해야 한다.

클라이언트나 제품을 사용할 클래스에서 구체적인 구상 클래스가 아닌 인터페이스의 래퍼런스를 사용하기 위한 인터페이스.


(2) Creator: 생산자

제품을 가지고 원하는 일을 하기 위한 모든 메소드들(anOperation()... 등)이 구현되어 있는 추상 클래스.

실제로 제품을 만드는 메소드(factoryMethod())는 구현이 없는 추상 메소드로 정의한다.

제품을 생산하는 역할을 가진 모든 클래스는 해당 클래스를 상속하여 해당 추상메소드를 구현해야한다.


* 실제로 제품을 생산하는 factoryMethod()는 Creator의 서브클래스에서 구현한다.

- 실제 구상 클래스 인스턴스(ConcreateProduct)를 만들어내는 작업은 해당 클래스에서 책임진다.

- 실제 제품을 만드는 방법을 알고있는 클래스는 Creator가 아닌 ConcreateCreator이다.




스타크래프트 테란 종족의 유닛과 건물의 일부를 표현해봤다.

Barracks는 지상 인간형 유닛인 Marine, Firebat을 생산하고 

Startport는 공중유닛인 Vessel, Battlecruiser, Dropship을 생산한다.


클라이언트에서 배럭이나 스타포트에서 표시된 유닛을 클릭한다.

그럼 해당 유닛의 이름을 인자로 넘기고 requestCreateUnit(String unitNm)을 호출한다.


requestCreateUnit()에서 클라이언트가 소유중인 미네랄과 해당 유닛의 미네랄을 비교하거나 하는 로직을 검사하고

추상메소드 createUnit(unitNm)을 호출한다. 

서브클래스가 배럭인 경우 배럭의 createUnit()을, 스타포트인 경우 스타포트의 createUnit()을 호출하게 될 것이다.

지상 인간형 유닛을 생성하는 로직은 배럭

공중 유닛을 생성하는 로직은 스타포트에 들어있다.

즉, 어떤 클래스를 만들지를 결정하는 것은 서브클래스(배럭, 스타포트)에서 하는 것이다.


피자가게로 치면 배럭과 스타포트는 각 분점과 같은 요소라고 표현할 수 있겠다.

본점인 PizzaCreator가 있고, 분점에서 각각 고유의 피자를 생산하고 판매한다.

이러한 서브클래스를 구상 생산자라고 한다.


* 팩토리 메소드는 인스턴스를 생성하는 createUnit() 메소드이다.


팩토리 메소드 패턴은 팩토리 메소드와 결환된 requestCreateUnit() 메소드를 제공하는 형태의 프레임워크이다.

Unit을 생산하고 싶은 클라이언트나 이를 사용하는 클래스에서는 requestCreateUnit()만 호출하면 되기때문이다.


이를 Unit에 관한 지식을 각 생산자에 캡슐화하는 방법에 초점을 맞추어보자.

그러면 다른 형태의 프레임워크로 표현할 수 있다.


Barracks에서는 지상 인간형 유닛을 만드는 것에 대한 모든 지식이 캡슐화 되어있다.

Starport는 공중 유닛을 만드는 것에 대한 모든 지식이 캡슐화 되어있다.


즉 이러한 지식을 캡슐화해서 클라이언트로부터 분리하여 제공하는 프레임워크로 바라볼 수 있다.

새로운 형태의 팩토리가 추가되면 (Ex: 커맨드 센터, 팩토리) UnitCreator을 상속받은 구상 생산자를 정의하고

각각에 알맞은 지식을 캡슐화하여 createUnit을 구현하면 된다.


* 여기서 requestCreateUnit의 매개변수로 String형의 unitNm을 전달했다.

  이렇게 전달받은 매개변수로 한 가지 이상의 객체를 만드는 것을 매개변수 팩토리 메소드라고 한다.


의문


(1) 구상 생산자(ConcreateCreator)가 하나만 있다면 팩토리 메소드 패턴의 장점이 있을까?

- 이러한 경우에도 팩토리 패턴을 사용하지 않는다면 제품을 생산하는 부분과 사용하는 부분이 같은 몸체에 있게된다.

  제품을 생산하는 역할을 분리시켜주면 결합이 느슨해진다. 

  결합이 느슨해지면 제품의 구성을 변경한다해도 생산자는 변경하지 않아도 된다.


(2) 간단한 팩토리와 팩토리 메소드 패턴의 차이?

- 간단한 팩토리는 팩토리를 사용하는 객체안에 포함되는 별개의 객체이다.

  즉 서브클래스와 같은 방법을 이용한게 아니고 하나의 팩토리가 직접 구상 제품에 의존하는 형태.

  팩토리 메소드 패턴에서는 구상 클래스를 만들 때, 추상 클래스를 확장해서 서브클래스를 이용한다.


  간단한 팩토리는 일회용 처방에 불과한 반면 팩토리 메소드 패턴은 어떤 구현을 사용할지를

  서브클래스에서 결정하는 프레임워크를 만들 수 있다는 결정적인 차이가 있다.

  간단한 팩토리에서는 객체 생성을 캡슐화하는 방법을 사용하긴 하지만,

  생성하는 제품을 마음대로 변경할 수 없기 때문에 팩토리 메소드 패턴처럼 강력한 유연성을 제공하진 못한다.


(3) 팩토리 패턴은 반드시 여러 개의 제품을 만들어여 하나?

- 매개변수 팩토리 메소드를 사용해서 매개변수를 바탕으로 한 가지 이상의 객체를 만들 수 있다.

  하지만 매개변수를 사용하지 않고 그냥 한 가지의 객체만 생성할 수도 있다.


(4) 매개변수 팩토리 메소드는 형안정성(type-safety)에 지장이 있지 않은가?

- 배럭에 매개변수로 marine을 넘겨야하는데 실수로 mairne을 넘기면 런타임 오류가 발생한다.

  이런 런타임 오류를 방지하여 형식 안정성을 조금 더 잘 보장해줄 수 있는 기법들이 존재한다.

  예를 들어 매개변수 형식을 나타내기 위한 객체(Enum 등)을 만들 수 있고, 정적 상수를 사용해서

  컴파일 단계에서 오류를 잡아낼 수 있다.



객체 생성을 캡슐화하여 서브클래스에게 위임하는 팩토리 메소드패턴에 대한 복습을 마치겠다.


1. 스트래티지 패턴

2. 옵저버 패턴

3. 데코레이터 패턴

4-1. 팩토리 메서드 패턴