팩토리 메서드 패턴(Factory Method Pattern)은 생성되는 객체의 종류가 여러가고 잦은 변화가 일어나지만, 일련의 기능(행동)은 고정되어 있는 경우 사용된다.
예를 들어. 카드사마다 할인 정책이 모두 다르지만 할인이라는 기능은 동일하다. 따라서 할인이라는 기능은 고정되어 있으나 할인 정책은 수시로 변경되고 여러 정책이 존재함으로 이럴 때 팩토리 메서드 패턴을 사용할 수 있다.
팩토리 메서드 패턴은 객체 생성을 캡슐화하여 객체를 생성하는 코드와 사용하는 코드를 분리한다. 객체 생성과 객체 사용의 책임이 분리되어 각각의 책임을 가진 클래스가 나눠지므로 단일책임원칙(SRP)으 지키는데 도움이 된다.
팩토리 메서드 패턴은 객체 생성 시점을 객체가 실제로 필요할 때 생성할 수 있기 때문에 시스템의 효율성과 유연성이 향상된다.
새로운 클래스를 추가하더라도 기존의 코드를 수정하지 않고도 객체를 생성할 수 있어 확장성이 향상된다.
팩토리 메서드 패턴은 구현체에 의존하지 않고 추상화에 의존하게 되어 코드의 결합도가 낮아져 시스템이 유연해진다.
펙토리 메서드 패턴은 다음과 같은 구조로 되어있다.
Creator(Factory)는 두가지의 메서드를 제공한다. 첫 번째는 Product 객체를 생성하는 메서드인 factoryMethod(), 즉 팩토리 메서드용 인터페이스를 제공한다. 두번째 메서드는 anOperation()이다. 이는 팩토리 메서드에 의해서 생성된 Concrete Product 객체로 Product를 구현한 모든 객체가 공통으로 해야하는 일련의 작업을 처리하는 퍼블릭 인터페이스를 제공한다.
실제 Product를 생산하는 factoryMethod를 구현하는 역할을 한다.
Product 추상화 또는 인터페이스 역할을 한다.
Product의 구현체 역할을 한다.
예제코드를 보면서 작동원리에 대해 설명해보자.
public abstract class DiscountPolicyFactory {
public BigDecimal calculateDiscountAmount(BigDecimal price, CardType cardType) {
DiscountPolicy discountPolicy = createDiscountPolicy(cardType);
return discountPolicy.calculateDiscountAmount(price);
}
protected abstract DiscountPolicy createDiscountPolicy(CardType cardType);
}
Factory에는 Product 객체를 생성하는 factoryMethod()와 Product 구현체들이 해야하는 일련의 작업을 처리하는anOperation()이 있다고 했다. 예제 코드를 보면 Product인 DiscountPolicy 객체를 생성하는 createDiscountPolicy가 있고 앞서 말한 일련의 기능이 바로 calculateDiscountAmount()이다.
이 코드가 바로 Factory를 구현한 구현체이다. 어떠한 팩토리 구현체를 선택하는가에 따라서 Product가 결정되기 때문에 사용하는 서브 클래스에 따라 생성되는 Product가 결정된다. 예를 들어 카드 할인 정책이 있을 수 있고, 포인트 할인 정책이 있을 수 있다. 그 때 이 서브 클래스로 적절한 클래스를 선택하여 유연하게 Product를 생성이 가능하다.
public class CardDiscountPolicyFactory extends DiscountPolicyFactory {
@Override
protected DiscountPolicy createDiscountPolicy(CardType cardType) {
return switch (cardType) {
case 국민 -> new KookminCardDiscountPolicy(new BigDecimal(String.valueOf(0.1)), new BigDecimal(String.valueOf(1000)));
case 신한 -> new ShinhanCardDiscountPolicy(new BigDecimal(String.valueOf(0.05)), new BigDecimal(String.valueOf(500)));
case 삼성 -> new SamsungCardDiscountPolicy(new BigDecimal(String.valueOf(0.07)), new BigDecimal(String.valueOf(700)));
};
}
}
예제 코드를 보면 CardDiscountPolicyFactory를 팩토리 서브 클래스로 선택하여서 Card Type에 따라 그에 맞는 DiscountPolicy 구현체 즉, Product의 구현체를 생성하게 된다.
public abstract class DiscountPolicy {
protected BigDecimal discountRate;
protected BigDecimal discountAmount;
public BigDecimal calculateDiscountAmount(BigDecimal price){
if(Optional.ofNullable(discountRate).isPresent()){
return price.subtract(price.multiply(discountRate));
}
return price.subtract(discountAmount);
}
}
Product는 Factory에서 일련의 작업을 할 때 실행할 메서드를 가지고 있는 추상 클래스 또는 인터페이스 역할을 한다고 했다. 코드를 보면 추상 클래스로 DiscountPolicy라는 Product가 있고 할인된 값을 정의하는 calculateDiscountAmount()라는 메서드를 가지고 있다.
public class KookminCardDiscountPolicy extends DiscountPolicy {
public KookminCardDiscountPolicy(BigDecimal discountRate, BigDecimal discountAmount) {
this.discountRate = discountRate;
this.discountAmount = discountAmount;
}
}
Concrete Product는 Product의 구현체로 코드를 보면 DiscountPolicy를 구현한 KookminCardDiscountPolicy로 DiscountPolicy의 메서드를 재정의하여 사용하거나 값을 초기화 시켜 팩토리에서 각각 다른 방식으로 동작하게 할 수 있다.
카드 할인 정책을 생성해주는 팩토리(concrete Factory)를 선택하여 파라미터로 받은 card type을 이용하여 적절한 할인 정책 객체(Concrete Product)를 생성한다.
이를 보면 어떤 종류의 팩토리를 선택하느냐에 따라 생성될 수 있는 Product가 결정 되기 때문에 사용하는 서브 클래스에 따라 생성되는 Product가 결정된다. 예를 들어보면 카드사 할인 정책 팩토리를 선택하면 카드할인정책 구현체들 중 하나의 객체가 생성될 것이고 포인트 할인 정책 팩토리를 선택하면 포인트 별 할인 정책 구현체 들 중 하나의 객체가 생성될 것이다.
클라이언트는 어떤 Product가 생성되는지 모른다. 하지만, Factory Method를 통해서 Product가 공통으로 해야하는 일을 수행할 수 있는 객체가 생성되어 올바르게 동작할 수 있다는 사실만 알면 되기 때문에 Product 내부 구현에 변경이 발생하더라도 Factory와 Client에게는 영향을 미치지 않는다.
팩토리 메서드 패턴은 다양한 경우에 사용할 수 있다. 자바를 쓰다보면 new 키워드를 사용할 수 밖에 없다. 그러나 new 키워드를 사용하다보면 개발자는 추상화에 의존하지 못하고 구체적인 것에 의존하여 코드의 결합도를 강하게 만드는 경우가 있다.
이 때 '변하는 것'과 '변하지 않는 것'을 잘 구분하여야 한다. '변하는 것'이라면 개발자는 생성과 사용을 분리해야하고 '변하지 않는 것'에 의존해야 한다.
이 때 팩토리 메서드 패턴을 적용함으로써 불필요한 의존성을 제거하고 느슨한 결합도를 유지할 수 있을 것이다.
public enum EnumCardDiscountPolicyFactory {
국민 {
@Override
public DiscountPolicy createDiscountPolicy(BigDecimal discountRate, BigDecimal discountAmount) {
return new KookminCardDiscountPolicy(discountRate, discountAmount);
}
},
신한 {
@Override
public DiscountPolicy createDiscountPolicy(BigDecimal discountRate, BigDecimal discountAmount) {
return new ShinhanCardDiscountPolicy(discountRate, discountAmount);
}
},
삼성 {
@Override
public DiscountPolicy createDiscountPolicy(BigDecimal discountRate, BigDecimal discountAmount) {
return new SamsungCardDiscountPolicy(discountRate, discountAmount);
}
};
public abstract DiscountPolicy createDiscountPolicy(BigDecimal discountRate, BigDecimal discountAmount);
}
이렇게 enum으로 팩토리 메서드 패턴을 구현할 수 있다. enum으로 CardDiscountPolicyFactory 즉, concrete Factory를 구현할 수 있다. enum을 이용한다면 concrete Factory의 싱글톤 패턴의 장점을 얻을 수 있고 코드가 보다 간결해진다.
public static void main(String[] args) {
// Factory Method Pattern by abstract class
DiscountPolicyFactory discountPolicyFactory = new CardDiscountPolicyFactory();
BigDecimal 국민_finalFeeByAbstractClass = discountPolicyFactory.calculateDiscountAmount(new BigDecimal(10000), CardType.국민);
System.out.println(국민_finalFeeByAbstractClass.toString()); // 9000.0
// Factory Method Pattern by enum
DiscountPolicy discountPolicy = EnumDiscountPolicyFactory.국민.createDiscountPolicy(new BigDecimal(String.valueOf(0.1)), new BigDecimal(1000));
BigDecimal finalFeeByEnum = discountPolicy.calculateDiscountAmount(new BigDecimal(10000));
System.out.println(finalFeeByEnum.toString()); // 9000.0
}
이렇게 enum을 사용하면 Factory와 Factory 구현체가 완전히 분리되지는 않지만 enum의 장점을 활용할 수 있다는 면에서 장점이 있다. 따라서 적절히 사용한다면 좋을 것 같다.
위의 예제에서는 추상 클래스로 Product를 구현하였다. 이번에는 interface로 Product를 구현하고 concrete Product를 구현해보자.
public interface DiscountPolicyInterface {
BigDecimal calculateDiscountAmount(BigDecimal price);
}
public class KookminCardDiscountPolicyInterface implements DiscountPolicyInterface {
protected BigDecimal discountRate;
protected BigDecimal discountAmount;
@Override
public BigDecimal calculateDiscountAmount(BigDecimal price) {
if(Optional.ofNullable(discountRate).isPresent()){
return price.subtract(price.multiply(discountRate));
}
return price.subtract(discountAmount);
}
public KookminCardDiscountPolicyInterface(BigDecimal discountRate, BigDecimal discountAmount) {
this.discountRate = discountRate;
this.discountAmount = discountAmount;
}
}
public class SamsungCardDiscountPolicyInterface implements DiscountPolicyInterface {
protected BigDecimal defaultDiscountAmount;
public SamsungCardDiscountPolicyInterface(BigDecimal defaultDiscountAmount) {
this.defaultDiscountAmount = defaultDiscountAmount;
}
@Override
public BigDecimal calculateDiscountAmount(BigDecimal price) {
return defaultDiscountAmount;
}
}
이렇게 Product를 interface를 이용하여 정의한다면 구현체에서 보다 더 유연하게 Product의 메서들 활용할 수 있다. 코드를 보면 기존의 Product인 DiscountPolciy에는 할인률과 할인값을 구현체에서 초기화해서 사용하였는데 interface로 정의된 Product를 구현한 예제를 보면 꼭 할인률과 할인값을 초기화할 필요도 없이 보다 유연하고 제약없이 구현체를 정의할 수 있게 된다.
public class CardDiscountPolicyInterfaceFactory extends DiscountPolicyInterfaceFactory {
@Override
protected DiscountPolicyInterface createDiscountPolicy(CardType cardType) {
return switch (cardType) {
case 국민 -> new KookminCardDiscountPolicyInterface(new BigDecimal(String.valueOf(0.1)), new BigDecimal(String.valueOf(1000)));
case 신한 -> new ShinhanCardDiscountPolicyInterface(new BigDecimal(String.valueOf(0.05)), new BigDecimal(String.valueOf(500)), new BigDecimal(String.valueOf(5000)));
case 삼성 -> new SamsungCardDiscountPolicyInterface(new BigDecimal(String.valueOf(700)));
};
}
}
위 코드를 보면 같은 구현체이지만 매개변수로 다양한 형태가 전달되는 것을 볼 수 있다.
이렇게 interface를 사용한다면 훨씬 유연하고 확장성이 향상되기 때문에 추상 클래스보다 interface를 사용하는 것이 더 좋다고 생각된다.
팩토리 메서드 패턴을 공부하면 스프링에서는 어디에 팩토리 메서드 패턴이 사용되는 지 궁금했다. 그래서 좀 알아보니 bean을 관리하는 AppliationContext가 팩토리 메서드 패턴으로 구현되어 있다고 한다.
ApplicationContext는 빈 설정 정보를 가져와 객체를 생성하고 이렇게 생성된 객체는 필요할 때 ApplciationContext에서 가져와 사용된다. 이렇게 빈 객체의 생성 및 생명 주기를 관리하는 역할을 하므로, 빈 팩토리(bean factory)라고도 한다.
참고 코드 : https://github.com/qwe916/Design_Pattern_Study/tree/main/design-pattern/src/factory_method_pattern