dh_0e

[Design Pattern] Creational Design Patterns II (Factory Method Pattern, Enum Factory Pattern) 본문

Software Analysis & Design/Design Patterns

[Design Pattern] Creational Design Patterns II (Factory Method Pattern, Enum Factory Pattern)

dh_0e 2025. 11. 2. 17:30

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

  • 객체 생성을 서브 클래스에 위임하는 생성(Creational) 패턴
    • 생성 패턴: 객체 생성 메커니즘을 캡슐화하여 객체가 생성되거나 변경되어도 프로그램 구조에 큰 영향을 주지 않도록 유연성을 제공하는 패턴

Creational Pattern 종류

  • 객체를 생성하기 위한 인터페이스는 상위 클래스에서 정의되지만, 실제로 어떤 클래스의 인스턴스를 생성할지는 서브 클래스가 결정
  • 사용 상황 
    • 객체의 생성코드의 나머지 부분으로부터 분리하려고 할 때
    • 사용자가 자체 객체를 만들어 시스템을 확장하는 방법을 제공하고자 할 때
    • 객체 생성에 있어서 기존의 코드를 건들지 않고 쉽게 확장할 수 있는 방법이 필요할 때 (OCP: 개방-폐쇄 원칙)
    • ex) ↓ 배를 만드는 공장(ShipFactory)에서 client의 요청에 맞추어 배를 주문받아(orderShip) 배(Ship)의 객체를 주는 상황

Factory method pattern이 필요한 상황
Factory method pattern이 필요한 상황

  • ShipFactory가 구체 규칙(화이트/블랙 색, 어떤 Ship을 만들지)을 직접 알고 있어 Ship과 강하게 결합
  • 새로운 배 타입(blueship)이 추가 시 if/else문을 뜯어고쳐야 함 >> OCP 위반
  • Ship 클래스에 새로운 멤버 변수가 추가(필드가 늠)되면 ShipFactory 코드도 함께 수정되어 변경 파급이 큼

 

팩토리 메서드 패턴의 구조

  • 팩토리 메서드 패턴은 인터페이스를 통해 팩토리 객체와 제품 객체 간의 결합을 느슨하게 해줌
  • 팩토리 클래스는 구체 팩토리 클래스(서브 클래스)의 상속 대상이지 제품 객체를 생성하지는 않음
    • 이를 상속하는 서브 클래스의 메서드에서 여러 제품 객체 생성을 각각 책임짐

팩토리 메서드 패턴의 Class diagram

  • Creator(추상 팩토리): 제품 객체를 생성하는 추상 메서드 $createProduct()$를 정의 (구체 팩토리의 상속 대상)
  • ConcreteCreator(구체 팩토리): $createProduct()$ 메서드를 구현하여 특정 ConcreteProduct 객체를 반환 (제품 객체 생성)
  • Product(추상 제품): 생성될 객체들의 인터페이스를 정의 (구체 제품의 상속 대상)
  • ConcreteProduct(구체 제품): Product 인터페이스를 구현하는 실제 제품 객체

팩토리 메서드 패턴으로 객체 생성시 Sequence diagram

  • ConcreteProductA에 대한 구체적인 생성은 ConcreteCreatorA(팩토리) 객체가 전담
  • Client는 ConcreteCreatorA를 통해서 Product 객체를 전달받음
    • ConcreteProductA product = new ConcreteCreatorA().createProduct() 

 

ex) ShipFactory에 Factory Method Pattern을 적용하여 코드 수정

수정된 Class Diagram

 

수정된 Code

실행 흐름(Client 기준)

  1. Client의 main에서 new WhiteShipFactory().orderShip("whiteship") 호출
  2. orderShip 템플릿이 실행
    1. $validate("whiteship")$로 인자 검증
    2. $prepareFor("whiteship")$로 준비 메시지 출력
    3. $createShip()$ 호출 >> WhiteShipFactory가 오버라이드한 createShip에서 new WhiteShip() 반환
  3. 반환된 Ship이 클라이언트로 전달됨

 

ex) BlackShip 추가

새로운 BlackShip을 추가하려고 할 때 팩토리 메서드 패턴이 적용된 구조에서는 기존 코드의 수정이 없음
Client에서도 ConcreteCreator인 WhiteShipFactory만 BlackShipFactory로 변경
Client 코드도 추가 수정이 최소화되도록 수정해 볼 수 있음

 

Factory Method Pattern 사용 예

  • XML document parser
    • 여러 가지 파싱 방법을 제공하기 위해 팩토리 메서드 패턴으로 구현됨

  • Java.util.Calendar, java.text.Numberformat, Spring BeanFactory, etc.

 

팩토리 메서드 패턴의 장점

  • 코드 유지보수 용이: 객체 생성을 캡슐화하여 나머지 코드와 분리함으로써 유지보수가 쉬움
  • 재사용성 증가: 객체 생성이 인터페이스를 통해 이루어져 인터페이스 기반의 재사용이 용이 (Client 코드의 수정 최소화)
  • 단일 책임 원칙(SPR) 준수: 팩토리 메서드 패턴은 객체의 생성 및 초기화에 대한 책임만 가짐
  • 개방-폐쇄 원칙(OCP) 준수
    • 기존 코드의 변경 없이 새로운 코드를 유연하게 추가할 수 있음
    • 객체 생성을 위한 인터페이스를 정의하지만, 어떤 클래스를 인스턴스화 할지는 서브 클래스가 결정

 

팩토리 메서드 패턴의 단점

  • 코드 구조 복잡도 증가
    • 각 제품 구현체마다 팩토리 객체를 모두 구현해주어야 하기 때문에 클래스 수가 많아지며, 한눈에 이해하기 어려움
  • 객체 생성 오버헤드
    • 추가적인 클래스 객체가 생성되므로 객체 생성에 따른 오버헤드가 발생하여 성능에 영향을 미칠 수 있음

 

팩토리 메서드 패턴의 개선 버전

  • Enum Factory Pattern
    • 서브 팩토리 클래스를 일일이 구현하지 않고, 하나의 enum 팩토리로 구성하여 클래스 폭발 문제를 완화
  • Dynamic factory pattern(동적 팩토리 패턴)
    • 서브 클래스의 수가 늘어나는 것을 Reflection API를 이용하여 동적으로 처리하여 해결

 

Enum Factory Method Pattern

  • Factory Method Pattern의 문제점을 해결하고자 개선된 Factory Method Pattern
    • 제품의 객체의 수마다 팩토리 서브 클래스를 모두 구현해주어야 함
    • 여러 팩토리 객체가 생성되어 자원이 낭비될 수 있음

도형에 대한 팩토리 메서드 패턴

  • Enum으로 팩토리 메서드 패턴을 구성해 준다면, 일일이 서브 팩토리 클래스 구현 없이 하나의 enum Factory에서 구성할 수 있음
    • 추가적인 팩토리 클래스를 만들지 않으며, 동시에 단일 팩토리(Singleton) 객체로 제품 객체를 생성할 수 있음

Enum을 활용한 해결 아이디어 (Factory class)
Enum을 활용한 해결 아이디어 (Client)

  • 위처럼 추가적인 팩토리 클래스를 만들지 않아야 하면서 동시에 단일 팩토리로 제품 객체를 생성할 수 있어야 함
    • Enum을 이용하여 싱글톤 객체 생성 

Singleton with Enum

  • Enum을 이용하면 멀티톤(multiton)으로도 일반화 가능
    • Multiton: 여러 개의 관리되는 인스턴스를 가지는 클래스

Multiton with Enum VS 수동 Multiton 구현
Operation with Enum vs 수동 Operation 구현

  • Enum factory method pattern도 새 종류를 추가할 때는 enum 본문을 수정해야 해서 OCP를 완전히 만족하지는 못함
  • 그래도 수동 구현(static finanl)한 고정 집합의 멀티톤보다 나은 점 (참고)
    • 인스턴스 유일성 보장: JVM이 상수당 1개 인스턴스를 강하게 보장(실수 여지 적음)
    • 직렬화 안전성: 별도 readResolve 없이도 역직렬화 시 동일 인스턴스 유지
    • 리플랙션 내성: 리플랙션으로 추가 인스턴스 생성이 사실상 불가
    • 스레드 안전 초기화: 클래스 로딩 타이밍에 안전하게 한 번만 생성
    • 간결한 문법/가독성: 상수 선언과 상수별 동작 오버라이드가 짧고 명확
    • 부가 기능: $values()$, $valueOf()$, $name()$ 사용 등 표준 유틸 제공
  • 자체적 의의(vs 기존 Factory Method Pattern)
    • 새로운 제품 추가: Product 클래스를 추가하고, EnumFactory에 팩토리 객체만 추가하면 됨
    • 싱글톤 공유: 하나의 Factory 객체는 싱글톤으로 공유되어 외부에서 객체로 만들어 줄 필요가 없음
      • 객체 생성에 따른 부담 완화
  • 단점
    • 새로운 Factory 추가 시 기존의 Enum 코드가 수정되어야 함
      (코드 구조의 복잡성을 줄이지만 OCP를 못 지키는 trade-off 발생)
    • 만일 Factory가 복잡한 상속 계층으로 구성된다면, Enum은 클래스 상속이 안 되기 때문에 상속 구조 표현에 한계 존재
      • ex) ↓
// 제품 계층
abstract class Shape { }
class Circle extends Shape { /* radius 등 */ }
class Rectangle extends Shape { /* w, h 등 */ }

// 1단계: 공통 템플릿 공장
abstract class BaseShapeFactory {
    public final Shape order(String spec) {
        audit("order: " + spec);
        validate(spec);
        Shape s = createShape(spec);   // 변하는 부분
        postProcess(s);
        return s;
    }
    protected void validate(String spec) { /* 공통 검증 */ }
    protected void postProcess(Shape s) { /* 공통 후처리 */ }
    protected void audit(String msg) { /* 로깅 */ }
    protected abstract Shape createShape(String spec);
}

// 2단계: 보안 검증 추가
abstract class SecureShapeFactory extends BaseShapeFactory {
    @Override protected void validate(String spec) {
        super.validate(spec);
        checkAuth();
    }
    protected void checkAuth() { /* 인증/권한 확인 */ }
}

// 3단계: 캐싱 추가
abstract class CachingShapeFactory extends SecureShapeFactory {
    private final java.util.Map<String, Shape> cache = new java.util.HashMap<>();
    @Override public final Shape order(String spec) {
        if (cache.containsKey(spec)) return cache.get(spec);
        Shape s = super.order(spec);
        cache.put(spec, s);
        return s;
    }
}

// 최종 구체 공장들
class CircleFactory extends CachingShapeFactory {
    @Override protected Shape createShape(String spec) {
        // spec 파싱해서 Circle 생성
        return new Circle();
    }
}
class RectangleFactory extends CachingShapeFactory {
    @Override protected Shape createShape(String spec) {
        // spec 파싱해서 Rectangle 생성
        return new Rectangle();
    }
}
  • enum은 다른 클래스를 상속할 수 없어, 위와 같은 복잡한 공통 절차를 다중 상속하며 단계적으로 재사용한 구조는 enum으로 바꿀 수 없음
    • 클래스 상속을 유지하거나, 인터페이스 default/조합/위임으로 구조를 바꾸는 편이 안전함 (참고)