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)를 더 잘 읽을 수 있고, 확장 가능한 모듈을 더 잘 설계할 수 있습니다.