1️⃣ Factory Pattern, Factory Method Pattern
1) Factory Pattern (팩토리 패턴)
팩토리 패턴은 생성 패턴 중 하나로, 객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴이다. 이는 상속 관계에 있는 두 클래스에서 상위 클래스가 중요한 뼈대를 결정하고, 하위 클래스에서 객체 생성에 대한 구체적인 내용을 결정하는 패턴이다. 즉, 객체 생성을 Factory 클래스로 캡슐화하고, 이를 상속하는 하위 클래스를 통해 여러 방법으로 객체를 생성한다.
팩토리 패턴은 상위 클래스와 하위 클래스가 분리되기 때문에 느슨한 결합을 갖는다. 따라서 상위 클래스에서는 인스턴스 생성 방식에 관한 정보가 필요 없기 때문에 유연한 개발이 가능하다. 객체 생성 로직 또한 분리되어 있기 때문에 리팩터링시 코드의 변경이 적을 수 있다. 따라서 유지보수성 또한 좋아진다. 팩토리 패턴은 일반적으로 아래와 같은 구조를 가진다.
- 추상 팩토리 (Abstract Factory) : 객체 생성을 위한 인터페이스를 제공한다. 이를 통해 객체를 생성하는 방법에 대한 추상화를 제공하여 클라이언트 코드가 구체적인 클래스에 종속되지 않도록 한다.
- 구체적인 팩토리 (Concrete Factory) : 추상 팩토리의 구현 클래스로, 실제로 객체를 생성한다.
- 제품 (Product) : 생성되는 객체를 나타낸다. 팩토리 패턴은 이러한 제품 객체의 생성 방법을 추상화한 것이다.
- 클라이언트 (Client) : 팩토리를 사용하여 객체를 생성하는 코드를 말한다.
2) Factory Method Pattern (팩토리 메서드 패턴)
팩토리 메서드 패턴은 객체 생성을 처리하기 위한 상위 클래스인 추상 클래스에서 객체 생성을 담당하는 메서드를 선언하고, 이를 하위 클래스에서 구현하는 방식이다. 상위 클래스에는 추상 팩토리 메서드가 있으며, 하위 클래스에서는 이를 오버라이딩하여 필요한 구체적인 객체를 생성한다. 팩토리 메서드 패턴은 상위 클래스에서 객체 생성의 일부분을 하위 클래스로 분리하여 확장성을 제공하고, 생성되는 객체의 타입을 하위 클래스에 위임한다. 팩토리 메서드 패턴은 일반적으로 아래와 같은 구조를 가진다.
- 생성자 (Creator) : 객체 생성 메서드를 포함한 추상 클래스 또는 인터페이스이다. 팩토리 메서드를 정의하여 객체 생성을 위임한다.
- 구체적인 생성자 (Concrete Creator) : Creator 클래스의 구현 클래스이다. 팩토리 메서드를 오버라이드하여 실제로 객체를 생성하는 구체적인 로직을 제공한다.
- 제품 (Product) : 생성되는 객체를 나타낸다. Creator 클래스에서 정의된 팩토리 메서드에 따라 생성된다.
- 구체적인 제품 (Concrete Product) : Product 클래스의 구현 클래스이다. 구체적인 제품을 생성하는 구현 코드를 말한다.
3) 팩토리 패턴 vs 팩토리 메서드 패턴
특징 | 팩토리 패턴 | 팩토리 메서드 패턴 |
주요 개념 | 객체 생성을 위한 인터페이스와 구체적인 팩토리 클래스 | 상위 클래스에서 객체 생성을 담당하는 추상 팩토리 메서드 |
역할 분리 | 객체 생성을 중앙화하여 클라이언트 코드와 분리 | 객체 생성을 상위 클래스로부터 분리하여 하위 클래스에 위임 |
구조 | 팩토리 인터페이스와 구체 팩토리 클래스 포함 | 추상 팩토리 메서드를 갖는 상위 클래스와 구체 하위 클래스 |
생성 방법 | 단일 지점에서 생성 방법을 결정 | 서브 클래스마다 다른 생성 방법을 구현 |
클라이언트 코드 의존성 |
클라이언트 코드는 구체 팩토리 클래스에 의존하지 않음 | 클라이언트 코드는 추상 팩토리 메서드에 의존하지 않음 |
확장성 | 객체 생성 로직을 변경하기 쉬움 | 객체 생성 로직을 확장하기 쉬움 |
2️⃣ 팩토리 패턴 예시
abstract class Food {
public abstract int getPrice();
@Override
public String toString() {
return "Price : " + this.getPrice();
}
}
public enum FoodType {
PIZZA("피자"),
BURGER("버거"),
CHICKEN("치킨");
private String name;
FoodType(String name) {
this.name = name;
}
}
public class FoodFactory {
public static Food getFood(FoodType type, int price) {
if (type == FoodType.PIZZA) {
return new Pizza(price);
} else if (type == FoodType.BURGER) {
return new Burger(price);
} else if (type == FoodType.CHICKEN) {
return new Chicken(price);
} else {
return new DefaultFood();
}
}
}
public class DefaultFood extends Food {
private int price;
public DefaultFood() {
this.price = 0;
}
@Override
public int getPrice() {
return this.price;
}
}
public class Pizza extends Food {
private int price;
public Pizza(int price) {
this.price = price;
}
@Override
public int getPrice() {
return price;
}
}
public class Burger extends Food {
private int price;
public Burger(int price) {
this.price = price;
}
@Override
public int getPrice() {
return price;
}
}
public class Chicken extends Food {
private int price;
public Chicken(int price) {
this.price = price;
}
@Override
public int getPrice() {
return price;
}
}
public class Main {
public static void main(String[] args) {
Food pizza = FoodFactory.getFood(FoodType.PIZZA, 16000);
Food chicken = FoodFactory.getChicken(FoodType.CHICKEN, 20000);
System.out.println("Pizza : " + pizza.toString());
System.out.println("Chicken : " + chicken.toString());
}
3️⃣ 장점
1. 유연성과 확장성
객체 생성 로직을 캡슐화함으로써, 클라이언트 코드는 구체적인 객체의 생성 방법을 알 필요가 없다. 이는 객체 생성 방법이 바뀌더라도 클라이언트 코드를 수정하지 않아도 된다. 따라서 새로운 객체를 추가하거나 기존 객체를 변경해도 Factory 클래스만 변경하면 되므로 시스템의 유연성과 확장성이 향상된다.
2. 코드의 중복 제거
객체 생성 로직을 Factory 클래스에 중앙 집중화함으로써 중복 코드를 방지할 수 있다. 여러 곳에서 동일한 객체를 생성하는 로직을 여러 번 작성할 필요가 없기 때문에 코드의 재사용성이 증가한다.
3. 결합도 감소
클라이언트 코드가 구체적인 클래스에 의존하지 않고 Factory 클래스를 통해 객체를 생성하므로 클래스 간 결합도가 낮아진다. 이는 시스템의 유지보수와 테스트 용이성을 향상시킨다.
4️⃣ 단점
1. 복잡성 증가
Factory 클래스를 도입하면 추가적인 클래스가 생기기 때문에 전반적으로 시스템 복잡성이 증가할 수 있다. 실제로 작은 규모의 프로젝트나 단순 객체 생성 로직의 경우 Factory 패턴을 사용하는 것이 오히려 더 복잡해질 수 있다.
2. 오버헤드
Factory 패턴을 도입하면 객체 생성을 위해 추가적인 메서드 호출이 필요하므로, 약간의 성능 오버헤드가 발생할 수 있다. 만약 성능이 중요한 서비스의 경우 충분히 고려하여 도입해야 한다.
3. 잘못된 사용
Factory 패턴을 잘못 사용하면 클래스가 너무 많이 생성되어 클래스 계층 구조가 복잡해질 수 있다. 이는 유지보수를 어렵게 만들 수 있기 때문에 역시 적절한 상황에서만 사용하는 것이 중요하다.