팩토리 패턴(Factory Pattern)

새로운 연산자를 만드는 것보다 객체지향에서는 ‘객체를 생성하는 과정’이 훨씬 중요하다.
객체의 인스턴스를 만드는 작업이 항상 공개되어야 하는것은 아니며, 모든 것을 공개했다가는 결합과 관련된 문제가 생길 수 있다.

따라서 객체 생성을 위한 별도의 클래스를 정의하고 이를 통해, 불필요한 의존성을 제거할 수 있고,
느슨한 결합을 이용하여 객체지향적으로 설계할 수 있다.

간단한 팩토리(Simple Factory) ; SRP, OCP, 객체 생성, 캡슐화, 변화

객체 생성 부분을 담당하는 별도의 객체, 보통 이런 객체의 이름은 .*Factory로 지어지며,
객체 생성을 담당하는 메소드는 static으로 정의된다.

간단한 팩토리는 단순히 객체 생성 역할을 캡슐화 했다는 것에 의의가 있을 뿐,
설계 패턴으로는 정의되지 않는다.

팩토리 메서드 패턴(Factory Method Pattern)

객체를 생성하기 위한 인터페이스를 정의한다. 이 때, 어떤 클래스의 인스턴스를 만들지는 서브 클래스에서 결정하게 만든다.
팩토리 메서드 패턴을 이용하면, 클래스의 인스턴스를 만드는 일을 서브 클래스에게 맡기게 된다.

1
abstract Product factoryMethod(String type); // 서브 클래스에서 구현 

추상 팩토리 패턴(Abstract Factory Pattern)

추상 팩토리 패턴에서는 인터페이스를 이용하여 서로 연관된, 또는 의존하는 객체를 구상 클래스를 지정하지 않고도 생성할 수 있다.

패턴을 별도로 정의하곤 있지만 발전과정일 뿐, abstract factory + factory method pattern 으로 구현되곤 한다.

책에서 나오는 예시 - 피자가게 시나리오

  1. 피자 종류가 추가되거나 삭제되는 경우 유연하게 확장할 수 없다.
  2. 변화가 생기는 부분과 변화하지 않는 부분을 분리한다.
  3. 이를 통헤 유연하게 확장이 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// AS-IS
public class PizzaStore {
public Pizza orderPizza(String type) {
Pizza pizza;

/**
* 만약 피자가 추가되거나 삭제된다면? --> 변화
*/
if ("cheese".equals(type)) {
pizza = new CheesePizza();
} else if ("greek".equals(type)) {
pizza = new GreekPizza();
} else if ("pepperoni".equals(type)) {
pizza = new PepperoniPizza();
}

pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();

return pizza;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// TO-BE

/**
* 피자 생성을 담당하는 SRP, OCP 원칙에 맞는 별도 팩토리 클래스 정의
*/
public class SimplePizzaFactory {
public static Pizza createPizza(String type) {
Pizza pizza = null;

if ("cheese".equals(type)) {
pizza = new CheesePizza();
} else if ("greek".equals(type)) {
pizza = new GreekPizza();
} else if ("pepperoni".equals(type)) {
pizza = new PepperoniPizza();
}
return pizza;
}
}

public class PizzaStore {
public Pizza orderPizza(String type) {
Pizza pizza = SimplePizzaFactory.createPizza(type);

pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();

return pizza;
}
}

Spring Boot 에서 찾은 팩토리 패턴

Spring 의 핵심 개념인 IoC 의 역할이 ‘객체 생성’과 관련되어 있다 보니,
내부적으로 무수히 많은 Factory Pattern 을 찾아볼 수 있다.

그 중 가장 많이 볼 수 있는 LoggerFactory 에 대해 살펴보려고 한다.
LoggerFactory의 역할은 하나다. 내가 지금 이 클래스에서 사용할 logger의 인스턴스를 반환해줘

따라서, LoggerFactory 는 적절한 logger 인스턴스를 생성한 뒤 반환해준다.

아래는 어떤 순서로 logger 인스턴스가 생성되는 지 추적해봤다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 사용 부분
private static final Logger logger = LoggerFactory.getLogger(클래스명.class);

// getLogger(Class<?> clazz) 구현 부분
public static Logger getLogger(Class<?> clazz) {
Logger logger = getLogger(clazz.getName());
if (DETECT_LOGGER_NAME_MISMATCH) {
Class<?> autoComputedCallingClass = Util.getCallingClass();
if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
autoComputedCallingClass.getName()));
Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
}
}
return logger;
}

// getLogger(String name) 구현 부분
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}

// getILoggerFactory()
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
performInitialization();
}
}
}
switch (INITIALIZATION_STATE) {
case SUCCESSFUL_INITIALIZATION:
return StaticLoggerBinder.getSingleton().getLoggerFactory();
case NOP_FALLBACK_INITIALIZATION:
return NOP_FALLBACK_FACTORY;
case FAILED_INITIALIZATION:
throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
case ONGOING_INITIALIZATION:
// support re-entrant behavior.
// See also http://jira.qos.ch/browse/SLF4J-97
return SUBST_FACTORY;
}
throw new IllegalStateException("Unreachable code");
}

LoggerFactory의 logger 구현체는 Spring Boot 의 경우 logback 을 사용하고 있다.

getLoggerFactory() 안을 보면, getLoggerContext() 부분이 있는데, 이 메소드의 구현체를 보면,
Logback 라이브러리이다.

slf4j 는 인터페이스이다.

slf4j 의 구현체는 여러 종류가 될 수 있다. log4j, logback
LoggerFactory 의 구현체가 어떤 라이브러리인지에 따라 나오는 logger 가 다를 수 있지만,
로거의 인스턴스를 각 클래스 명에 맞게 반환해준다는 점에서 abstract factory + factory method 패턴 모두가 사용되었다는 부분을 확인할 수 있다.

생각해보자

팩토리 패턴은 그 정의와 같이 생각해 보면 이름을 참 잘 지은 것 같다는 생각이 들었다.
특히 ‘변화’라는 단어에서부터 도출되는 SRP(단일 책임의 원칙), OCP(open-closed 원칙)의 객체지향의 설계 원칙이 조금씩 이해되기 시작했다.

이전 데코레이터 패턴에서도 살펴 봤듯이, 데코레이터를 사용하여 확장에 유연하게 대처할 수 있도록 설계를 고쳐보았다.
하지만, 결과적으론 ‘사용자가 원하는 객체를 생성하려면 어떤 래핑 클래스를 사용해야 하는가?’ 에 대한 문제가 남아 있었다.
이를 해결하는 방법 중 하나가 팩토리 패턴이다.

이전 데코레이터의 예시에서 다크로스트 커피를 주문하는 경우, 팩토리 패턴과 함께 사용했다면,
팩토리에선 래핑클래스를 사용하여 반환해주는 부분 까지 책임이 분리 될 것이다.

각 디자인 패턴들이 독립적으로 사용되는 경우도 좋은 설계라고 할 수 있지만,
여러 디자인 패턴들을 적재적소에 적용하는 것 만큼 시너지가 날 수는 없다는 것을 알게 되었다.

IoC vs Factory

문득 객체 생성의 관점에서 IoC 의 개념과 factory 패턴의 효과가 헷갈리기 시작했다.
정리해보면 아래와 같다.

공통점

  • 객체를 생성하는 일을 ‘무언가’가 해준다.

차이점

  • 관점이 다르다.
    • IoC : 객체 생성은 신경쓰지마. 전적으로 내가 한다.
    • 팩토리 : 내가 만들 수 있는게 여러개 있거든?? 근데 선택좀 해줘.
      선택만 해주면 생성은 내가 해줄게~
  • 즉, IoC는 말 그대로 ‘의존성을 주입’ 해준다.
  • 팩토리는 어떤 관계로든 팩토리 객체를 의존할 수 밖에 없다.

이런 차이는 클래스 다이어그램을 그려보면 더 명확하게 알 수 있다.
IoC는 클래스 다이어그램에서 표현할 수 없다.(IoC 컨테이너는 클래스 다이어그램에서 의존성을 표시하는 화살표를 그려주는 역할)
반면, 구성 객체는 어떻게든 팩토리 객체를 의존하고 있기 때문에 팩토리는 클래스 다이어그램에서 표현할 수 있다.

참고자료