팩토리 패턴

곰겜·2025년 9월 10일

Design Pattern

목록 보기
2/2
post-thumbnail

목차

  1. 팩토리 패턴이란?
  2. 자바에서 팩토리 패턴 사용 예시
  3. 팩토리 패턴의 장단점

팩토리 패턴이란?

객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴이자 상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고, 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴.

비유 : 라떼 레시피와 아메리카노 레시피, 우유 레시피라는 구체적인 내용이 들어 있는 하위 클래스가 컨베이어 벨트를 통해 전달되고, 상위 클래스인 바리스타 공장에서 이 레시피들을 토대로 우유 등을 생산하는 생산 공정을 생각하면 된다.

자바에서 팩토리 패턴

/**
 * 간단한 '팩토리 패턴(Factory Pattern)' 예제.
 *
 * - Coffee: 공통 인터페이스(추상 클래스) 역할. 각 커피 유형은 getPrice()를 구현해야 함.
 * - Latte, Americano: 실제 제품 클래스. 생성자로 가격을 주입받아 불변(final)로 보관.
 * - DefaultCoffee: 유효하지 않은 타입이 들어왔을 때의 기본 객체. 가격은 -1(센티널 값)로 고정.
 * - CoffeeFactory: 정적 팩토리 메서드(getCoffee)로 문자열 타입 + 가격을 받아 적절한 Coffee 인스턴스를 생성.
 * - toString(): 다형성 기반으로 getPrice()를 호출해 사람이 읽기 좋은 문자열을 반환.
 */
abstract class Coffee {
    /**
     * 각 커피의 가격을 반환하는 추상 메서드.
     * 하위 클래스에서 반드시 구현해야 한다.
     */
    public abstract int getPrice();

    @Override
    public String toString() {
        // 다형성: 실제 인스턴스의 getPrice()가 호출된다.
        return "This Coffee is " + this.getPrice();
    }
}

/**
 * 커피 객체를 생성해 주는 '정적 팩토리 클래스'.
 * - 입력 문자열(type)을 대소문자 구분 없이 비교하여 해당 타입의 커피 인스턴스를 생성한다.
 * - 매칭되는 타입이 없으면 DefaultCoffee(-1)를 반환한다.
 */
class CoffeeFactory {
    public static Coffee getCoffee(String type, int price) {
        // 문자열 비교 시 NPE 방지를 위해 상수 문자열 리터럴에서 equalsIgnoreCase 호출
        if ("Latte".equalsIgnoreCase(type)) return new Latte(price);
        else if ("Americano".equalsIgnoreCase(type)) return new Americano(price);
        else {
            // 알 수 없는 타입: 기본 커피 반환(가격 -1)
            // 주의: 전달된 price 파라미터는 무시된다.
            return new DefaultCoffee();
        }
    }
}

/**
 * 유효하지 않은 타입에 대한 기본 커피 클래스.
 * - 가격을 -1로 고정하여 '유효하지 않음'을 나타내는 센티널 값으로 활용.
 * - 불변성을 위해 price는 final.
 */
class DefaultCoffee extends Coffee {
    private final int price;

    public DefaultCoffee() {
        this.price = -1; // 센티널 값
    }

    @Override
    public int getPrice() {
        return this.price;
    }
}

/**
 * 라떼 커피 클래스.
 * - 생성 시 주어진 price를 그대로 보관한다.
 * - 불변성을 위해 price는 final.
 */
class Latte extends Coffee {
    private final int price;

    public Latte(int price) {
        this.price = price;
    }

    @Override
    public int getPrice() {
        return this.price;
    }
}

/**
 * 아메리카노 커피 클래스.
 * - 생성 시 주어진 price를 그대로 보관한다.
 * - 불변성을 위해 price는 final.
 */
class Americano extends Coffee {
    private final int price;

    public Americano(int price) {
        this.price = price;
    }

    @Override
    public int getPrice() {
        return this.price;
    }
}

/**
 * 실행 진입점.
 * - 팩토리로부터 각 커피 인스턴스를 생성하고,
 *   toString() 결과(가격 포함)를 출력한다.
 */
public class Main {
    public static void main(String[] args) {
        // "Latte" 타입 → Latte 인스턴스(가격 4000)
        Coffee latte = CoffeeFactory.getCoffee("Latte", 4000);

        // "Americano" 타입 → Americano 인스턴스(가격 3000)
        Coffee ame   = CoffeeFactory.getCoffee("Americano", 3000);

        // 미지정 타입 "Default" → DefaultCoffee 인스턴스(가격 -1)
        // 주의: 여기서 전달한 price=2000 은 무시된다.
        Coffee de    = CoffeeFactory.getCoffee("Default", 2000);

        // 각 인스턴스의 toString()은 다형성으로 getPrice()를 호출하여 가격을 포함한 문자열을 출력한다.
        System.out.println("Factory latte :: " + latte);
        System.out.println("Factory ame   :: " + ame);
        System.out.println("Factory de    :: " + de);
    }
}

/*
예상 출력:
Factory latte :: This Coffee is 4000
Factory ame   :: This Coffee is 3000
Factory de    :: This Coffee is -1
*/	

팩토리 패턴의 장단점

장점

  • 결합도 감소: 상위 클래스와 하위 클래스가 분리되기 때문에 느슨한 결합을 가지며 상위 클래스에서는 인스턴스 생성 방식에 대해 전혀 알 필요가 없기 때문에 유연성을 갖게 됩니다.

  • 확장에 유리: 객체 생성 로직이 따로 떼어져 있기 떄문에 코드를 리팩터링 하더라도 한 곳만 고칠 수 있게 되니 유지 보수성이 증가되고, 새제품(구현체)을 추가해도 클라이언트 코드는 거의/전혀 수정하지 않습니다.

단점

  • 복잡도/클래스 수 증가: 단순 생성에도 레이어가 늘어나 구조가 과도해질 수 있습니다.

  • 오버엔지니어링 위험: 생성이 단순한 경우에는 팩토리로 인한 이득보다 비용이 큽니다.

profile
Python->Java,Kotlin->Rust->Java,Kotlin

0개의 댓글