
목차
- 팩토리 패턴이란?
- 자바에서 팩토리 패턴 사용 예시
- 팩토리 패턴의 장단점
객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴이자 상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고, 하위 클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴.
비유 : 라떼 레시피와 아메리카노 레시피, 우유 레시피라는 구체적인 내용이 들어 있는 하위 클래스가 컨베이어 벨트를 통해 전달되고, 상위 클래스인 바리스타 공장에서 이 레시피들을 토대로 우유 등을 생산하는 생산 공정을 생각하면 된다.
/**
* 간단한 '팩토리 패턴(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
*/
장점
결합도 감소: 상위 클래스와 하위 클래스가 분리되기 때문에 느슨한 결합을 가지며 상위 클래스에서는 인스턴스 생성 방식에 대해 전혀 알 필요가 없기 때문에 유연성을 갖게 됩니다.
확장에 유리: 객체 생성 로직이 따로 떼어져 있기 떄문에 코드를 리팩터링 하더라도 한 곳만 고칠 수 있게 되니 유지 보수성이 증가되고, 새제품(구현체)을 추가해도 클라이언트 코드는 거의/전혀 수정하지 않습니다.
단점
복잡도/클래스 수 증가: 단순 생성에도 레이어가 늘어나 구조가 과도해질 수 있습니다.
오버엔지니어링 위험: 생성이 단순한 경우에는 팩토리로 인한 이득보다 비용이 큽니다.