
이 내용은 지난 장의 내용을 이어서 정리한 내용이다.
팩토리 패턴(Factory Pattern)은 객체를 사용하는 코드와 객체를 생성하는 코드를 분리하는 디자인 패턴이다.
즉, 객체를 직접 생성하지 않고 별도의 팩토리를 통해 객체를 생성하도록 구성한다.
특히 상속 관계를 이용하는 팩토리 메서드 패턴에서는
하는 방식으로 구성된다.
객체를 사용하는 코드
↓
팩토리
↓
어떤 객체를 만들지 결정
↓
구체적인 객체 생성
상위 클래스와 구체적인 객체 생성 로직이 분리되므로 서로 간의 의존성이 줄어들고 느슨한 결합(Loose Coupling)을 만들 수 있다.
상위 클래스는 객체 생성 과정의 전체적인 뼈대를 담당한다.
예를 들어 커피를 만드는 시스템이라면 상위 클래스는 커피를 주문받고 생성하는 전체적인 과정을 담당한다.
하위 클래스는 실제로 어떤 객체를 생성할지 결정하고 구체적인 생성 로직을 구현한다.
예를 들어 다음과 같이 나눌 수 있다.
LatteFactory
↓
Latte 생성
EspressoFactory
↓
Espresso 생성
즉,
을 담당한다.
커피 공장에 비유하면 다음과 같다.
커피 주문
↓
CoffeeFactory
↓
어떤 커피인가?
↓
┌───────────────┐
│ │
LatteFactory EspressoFactory
│ │
Latte 생성 Espresso 생성
CoffeeFactory는 전체 생산 공정을 관리하고, LatteFactory, EspressoFactory는 각각 어떤 커피를 실제로 만들 것인지 결정한다.
자바스크립트의 Object 생성자는 전달받은 값에 따라 서로 다른 Wrapper 객체를 생성할 수 있다.
const num = new Object(42)
const str = new Object('abc')
console.log(num.constructor.name) // Number
console.log(str.constructor.name) // String
숫자를 전달하면 Number 객체가 생성되고, 문자열을 전달하면 String 객체가 생성된다.
즉, 입력값에 따라 생성되는 객체의 타입이 달라질 수 있다는 점에서 팩토리와 비슷한 객체 생성 방식을 확인할 수 있다.
다만 이것을 일반적인 의미의 팩토리 메서드 패턴 자체라고 보는 것은 정확하지 않고, 객체 생성 로직을 내부에서 결정하는 예시 정도로 이해하는 것이 좋다.
class CoffeeFactory {
static createCoffee(type) {
const factory = factoryList[type]
return factory.createCoffee()
}
}
class Latte {
constructor() {
this.name = "latte"
}
}
class Espresso {
constructor() {
this.name = "espresso"
}
}
class LatteFactory extends CoffeeFactory {
static createCoffee() {
return new Latte()
}
}
class EspressoFactory extends CoffeeFactory {
static createCoffee() {
return new Espresso()
}
}
const factoryList = {
LatteFactory,
EspressoFactory
}
const main = () => {
const coffee = CoffeeFactory.createCoffee("LatteFactory")
console.log(coffee.name) // latte
}
main()
전체 흐름은 다음과 같다.
CoffeeFactory.createCoffee("LatteFactory")
↓
factoryList에서 LatteFactory 선택
↓
LatteFactory.createCoffee()
↓
new Latte()
↓
Latte 객체 반환
CoffeeFactory는 어떤 팩토리를 사용할지 결정하는 역할을 하고, LatteFactory와 EspressoFactory는 실제 객체 생성 방법을 담당한다.
따라서 객체를 사용하는 쪽에서는 직접 다음과 같이 작성할 필요가 없다.
const coffee = new Latte()
대신 팩토리를 통해 객체를 생성한다.
const coffee = CoffeeFactory.createCoffee("LatteFactory")
이를 통해 객체 생성 로직과 객체를 사용하는 로직을 분리할 수 있다.
CoffeeFactory의 createCoffee()는 static으로 선언되어 있다.
static createCoffee(type) {
...
}
정적 메서드는 클래스의 인스턴스를 생성하지 않고도 클래스 자체에서 직접 호출할 수 있다.
CoffeeFactory.createCoffee("LatteFactory")
따라서 다음과 같이 객체를 먼저 생성할 필요가 없다.
const factory = new CoffeeFactory()
factory.createCoffee(...)
즉, 이 예제에서 static을 사용하는 핵심 이유는 팩토리 객체를 별도로 생성하지 않고 객체 생성 기능을 바로 사용할 수 있도록 하기 위함이다.
static메서드의 핵심은 클래스의 인스턴스가 아니라 클래스 자체에 속한 메서드라는 점이다.
팩토리 패턴과 의존성 주입(Dependency Injection)은 서로 관련될 수 있지만 동일한 개념은 아니다.
팩토리 패턴은
객체를 어떻게 생성할 것인가
에 초점을 둔다.
반면 의존성 주입은
객체가 필요로 하는 다른 객체를 외부에서 어떻게 전달할 것인가
에 초점을 둔다.
따라서 위 예제는 기본적으로 팩토리를 이용한 객체 생성 패턴으로 보는 것이 적절하며, 단순히 factoryList를 사용했다는 이유만으로 의존성 주입이라고 보기는 어렵다.
enum CoffeeType {
LATTE,
ESPRESSO
}
abstract class Coffee {
protected String name;
public String getName() {
return name;
}
}
class Latte extends Coffee {
public Latte() {
name = "Latte";
}
}
class Espresso extends Coffee {
public Espresso() {
name = "Espresso";
}
}
class CoffeeFactory {
public static Coffee createCoffee(CoffeeType type) {
switch (type) {
case LATTE:
return new Latte();
case ESPRESSO:
return new Espresso();
default:
throw new IllegalArgumentException("Invalid coffee type: " + type);
}
}
}
public class Main {
public static void main(String[] args) {
Coffee coffee = CoffeeFactory.createCoffee(CoffeeType.LATTE);
System.out.println(coffee.getName()); // Latte
}
}
Coffee → 공통 상위 클래스Latte, Espresso → 실제 생성되는 객체CoffeeFactory → 전달받은 타입에 따라 적절한 객체 생성Main → 객체를 직접 new하지 않고 팩토리를 통해 생성CoffeeType.LATTE
↓
CoffeeFactory
↓
new Latte()
↓
Latte 객체 반환
객체 생성 로직을
CoffeeFactory에 분리하여 객체를 사용하는 코드와 생성하는 코드의 결합도를 낮춘다.
※ 이 코드는 엄밀히 말하면 Simple Factory 형태에 가깝다.
Enum
- enum은 서로 관련된 상수들을 하나의 타입으로 묶어 관리하는 자료형이다.
- 정해진 값 중 하나만 사용할 수 있어 잘못된 값이 들어가는 것을 방지할 수 있다.
ex) LATTE, ESPRESSO처럼 선택 가능한 종류가 제한된 경우에 사용한다.