객체의 생성 부분을 캡슐화해서, 객체의 생성 로직이 복잡 할 시 도움이 되는 디자인 패턴.
한 추상에서 다양한 객체의 생성이 필요 할 때, 상위 추상의 타입으로 하위 객체들을 관리하기 위함이다.
말은 좀 어렵지만, 구현체의 인스턴스 생성을 제한함으로써 얻는 이익을 극대화 할 수 있다.
즉, 결합도를 낮출 수 있다. -> 확장에 용이하다 가장 중요한 컨셉이다.
상위 추상의 타입으로 인스턴스를 선언하고, 마치 구현체로 선언한 것 처럼 사용 할 수 있게 해주는 녀석.
스프링은 빈팩토리에서 빈의 생성과 소멸을 관리하는데, 이 빈팩토리가 팩토리 패턴으로 구현되어 있다.
// 제품(Product) 인터페이스
public interface Product {
void use();
}
// 제품(Product) 구현 클래스: 제품 A
public class ProductA implements Product {
@Override
public void use() {
System.out.println("Product A를 사용합니다.");
}
}
// 제품(Product) 구현 클래스: 제품 B
public class ProductB implements Product {
@Override
public void use() {
System.out.println("Product B를 사용합니다.");
}
}
// 팩토리(Factory) 클래스
public class ProductFactory {
// 제품 생성 메서드
public Product createProduct(String type) {
if (type.equals("A")) {
return new ProductA();
} else if (type.equals("B")) {
return new ProductB();
} else {
throw new IllegalArgumentException("유효하지 않은 제품 타입입니다.");
}
}
}
// 클라이언트(Client) 클래스
public class Client {
public static void main(String[] args) {
ProductFactory factory = new ProductFactory();
// 제품 A 생성
Product productA = factory.createProduct("A");
productA.use(); // Output: Product A를 사용합니다.
// 제품 B 생성
Product productB = factory.createProduct("B");
productB.use(); // Output: Product B를 사용합니다.
}
}
자바 코드는 간단하다.
두 객체의 상위 추상을 선언하고, 추상 매서드를 선언한다. 이 추상을 구현받는 구현체는 추상의 기능을 수행함을 보장받는다.
"팩토리" 클래스에서는 인스턴스의 생성 로직을 구현한다. 들어온 타입에 따라 어떤 인스턴스를 사용 할 지 정해준다.
Client 클래스에서는 "추상의 타입" 으로 인스턴스를 생성한다.
위와 같이 if문을 사용 해 구현 할 수 있다. 그러나, 스프링에서는 좀 더 예쁜 방식으로 가능 할 것 같다.
스프링에서는 어떻게 작동할까?
서비스의 추상 인터페이스가 하나 존재하고, 이 구현체가 많으며 더 추가될 가능성마저 있다고 하자. 구현체의 기능은 대동소이하다
( 예를 들어, Oauth2 소셜 로그인의 경우 카카오, 네이버 등 비슷하다)
예제 코드는 자바와 맞추기 위해서 Product로 했다.
// 공통 추상
public interface ProductService {
void use();
ProductProvider getProvider();
}
// 추상의 구현체 서비스 1
@Service
public class ProductServiceA implements ProductService {
@Override
public void use() {
System.out.println("use A");
}
@Override
public ProductProvider getProvider() {
return ProductProvider.ProductA;
}
}
// 구현체 서비스 2
@Service
public class ProductServiceB implements ProductService {
@Override
public void use() {
System.out.println("use B");
}
@Override
public ProductProvider getProvider() {
return ProductProvider.ProductB;
}
}
// 위 구현체들의 타입 명시를 위한 열거형 클래스
public enum ProductProvider {
ProductA, ProductB
}
// 구현체들을 HashMap에 담고, 적절히 배분해주는 로직을 가지는 팩토리 클래스
@Component
public class ProductProviderFactory {
private final List<ProductService> products;
private HashMap<ProductProvider, ProductService> map = new HashMap<>();
public ProductProviderFactory(List<ProductService> products) {
this.products = products;
for (ProductService product : products) {
this.map.put(product.getProvider(), product);
}
}
public ProductService getService(ProductProvider provider){
return this.map.get(provider);
}
}
// 팩토리에서 서비스를 호출해서 use() 매서드를 사용하는 부분
@Component
public class Client {
private final ProductProviderFactory productProviderFactory;
public Client(ProductProviderFactory productProviderFactory) {
this.productProviderFactory = productProviderFactory;
}
public void doService(ProductProvider provider){
ProductService service = productProviderFactory.getService(provider);
service.use();
}
}
위와 같이 하나의 추상이 여러 개의 구현체를 가질 때, Factory 클래스를 의존 주입 하는 것으로 여러 개의 구현체 중 필요한 것을 선택해서 배분 할 수 있다.
따라서 Product C, D...확장이 계속 발생하더라도 새롭게 추가되는 코드는 있으나 변경은 일어나지 않는다.