Factory Method와 Abstract Factory in Java

1. 개요

Factory Method와 Abstract Factory는 모두 creational pattern에 속하며, 구체적인 클래스를 직접 new로 생성하지 않고 객체를 생성하는 데 도움이 됩니다. 하지만 이 두 패턴은 서로 다른 문제를 해결하며, 처음 배울 때 쉽게 혼동될 수 있습니다.

이 글에서는 PaymentMethod 예제(Factory Method)와 테마에 따른 UI 생성 시스템(Abstract Factory)을 통해 두 패턴의 차이점을 알 수 있습니다. 이를 읽은 후, 언제 간단한 factory만 필요한지, 언제 “공장을 만드는 공장”이 필요한지 알게 될 것입니다.

2. Factory Method란?

Factory Method는 객체를 생성하기 위한 메서드를 정의하되, 서브클래스가 어떤 구체적인 클래스를 생성할지 결정하도록 합니다. 간단히 말해: create() 인터페이스 메서드를 가지고 있으며, 각 서브클래스가 이를 재정의(override)하여 다른 구현을 반환합니다.

2.1. 해결해야 할 문제

여러 유형의 결제 시스템이 있다고 가정해봅시다: CreditCardPayment, MomoPayment, BankTransferPayment. 만약 클라이언트 코드가 직접 생성한다면:

public void processOrder(Order order, String type) {
    PaymentMethod payment;
    if (type.equals("CREDIT_CARD")) {
        payment = new CreditCardPayment();
    } else if (type.equals("MOMO")) {
        payment = new MomoPayment();
    } else {
        payment = new BankTransferPayment();
    }
    payment.pay(order.getAmount());
}

새로운 결제 방식을 추가할 때마다 이 if-else 문을 모든 곳에서 수정해야 합니다. 이것이 바로 Factory Method가 해결하는 코드 냄새(code smell)입니다.

2.2. Factory Method 적용

public interface PaymentMethod {
    void pay(BigDecimal amount);
}

public class CreditCardPayment implements PaymentMethod {
    @Override
    public void pay(BigDecimal amount) {
        System.out.println("신용카드로 " + amount + "를 결제합니다");
    }
}

public class MomoPayment implements PaymentMethod {
    @Override
    public void pay(BigDecimal amount) {
        System.out.println("Momo를 통해 " + amount + "를 결제합니다");
    }
}

설명: PaymentMethod는 공통 인터페이스이며, 각 구현은 자신의 결제 로직만 담당합니다. 이것이 factory가 생성할 “상품(product)“입니다.

다음은 Factory Method를 포함하는 abstract class입니다:

public abstract class PaymentProcessor {

    // Factory Method - 서브클래스가 어떤 인스턴스를 생성할지 결정합니다
    protected abstract PaymentMethod createPaymentMethod();

    public void processOrder(Order order) {
        PaymentMethod payment = createPaymentMethod();
        payment.pay(order.getAmount());
    }
}

public class CreditCardProcessor extends PaymentProcessor {
    @Override
    protected PaymentMethod createPaymentMethod() {
        return new CreditCardPayment();
    }
}

public class MomoProcessor extends PaymentProcessor {
    @Override
    protected PaymentMethod createPaymentMethod() {
        return new MomoPayment();
    }
}
  • createPaymentMethod()는 Factory Method입니다: 이것은 추상 메서드이며, 각 서브클래스가 재정의하여 자신에게 필요한 PaymentMethod 유형을 반환합니다.
  • processOrder()는 공통 “템플릿”으로, 어떤 결제 방식이 생성되는지 알 필요가 없습니다.

클라이언트는 다음과 같이 사용합니다:

PaymentProcessor processor = new MomoProcessor();
processor.processOrder(new Order(new BigDecimal("150000")));
Momo를 통해 150000를 결제합니다

2.3. Factory Method 테스트

class PaymentProcessorTest {

    @Test
    void shouldCreateCorrectPaymentMethod() {
        PaymentProcessor processor = new CreditCardProcessor();
        PaymentMethod method = processor.createPaymentMethod();

        assertThat(method).isInstanceOf(CreditCardPayment.class);
    }
}

주의: createPaymentMethod() (protected 메서드)를 테스트하려면 테스트 클래스를 PaymentProcessor와 같은 패키지에 둔다거나, 필요한 경우 메서드를 public으로 변경하세요.

3. Abstract Factory란?

Abstract Factory는 **관련된 객체들의 집합(family)**을 생성하기 위한 인터페이스를 제공하되, 각 객체의 구체적인 클래스를 명시하지 않습니다. Factory Method와 달리 Abstract Factory는 한 종류의 상품을 만드는 것이 아니라 여러 상품을 만드는데, 그 상품들은 “함께 가야 합니다”.

3.1. 해결해야 할 문제

애플리케이션의 UI 테마를 만든다고 가정합시다: “Light” 테마와 “Dark” 테마가 있습니다. 각 테마는 고유한 Button과 Checkbox를 가져야 하며, 이 둘의 스타일이 일치해야 합니다. LightButton과 DarkCheckbox를 섞을 수 없다는 뜻입니다.

public interface Button {
    void render();
}

public interface Checkbox {
    void render();
}

public class LightButton implements Button {
    @Override
    public void render() {
        System.out.println("흰색 배경에 검은 텍스트 버튼 렌더링");
    }
}

public class DarkButton implements Button {
    @Override
    public void render() {
        System.out.println("검은색 배경에 흰색 텍스트 버튼 렌더링");
    }
}

public class LightCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("밝은 회색 테두리 체크박스 렌더링");
    }
}

public class DarkCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("흰색 테두리 체크박스 렌더링");
    }
}

3.2. Abstract Factory 정의

public interface UiComponentFactory {
    Button createButton();
    Checkbox createCheckbox();
}

public class LightThemeFactory implements UiComponentFactory {
    @Override
    public Button createButton() {
        return new LightButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new LightCheckbox();
    }
}

public class DarkThemeFactory implements UiComponentFactory {
    @Override
    public Button createButton() {
        return new DarkButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new DarkCheckbox();
    }
}

UiComponentFactory는 상품 집합 (Button + Checkbox)을 정의하는 인터페이스입니다. 각 구현(LightThemeFactory, DarkThemeFactory)은 생성되는 상품들이 테마 관점에서 항상 일치하도록 보장합니다.

3.3. 클라이언트에서 Abstract Factory 사용

public class ThemedApplication {

    private final UiComponentFactory factory;

    public ThemedApplication(UiComponentFactory factory) {
        this.factory = factory;
    }

    public void renderUi() {
        Button button = factory.createButton();
        Checkbox checkbox = factory.createCheckbox();
        button.render();
        checkbox.render();
    }
}
ThemedApplication app = new ThemedApplication(new DarkThemeFactory());
app.renderUi();
검은색 배경에 흰색 텍스트 버튼 렌더링
흰색 테두리 체크박스 렌더링

ThemedApplication은 생성되는 구체적인 클래스가 무엇인지 알 필요도, 관심도 없습니다. 오직 UiComponentFactory 인터페이스에만 의존하므로, 주입된 factory를 바꾸는 것으로 테마를 변경할 수 있습니다.

3.4. Abstract Factory 테스트

class ThemedApplicationTest {

    @Test
    void shouldCreateConsistentComponentsForDarkTheme() {
        UiComponentFactory factory = new DarkThemeFactory();

        Button button = factory.createButton();
        Checkbox checkbox = factory.createCheckbox();

        assertThat(button).isInstanceOf(DarkButton.class);
        assertThat(checkbox).isInstanceOf(DarkCheckbox.class);
    }
}

이 테스트는 Abstract Factory의 가장 중요한 특성을 검증합니다: 같은 factory에서 생성된 상품들은 항상 같은 “집합”에 속하므로 Light와 Dark가 섞이지 않는다는 것입니다.

4. Factory Method와 Abstract Factory 비교

기준 Factory Method Abstract Factory
상품 종류 1가지 상품(PaymentMethod) 여러 관련 상품(Button, Checkbox)
메커니즘 서브클래스에서 메서드 재정의 여러 factory 메서드를 하나의 인터페이스로 구성
관계 Inheritance(상속) 보통 composition(주입)을 사용
목표 클라이언트를 하나의 객체 구체 클래스에서 해방 여러 관련 객체 간의 일관성 보장

기억하기 쉬운 방법: Abstract Factory는 보통 여러 Factory Method의 집합으로 구현됩니다. 위 예제에서 createButton()과 createCheckbox()는 UiComponentFactory 내의 두 개의 Factory Method입니다.

4.1. Factory Method를 언제 사용할까

  • 한 종류의 객체만 생성하면 되지만 생성 로직이 복잡하거나 상황에 따라 달라질 때.
  • 클래스 계층을 다루고 있으며, 서브클래스가 사용할 객체 타입을 스스로 결정하게 하고 싶을 때(보통 template method pattern과 함께 사용).

4.2. Abstract Factory를 언제 사용할까

  • 여러 관련 객체를 생성해야 하며, 그들이 서로 호환 가능한 상태를 유지해야 할 때(같은 테마, 같은 데이터베이스 벤더, 같은 플랫폼 등).
  • 새로운 “상품 집합”을 추가할 가능성이 있으며(예: HighContrastThemeFactory 추가), 클라이언트 코드를 수정하고 싶지 않을 때.

주의: 상품이 하나뿐이라면 Abstract Factory를 쓰지 마세요. 불필요한 인터페이스 레이어만 생깁니다. Factory Method로 충분합니다.

5. Spring Boot와의 통합

Spring에서는 순수한 GoF 방식의 Factory Method/Abstract Factory를 직접 구현하는 일이 드뭅니다. Spring 컨테이너 자체가 factory의 역할을 하기 때문입니다. 그러나 이 원리를 이해하면 bean을 더 잘 설계할 수 있습니다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
@Configuration
public class PaymentFactoryConfig {

    @Bean
    public Map<String, PaymentMethod> paymentMethods() {
        return Map.of(
            "CREDIT_CARD", new CreditCardPayment(),
            "MOMO", new MomoPayment()
        );
    }
}
@Service
public class PaymentService {

    private final Map<String, PaymentMethod> paymentMethods;

    public PaymentService(Map<String, PaymentMethod> paymentMethods) {
        this.paymentMethods = paymentMethods;
    }

    public void pay(String type, BigDecimal amount) {
        PaymentMethod method = paymentMethods.get(type);
        if (method == null) {
            throw new IllegalArgumentException("지원하지 않는 결제 방식: " + type);
        }
        method.pay(amount);
    }
}

여기서 Spring 컨테이너는 실제 “공장” 역할을 합니다: 미리 PaymentMethod bean들을 생성해두고 클라이언트는 키로 조회할 뿐입니다. 이는 Spring 환경에서의 Factory Method 실용적인 변형입니다. 각 결제 방식마다 XxxProcessor 클래스를 만들 필요가 없어집니다.

6. 흔한 실수

  • Factory Method와 static factory method 혼동(예: Optional.of(), List.of()): static factory method는 단순히 생성자보다 간결한 방식일 뿐이며, GoF 패턴의 다형성과는 무관합니다.
  • 상품이 하나뿐일 때 Abstract Factory 남용: Button만 있고 Checkbox가 없다면 Abstract Factory는 오버엔지니어링입니다.
  • factory에 너무 많은 비즈니스 로직 넣기: factory는 객체 생성만 담당해야 하며, 주문 처리, 검증 등의 로직을 섞으면 안 됩니다.
  • Abstract Factory에서 일관성 보장 깨뜨리기: 클라이언트가 A factory의 Button과 B factory의 Checkbox를 섞어서 사용하도록 두면 pattern의 목적을 무너뜨립니다.

7. 결론

Factory Method는 서브클래스에게 하나의 객체 생성을 위임할 때 유용하며, Open/Closed 원칙을 따라 코드 확장을 쉽게 만듭니다. Abstract Factory는 한 발 더 나아가서 관련된 객체들의 집합이 항상 일관되게 생성되도록 보장하고, 호환되지 않는 컴포넌트 섞임을 방지합니다.

실제로 Spring Boot를 사용할 때는 GoF 구조를 그대로 따르기보다는 DI 컨테이너를 factory로 활용합니다. 하지만 이 두 pattern의 본질을 정확히 이해하면 라이브러리 코드(예: DocumentBuilderFactory, JDBC의 ConnectionFactory)를 더 잘 읽을 수 있고, 확장 가능한 모듈을 더 잘 설계할 수 있습니다.

design-patternjavacreational-patternspring-bootclean-code