김코딩

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

디자인 패턴

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

김코딩딩 2026. 1. 13. 16:26

디자인 패턴 시리즈의 두 번째 주제는 팩토리 메서드 패턴(Factory Method Pattern)이다.

 

싱글톤 패턴"인스턴스를 하나만 만들자"였다면, 팩토리 메서드 패턴"객체 생성을 어떻게 하면 유연하게 할까?"에 대한 해답이다.

 

프로그래밍을 하다 보면 수많은 객체를 생성하게 된다. 그런데 new 키워드를 직접 사용해서 객체를 만들면, 코드가 특정 클래스에 강하게 결합되어 나중에 변경하기 어려워진다. 팩토리 메서드 패턴은 이런 문제를 해결한다.

 

이번 글에서는 팩토리 메서드 패턴무엇인지, 왜 필요한지, 어떻게 구현하는지 알아보자.


팩토리 메서드 패턴의 정의

팩토리 메서드 패턴(Factory Method Pattern)객체 생성 로직을 서브클래스로 위임하여, 클라이언트 코드가 구체적인 클래스에 의존하지 않고 객체를 생성할 수 있도록 하는 생성 패턴이다.

핵심 특징:

  1. 객체 생성 코드를 별도의 메서드로 분리한다
  2. 어떤 클래스의 인스턴스를 만들지는 서브클래스가 결정한다
  3. 클라이언트는 인터페이스만 알면 되고, 구체적인 클래스는 몰라도 된다

왜 팩토리 메서드가 필요할까?

결제 시스템을 개발한다고 가정해보자. 처음에는 신용카드 결제만 지원했다.

public class PaymentService {
    public void processPayment(String type, int amount) {
        if (type.equals("CARD")) {
            CreditCardPayment payment = new CreditCardPayment();
            payment.pay(amount);
        }
    }
}

문제없이 잘 작동한다. 그런데 요구사항이 추가된다.

"카카오페이, 네이버페이, 토스페이도 지원해주세요!"

코드는 이렇게 변한다.

public class PaymentService {
    public void processPayment(String type, int amount) {
        if (type.equals("CARD")) {
            CreditCardPayment payment = new CreditCardPayment();
            payment.pay(amount);
        } else if (type.equals("KAKAO")) {
            KakaoPayment payment = new KakaoPayment();
            payment.pay(amount);
        } else if (type.equals("NAVER")) {
            NaverPayment payment = new NaverPayment();
            payment.pay(amount);
        } else if (type.equals("TOSS")) {
            TossPayment payment = new TossPayment();
            payment.pay(amount);
        }
    }
}

문제점:

  1. 새로운 결제 수단이 추가될 때마다 PaymentService를 수정해야 함 (OCP 위반)
  2. 모든 결제 클래스를 알아야 함 (높은 결합도)
  3. if-else가 계속 늘어남 (유지보수 악몽)
  4. 테스트하기 어려움

이런 상황에서 팩토리 메서드 패턴이 빛을 발한다.


순수 Java로 구현하기

1단계: 공통 인터페이스 정의

먼저 모든 결제 수단이 구현할 인터페이스를 만든다.

public interface Payment {
    void pay(int amount);
}

2단계: 구체적인 결제 클래스 구현

public class CreditCardPayment implements Payment {
    @Override
    public void pay(int amount) {
        System.out.println("신용카드로 " + amount + "원 결제");
    }
}

public class KakaoPayment implements Payment {
    @Override
    public void pay(int amount) {
        System.out.println("카카오페이로 " + amount + "원 결제");
    }
}

public class NaverPayment implements Payment {
    @Override
    public void pay(int amount) {
        System.out.println("네이버페이로 " + amount + "원 결제");
    }
}

public class TossPayment implements Payment {
    @Override
    public void pay(int amount) {
        System.out.println("토스페이로 " + amount + "원 결제");
    }
}

3단계: 팩토리 클래스 생성

객체 생성의 책임을 팩토리가 가진다.

public class PaymentFactory {
    public static Payment createPayment(String type) {
        switch (type) {
            case "CARD":
                return new CreditCardPayment();
            case "KAKAO":
                return new KakaoPayment();
            case "NAVER":
                return new NaverPayment();
            case "TOSS":
                return new TossPayment();
            default:
                throw new IllegalArgumentException("지원하지 않는 결제 수단: " + type);
        }
    }
}

4단계: 클라이언트 코드 개선

public class PaymentService {
    public void processPayment(String type, int amount) {
        Payment payment = PaymentFactory.createPayment(type);
        payment.pay(amount);
    }
}

// 사용 예시
public class Main {
    public static void main(String[] args) {
        PaymentService service = new PaymentService();
        
        service.processPayment("CARD", 10000);
        service.processPayment("KAKAO", 20000);
        service.processPayment("TOSS", 15000);
    }
}

개선된 점:

  • PaymentService는 더 이상 구체적인 결제 클래스를 알 필요가 없다
  • 새로운 결제 수단 추가 시 팩토리만 수정하면 된다
  • 코드가 훨씬 깔끔하고 읽기 쉽다

더 나아가기: 추상 팩토리 메서드

위 예시는 Simple Factory 또는 Static Factory라고 부른다. 진짜 팩토리 메서드 패턴은 조금 더 진화된 형태다.

// 추상 팩토리 클래스
public abstract class PaymentProcessor {
    // 팩토리 메서드 (서브클래스가 구현)
    protected abstract Payment createPayment();
    
    // 템플릿 메서드
    public void process(int amount) {
        Payment payment = createPayment();
        
        // 공통 로직
        System.out.println("결제 시작...");
        payment.pay(amount);
        System.out.println("결제 완료!");
    }
}

// 구체적인 팩토리들
public class CardPaymentProcessor extends PaymentProcessor {
    @Override
    protected Payment createPayment() {
        return new CreditCardPayment();
    }
}

public class KakaoPaymentProcessor extends PaymentProcessor {
    @Override
    protected Payment createPayment() {
        return new KakaoPayment();
    }
}

// 사용
public class Main {
    public static void main(String[] args) {
        PaymentProcessor processor = new KakaoPaymentProcessor();
        processor.process(10000);
    }
}

이 방식의 장점은 확장은 쉽고(Open), 수정은 어렵게(Closed) 만든다는 것이다 (OCP 원칙).


실전 예제: 로거 팩토리

또 다른 실용적인 예시를 보자.

// 로거 인터페이스
public interface Logger {
    void log(String message);
}

// 구체적인 로거들
public class FileLogger implements Logger {
    @Override
    public void log(String message) {
        System.out.println("[FILE] " + message);
        // 실제로는 파일에 기록
    }
}

public class DatabaseLogger implements Logger {
    @Override
    public void log(String message) {
        System.out.println("[DB] " + message);
        // 실제로는 DB에 기록
    }
}

public class ConsoleLogger implements Logger {
    @Override
    public void log(String message) {
        System.out.println("[CONSOLE] " + message);
    }
}

// 로거 팩토리
public class LoggerFactory {
    public static Logger createLogger(String env) {
        switch (env.toUpperCase()) {
            case "DEV":
                return new ConsoleLogger();
            case "PROD":
                return new FileLogger();
            case "TEST":
                return new DatabaseLogger();
            default:
                return new ConsoleLogger();
        }
    }
}

// 사용
public class Application {
    private static final Logger logger = LoggerFactory.createLogger("PROD");
    
    public void doSomething() {
        logger.log("작업 시작");
        // ... 비즈니스 로직
        logger.log("작업 완료");
    }
}

환경에 따라 다른 로거를 사용하지만, 코드 변경 없이 설정만 바꾸면 된다.


팩토리 메서드 패턴의 장점

  1. 느슨한 결합: 클라이언트가 구체 클래스에 의존하지 않음
  2. 단일 책임 원칙: 객체 생성 코드를 한 곳에 모음
  3. 개방-폐쇄 원칙: 새로운 타입 추가 시 기존 코드 수정 최소화
  4. 코드 재사용성: 객체 생성 로직을 여러 곳에서 재사용 가능
  5. 테스트 용이성: Mock 객체로 쉽게 대체 가능

팩토리 메서드 패턴의 단점

  1. 코드 복잡도 증가: 단순한 경우에는 오히려 과한 설계
  2. 클래스 수 증가: 타입마다 새로운 클래스 필요
  3. 간접 참조: 객체 생성이 한 단계 더 거침

Spring에서의 팩토리 메서드

Spring도 팩토리 패턴을 광범위하게 사용한다.

@Configuration
public class PaymentConfig {
    
    @Bean
    public Payment kakaoPayment() {
        return new KakaoPayment();
    }
    
    @Bean
    public Payment cardPayment() {
        return new CreditCardPayment();
    }
}

// 사용
@Service
public class PaymentService {
    private final Payment payment;
    
    public PaymentService(@Qualifier("kakaoPayment") Payment payment) {
        this.payment = payment;
    }
}

Spring의 BeanFactory와 ApplicationContext가 대표적인 팩토리!

실제로 Spring Framework 자체가 거대한 팩토리 컨테이너라고 볼 수 있다.


실무에서 언제 사용할까?

사용하면 좋은 경우:

  • 어떤 객체를 생성할지 런타임에 결정해야 할 때
  • 객체 생성 로직이 복잡할 때
  • 비슷한 객체들을 생성하는 로직이 여러 곳에 흩어져 있을 때
  • 확장 가능한 라이브러리나 프레임워크를 만들 때

사용하지 않아도 되는 경우:

  • 생성할 객체 타입이 고정되어 있고 변경 가능성이 없을 때
  • 객체 생성이 매우 단순할 때
  • 오버엔지니어링이 될 위험이 있을 때

핵심 원칙:

"객체 생성이 복잡하거나 확장 가능해야 한다면 팩토리를 고려하라"


팩토리 메서드 vs Simple Factory vs 추상 팩토리

Simple Factory (정적 팩토리):

Payment payment = PaymentFactory.createPayment("KAKAO");
  • 가장 단순
  • 하나의 팩토리 클래스로 모든 객체 생성

Factory Method (팩토리 메서드):

PaymentProcessor processor = new KakaoPaymentProcessor();
Payment payment = processor.createPayment();
  • 서브클래스가 생성할 객체 결정
  • 더 유연하고 확장 가능

Abstract Factory (추상 팩토리):

  • 연관된 객체들의 군을 생성
  • 더 복잡하지만 강력함 (다음 글에서 다룰 예정)

결론

팩토리 메서드 패턴은 객체 생성의 책임을 분리하여 코드를 더 유연하고 확장 가능하게 만든다.

기억해야 할 포인트:

  • new 키워드를 직접 사용하는 것보다 팩토리를 통한 생성이 유연하다
  • 인터페이스에 의존하고 구체 클래스에 의존하지 않는다
  • 새로운 타입 추가가 쉽다 (OCP 원칙)
  • Spring을 사용한다면 이미 강력한 팩토리 시스템을 쓰고 있다
  • 과도한 사용은 오히려 복잡도를 높인다

다음 글에서는 여러 연관된 객체들을 함께 생성하는 추상 팩토리 패턴을 알아볼 것이다.

다음 글 예고: 추상 팩토리 패턴 - 관련 있는 객체들을 한 번에 생성하라