3가지 디자인 패턴을 알아보자 - 전략 / 템플릿 메서드 / 추상 팩토리

이누의 벨로그·2022년 4월 21일
2

아래 글은 코드스피츠 83-6회차 강의를 요약한 글입니다.

템플릿 메소드 패턴

우리가 상속이 좋지 않다고 할 때, 그 이유는 부모 클래스의 수정의 여파가 모든 자식에게 미치기 때문이다. 그런데, 템플릿 메소드 패턴에서는 이러한 부모의 자식 간의 의존성 방향을 부모가 자식을 아는 방향으로 역전시키게 된다. 일반적으로는 자식이 부모의 세부구현을 알고서 이를 이용하는 쪽으로 상속이 일어나지만, 템플릿 메서드 패턴에서는 부모가 반대로 추상 메소드 인터페이스를 통해서 자식을 알게 된다. 부모가 자식이 미래에 구현해야할 오퍼레이션을 확정 지은 상태에서 자식을 이용해서 자신의 로직을 구현하고, 자식은 제한된 자신의 책임만 구현하고 부모의 지식을 몰라도 된다.

abstract class DiscountPolicy{
	private Set<DiscountCondition> conditions = new HashSet<>();
	public void addCondition(DiscountCondition condition){conditions.add(condition)}
	public Money calculateFee(Screeening screening, int count, Money fee){//템플릿 메서드
		for(DiscountCondition conditions:conditions){
			if(condition.isSatisfiedBy(screening, count)) return calculateFee(fee);
		}
		return fee;
	}
	protected abstract Money calculateFee(Money fee); //훅
}

디자인 패턴에서는 이러한 의존성을 간단하게 표현하기 위해 이를 2가지 용어로 정하였다. 미래의 자식이 구현해야 하는 책임을 이라고 부르며, 이를 이용하는 메서드를 템플릿 메서드 라고 부른다.

템플릿 메서드 패턴을 이용해 AmountPolicy를 구현해볼 수 있다.

public class AmountPolicy extends DiscountPolicy{
	private final Money amount;
	public AmountPolicy(Money amount){
		this.amount=amount;
	}
	@Override
	public Money calculateFee(Money fee){
		return fee.minus(amount);
	}
}

AmountPolicy는 abstract로 지정한 자신의 책임만 구현하고 있고, 나머지 모든 구현은 부모의 속성이나 super 등을 사용하고 있지 않다. 부모 클래스에는 public으로 외부에 대외적으로 공개된 메소드를 제외하면 모두 private으로 자신의 접근을 막고 있기 때문에부모의 변경은 자식 클래스에 여파를 끼치지 않는다. 이 때 protected 권한의 추상 메서드를 통해서 자식 클래스가 메서드를 제공해야 되는 책임을 미리 약속함으로써, 미리 약속된 프로토콜을 통해서만 부모와 통신하도록 한정함으로써 의존성을 역전시킨 것이다.

따라서 템플릿 메서드 패턴은 추상 메서드라는 일종의 프로토콜을 이용해서 부모와 자식이라는 두 객체가 대화하게끔 도와주는 패턴이라고 할 수 있으며, 상속구조에서 자식이 부모를 앎으로써 부모의 수정이 자식에 여파를 끼치는 구조를 부모가 추상 메소드 프로토콜을 통해서 자식을 알도록 의존성을 방향을 바꾸게 해준다. 상속이라는 의존성의 문제를 해결해주는 패턴이라고 할 수 있다. 만약 우리가 상속하는 어떤 부모 클래스에서 private이 아닌 protected, 혹은 internal 속성이나, getter/setter를 가지고 있다면 이미 그 단계에서 우리는 부모 클래스를 수정할 수 없게 된다. 따라서 상속을 사용할 때는 여파가 미치지 않도록 의존성을 역전시키거나, 혹은 부모 클래스의 속성을 private으로 노출되지 않게 해야 한다.

다만 상속은, 언어차원에서 어떠한 객체가 다른 객체에 대한 지식을 알게 하는 의존성을 생성하는 것이며, 이러한 의존성의 문제는 비단 상속만의 문제가 아니다. 어떠한 객체를 아는 객체의 수가 많아지면, 해당 객체를 수정하기 힘들어지는 문제가 발생하며, 이를 단일 의존포인트라고 한다. 이 때 이러한 의존성을 해결하기 위해서는, 다른 여러 객체들(’자식들')이 알고 있는 하나의 객체(’부모')가 자식들에 대한 지식을 사용하고, 자식들은 부모에 대한 지식을 사용하지 않도록 해야 한다. 템플릿 메서드의 경우에는 이러한 자식들의 지식을 하나의 추상 메서드 프로토콜로써 인식하게 해준다.

전략 패턴

우리는 똑같은 코드를 전략 패턴으로 바꿀 수 있다.

public class DiscountPolicy{
	private final Set<DiscountCondition> conditions = new HashSet<>();
	private final Calculator calculator;
	public DiscountPolicy(Calculator calculator){this.calculator = calculator;}
	public void addCondition(DiscountCondition condition){conditions.add(condition)}
	public Money calculateFee(Screeening screening, int count, Money fee){
		for(DiscountCondition conditions:conditions){
			if(condition.isSatisfiedBy(screening, count)) return calculator.calculateFee(fee);
		}
		return fee;
	}
}

템플릿 메소드 패턴과 전략 패턴은 마치 재귀를 루프로 바꾸는 것과 같이 서로 변환이 가능하다. 전략패턴은 상속이라는 컴파일러 단계에서 확정된 의존성을 가지지 않기 때문에, 템플릿 메서드 패턴보다 더 유연하게 구상객체를 받아들일 수 있다. 전략패턴이 사용하는 원리는 상속이 아니라 객체로 합성한 코드를 소유하는 것이다. 앞서 상속에서 추상 인터페이스로 처리했던 부분을 구현하고 있는 외부 객체를 소유하여, 외부 객체가 대신 처리해준다.

앞서 템플릿 메서드 패턴에서는 추상 인터페이스 부분을 구현하는 부분 뿐만 아니라 구현에 필요한 상태도 자식 클래스가 구현했다. 그렇다면, 이러한 상태와 추상 인터페이스만 구현한다면 상속이 없이도 어떠한 객체라도 자식 클래스와 동일한 역할을 할 수 있게 된다. 다만 이 때 컴파일러가 의존성을 생성을 해주는 상속을 이용하지 않기 때문에, 프로그래머가 직접 이를 객체로 합성하여 소유 하게 되는 것이다.

public class AmountCalculator implements Calculator{
	private final Money amount;
	public AmountPolicy(Money amount){
		this.amount=amount;
	}
	@Override
	public Money calculateFee(Money fee){
		return fee.minus(amount);
	}
}

따라서 추상 인터페이스를 구현하고 똑같은 상태를 가지는 객체를 위와 같이 구현할 수 있다. 추상 인터페이스 부분을 Calculator라는 인터페이스로 분리한 것을 제외하면 템플릿 메서드 패턴과 완전히 동일하다. 이를 통해 우리가 얻을 수 있는 교훈은, 앞서 우리가 상속했던 DiscountPolicy가 했던 역할은 사실은 calculateFee 메서드를 가지는 Calculator라는 인터페이스의 역할과 완전히 동일하다는 것이다. 템플릿메서드에서는 추상클래스를 마치 인터페이스처럼 하나의 프로토콜로써 사용한다는 것을 알 수 있다. 이것으로부터 우리는, 상속에서 수정여파로부터 자식클래스들을 격리할 수 있으려면 추상 클래스를 마치 하나의 인터페이스(프로토콜)처럼 사용해야 한다는 것을 알 수 있다. 만약 우리가 상속에서 OCP를 준수하여 추상 레이어를 격리했다면, 템플릿 메서드 패턴은 전략패턴으로 100% 변환가능하게 된다.

그렇다면 전략패턴과 비교했을 때 템플릿 메서드 패턴의 장점은 무엇일까? 바로 부모 객체와 구상 객체 단 2개의 객체만 존재하므로 의존성 관계를 단순화여 의존성을 역전했다는 것이다.

반면 전략패턴은 템플릿 메서드 패턴과 문제를 해결하는 방식이 전혀 다르다. 전략 패턴은 추상 레이어와 구상 레이어가 서로를 직접 모르게 하기 위해서 중간에 전략 레이어 라고 부르는 추상 인터페이스를 끼워넣음으로써 단방향 의존성을 이루었다. 따라서 템플릿 메서드 패턴보다는 의존성이 하나 더 생겼지만, 의존성의 관계는 1대 1로 가벼워졌다. 반면 전략 레이어는 의존성을 양쪽으로 받게 되어 무게가 무거워지므로, 마치 상속에서 부모 클래스가 변경에 취약했던 것처럼 추상 인터페이스가 변경에 취약해지게 된다.

따라서 도메인이 제한되어 있는 경우, 템플릿 메서드로 관계를 단순화하여 의존성을 역전시키며, 도메인이 파악이 되지 않은 경우에는 전략패턴을 사용하여 관계를 분리하고 전략레이어를 하나 추가하여 의존성을 흡수시킨다. 전략 패턴의 전략 레이어는 추상 레이어와 전략 레이어의 변화율을 흡수하는 일종의 어댑터로써 역할한다. 전략 레이어는 여러개의 층을 다중으로 존재할 수도 있어서 각각 또다른 변화율을 흡수할 수 있다.

템플릿 메서드와 전략패턴의 차이를 조금 더 알아보자. 템플릿 메서드 패턴은 런타임에 구상 클래스 타입을 선택 해야 한다. 그에 비해 전략패턴은 런타임에 전략 객체를 합성 한다. 템플릿 메서드 패턴에서는 런타임에 어떤 구상클래스 타입을 if 분기로 선택하여 이를 생성한다면, 전략패턴은 런타임에 확정된 하나의 클래스를 생성하고, 이 객체가 소유할 전략 객체 를 if로 분기하여 생성한다. 선택의 대상이 한 단계 더 구상 레이어로 미루어지게 됐으므로, 클라이언트 객체의 안정성이 확보된다.

따라서 우리는 처음부터 확정되어 있거나, 안정화 시켜야 하는 객체의 경우에는 선택의 여지 없이 전략패턴을 사용하고, 그렇지 않다면 템플릿 메서드 패턴을 사용한다.

템플릿 메서드 패턴은 자식의 중첩되어있거나, 상속 계층이 깊어질 경우 조합 폭발을 유발한다. 같은 레벨에 있는 구상 레이어가 여러 개 있는 하나의 병렬 구조는 처리할 수 있지만, 레이어가 중첩되어 있는 경우에는 레이어마다(훅 메서드의 개수마다) 조합 가능한 경우의 수가 곱해지고, 이 수만큼 구상 클래스 타입을 만들어야 한다. 이에 반해 전략패턴은 확정된 하나의 클래스에서 레이어의 개수만큼 전략객체를 소유하면 되므로 클래스 타입은 1개로 확정되어 있으므로 조합폭발에서 자유롭다.

그러나 전략패턴은 의존성 폭발 이라는 문제가 생긴다. 의존성 폭발은 조합 폭발이 클래스 내부에 코드로 들어와 의존성이 된 것을 말한다. 간단하게 예를 들면, 우리가 구상 레이어가 3개인 어떤 클래스를 템플릿 메서드 패턴으로 구현한다고 할 때, 우리는 이를 가장 클라이언트 측의 코드에서 if문으로 분기를 나눠 미리 생성한 구상 타입을 생성해야 한다. 하지만 전략패턴은 이러한 if 분기문이 클래스 내부로 들어가서 소유 객체를 생성하게 되고 이를 의존성 폭발이라고 한다.

의존성 폭발은 해결이 가능한 문제이지만, 조합 폭발은 상속이 가지는 근본적인 문제로써 템플릿 메서드 패턴의 한계이기도 하다(해결이 불가능함). 따라서 템플릿 메서드를 쓸 때는 조합 가능한 레이어의 개수인 훅 메서드의 개수가 2개 이상이 되지 않도록 해야 한다.

따라서 우리의 목표는 합성 모델을 사용할 때 생기는 의존성 폭발을 어떻게 해결하느냐가 되고, 이것이 바로 객체지향의 주요 관심사이다.

생성사용패턴과 팩토리

전략패턴과 템플릿 메서드 패턴에서 모두, 객체를 생성하기 위한 코드가 존재함을 알 수 있다. 생성사용패턴이란 이러한 객체생성코드와 객체를 실제로 사용하는 코드를 분리하라는 패턴이다. 분리의 이유는 다양하다.

우선, 객체를 생성하는 코드와 사용하는 코드가 같이 있다면 관리가 힘들어진다. 거기에, 객체를 생성하는 코드의 라이프사이클과 사용코드의 라이프사이클은 다르다. 생성은 단 한번만 필요하지만, 사용은 여러번에 걸쳐 사용할 수 있기 때문이다. 심지어는 생성과 사용 타이밍이 다르기도 하다.

따라서 우리는 이 두 가지의 코드를 분리해서 관리해야 훨씬 유지보수를 쉽게 할 수 있다. 객체지향이든, 함수형 프로그래밍이든 프로그래밍 패러다음의 설계 원칙은 코드를 변화율에 따라 나눠주는 것이며, 이 때 객체지향에서는 이러한 나누는 작업을 위해 책임 기반 모델링을 사용할 뿐이다.

그렇다면 이 두가지의 코드를 어떻게 나누어야할까?

바로 생성하는 코드는 클라이언트 측으로 밀어내고, 사용하는 코드는 서비스쪽으로 밀어넣는다. 이를 통해서 , 클라이언트 쪽에서 서비스쪽으로 객체를 주입하는 의존성 주입(Dependency Injection)이 발생한다. 따라서 마지막까지 생성코드를 밀어낸다면 생성코드는 main class에만 남게 될 것이고, 클래스 내부에는 사용코드만 남게될 것이다. 물론, 이 위치는 상대적인 것으로, 생성코드의 위치가 사용코드보다 더 클라이언트 측에 가깝게, 사용코드는 보다 더 서비스 쪽으로 위치하는 것이 생성사용패턴이다.

우리가 생성사용패턴을 사용한다는 것을 그에 따라 코드를 작성한다는 것이 아니라 생성사용패턴을 이용해서 코드를 리팩토링하는 것에 가깝다. 즉, 기존에 알고리즘이라고 생각했던, 생성 코드가 아예 없던 코드를 타입으로 바꿔서 타입을 생성하고, 이를 클라이언트로 밀어내버리는 것이 바로 생성사용패턴이다. 알고리즘이 담당하던 역할을 역할을 생성하는 코드와 역할을 사용하는 코드로 다시 작성하고 생성하는 코드를 클라이언트로 밀어냄으로써, 서비스 코드를 줄이는 것으로, 사실은 패턴이 아니라 알고리즘을 변경하는 기법이다.

그렇다면 클라이언트 코드를 서비스 코드로 어떻게 주입하는지 알아보자

public class DiscountPolicy{
	private final Set<DiscountCondition> conditions = new HashSet<>();
	private final Calculator calculator;
	public DiscountPolicy(Calculator calculator){this.calculator = calculator;}
	public void addCondition(DiscountCondition condition){conditions.add(condition)}
	public Money calculateFee(Screeening screening, int count, Money fee){
		for(DiscountCondition conditions:conditions){
			if(condition.isSatisfiedBy(screening, count)) return calculator.calculateFee(fee);
		}
		return fee;
	}
}

대표적인 주입 injection 이 앞서 알아본 전략패턴이다. 전략객체를 사용하는 서비스코드는 클라이언트로부터 전략객체를 주입받게 된다. 그러나 주입이라는 것은, 객체의 주도권을 전부 클라이언트에게 넘기고 자신의 책임과 역할을 상실하게 되는 것이다. 자신이 원할 때 객체를 요청하여 전달 받을 자율성을 상실하기 때문이다. 따라서 DiscountPolicy는 헐리웃 원칙에 따라 객체를 주입 받는 것이 아니라, 자신이 원할 때 이를 요청 pull 하고 싶을 것이다.

따라서 우리는 주입시에도 제어를 역전할 수 있도록 다음과 같은팩토리 를 만들 것이다.

public interface CalculatorFactory{
		Calculator getCalculator();
}

팩토리는 마치 함수형의 프로그래밍의 지연함수와도 같이 내가 calculator를 원할 때 얻을 수 있게 해준다.

따라서 이를 다음과 같이 구현할 것이다.

public class AmountCalculatorFactory implements CalculatorFactory{
	private final Money money;
	private AmountCalculator cache;
	public AmountCalculatorFactory(Money money){this.money=money;}
	@Override
	synchronized public Calculator getCalculator(){
		if(cache==null) cache= new AmountCalculator(money);
		return cache;
	}
}

구상 팩토리는 money를 가지고 있지만, 실제로 Calculator를 상대방이 원할 때 만들어서 주고 있으며, 한 번 만들어진 인스턴스를 캐시로 재활용하고 있다. 캐시 및 synchronized 등은 팩토리가 사용할 수 있는 정책의 예시이다. 여기서 중요한 점은, 팩토리를 사용함으로써 우리는 서비스 코드가 클라리언트에서 객체를 주입 받는 것이 아닌, 자율성을 가지고 자신의 역할에 따라 필요할 때 객체를 요청 pull할 수 있는 기회를 얻게되었다는 것이다. 그 외에도, 캐시 적용 등 객체 생성에 대해 자신의 책임에 따라 선택할 수 있는 자율권을 가질 수도 있게 됐다.

팩토리를 사용하는 서비스 코드는 다음과 같다. 이제 DiscountPolicy는 클라이언트에서 전략객체를 주입받는 것이 아니라, 전략객체를 생성하는 팩토리를 주입받게 된다.

public class DiscountPolicy{
	private final Set<DiscountCondition> conditions = new HashSet<>();
	private final CalculatorFactory supplier;
	public DiscountPolicy(CalculatorFactory supplier){this.supplier = supplier;}
	public void addCondition(DiscountCondition condition){conditions.add(condition)}
	public Money calculateFee(Screeening screening, int count, Money fee){
		for(DiscountCondition conditions:conditions){
			if(condition.isSatisfiedBy(screening, count)) {
				return supplier.getCalculator().calculateFee(fee);
			}
		}
		return fee;
	}
}

이제 DiscountPolicy는 생성시에 전략객체를 주입받아 이를 그대로 사용하지 않고, 자신이 원할 때 팩토리에 전략객체를 생성하도록 요청하여 이를 사용할 수 있다. 또한 원하는 전략객체가 런타임에 팩토리의 getCalculator 에 동적으로 바인딩 되기 때문에, 팩토리의 정책을 런타임에 얼마든지 변경할 수 있다.

팩토리는 의존성을 역전하지는 않지만 내가 객체를 주입받느냐 요청하느냐의 push &pulll 문제에 해당하는 사용성을 역전한다.

그러나 팩토리에 관한 지식을 아는 것이 과연 최소 지식의 법칙 에 부합할까? 최소지식의 법칙(디미터 법칙 )에 따르면 객체는 필드에 있는 지식만을 알아야 한다. DiscountPolicy는 팩토리만을 알고 있는데, 팩토리로 부터 얻은 전략 객체의 지식인 calucalteFee를 사용하고 있다.

그렇다면 디미터 법칙을 지키기 위해서는 어떻게 해야할까? 아마

  1. factory와 calculator을 안다.
  2. factory만 알게 한다.

의 방법이 있을 것이다.

그러나 1의 방법은 사용할 수 없다. 그 이유는 다음 그림에서 알 수 있는데,

바로 참조가 순환하게 되기 때문이다.

직접적인 의존성은 아니지만 순환을 통해서 양방향 의존성이 생기게 된다. 서비스 코드 측에서 factory와 전략객체에 대한 지식을 모두 알게 된다면 무조건 순환 참조가 발생한다.

그렇다면 우리는 2의 factory만 알게 하는 방법을 써야하는데, factory만 안다는 것은 바로 팩토리에게 전략객체의 책임을 위임해야 한다는 것이다. 따라서 실제 팩토리 인터페이스와 구상 팩토리는 다음과 같아진다.

public interface CalculatorFactory{
		Money calculateFee(Money fee);
}
public class AmountCalculatorFactory implements CalculatorFactory{
	private final Money money;
	private AmountCalculator cache;
	public AmountCalculatorFactory(Money money){this.money=money;}
	
	synchronized public Calculator getCalculator(){
		if(cache==null) cache= new AmountCalculator(money);
		return cache;
	}
	@Override
	public Money calculateFee(Money fee){return getCalculator().calculateFee(fee);}
}

즉 우리는 최소 지식의 법칙 울 지키려면 전략객체의 책임을 위임받은 팩토리를 사용해야 한다.

public Money calculateFee(Screeening screening, int count, Money fee){
		for(DiscountCondition conditions:conditions){
			if(condition.isSatisfiedBy(screening, count)) {
				return supplier.calculateFee(fee);
			}
		}
		return fee
}

팩토리에 대한 지식만으로 모든 것을 해결하였으므로 최소 지식의 법칙 을 지켰다. 그런데 이상한 기분이 들지 않으신가? 왜 팩토리의 메서드인데 전략 객체의 메서드와 똑같이 생겼을까? 그렇다.위임된 팩토리는 구상 전략 객체 그 자체인 것이다. 따라서 팩토리 인터페이스는 원래 존재하지 않는다. 팩토리는 전략 객체의 추상 인터페이스를 상속받는 구상 전략 객체가 된다.

//public interface CalculatorFactory{
//		Money calculateFee(Money fee);
//} 팩토리의 인터페이스는 없다.
public class AmountCalculatorFactory implements Calculator{ //팩토리는 단지 구상 전략 객체이다
	private final Money money;
	private AmountCalculator cache;
	public AmountCalculatorFactory(Money money){this.money=money;}
	
	synchronized public Calculator getCalculator(){
		if(cache==null) cache= new AmountCalculator(money);
		return cache;
	}
	@Override
	public Money calculateFee(Money fee){return getCalculator().calculateFee(fee);}
}

public class DiscountPolicy{
	private final Set<DiscountCondition> conditions = new HashSet<>();
	private final Calculator supplier;
	public DiscountPolicy(Calculator supplier){this.supplier = supplier;}
	public void addCondition(DiscountCondition condition){conditions.add(condition)}
	public Money calculateFee(Screeening screening, int count, Money fee){
		for(DiscountCondition conditions:conditions){
			if(condition.isSatisfiedBy(screening, count)) {
				return supplier.getCalculator().calculateFee(fee);
			}
		}
		return fee;
	}
}

위임된 팩토리는 전략 객체를 위임했기 때문에 그저 전략객체일 뿐이다. 그림과 같이, 팩토리의 추상계층은 사라지게 된다.

추상 팩토리 메소드 패턴

추상 팩토리 메소드는 의존성 폭발을 해결하기 위한 패턴이다. 따라서 여러 구상 레이어에 해당하는 여러 층의 객체를 반환할 수 있는 능력을 가져야 한다. DiscountPolicy의 모든 의존성을 팩토리에 위임하여 객체를 전부 팩토리에서 공급받는다고 가정해보자.

public class DiscountPolicy{
	private final PolicyFactory factory;
	public DiscountPolicy(PolicyFactory factory){this.factory = factory;}
	public Money calculateFee(Screeening screening, int count, Money fee){
		for(DiscountCondition conditions: factory.getConditions()){
			if(condition.isSatisfiedBy(screening, count)) {
				return factory.calculateFee(fee);
			}
		}
		return fee;
	}
}

팩토리는 여러 층의 객체를 반환해야 하기 때문에 더이상 Calculator가 아닌 별도의 추상 레이어가 되어야 한다.

그렇다면 PolicyFactory는 Calculator 이면서 Set<DiscountCondtion> getConditions()만 구현하면 되지 않을까?

public interface PolicyFactory extends Calculator{
	Set<DiscountCondition> getConditions();
}

그러나 이러한 인터페이스는 반환해야될 객체가 2개, 3개가 넘어가기 시작하면, 그 객체들마다 추상 레이어가 있는 경우 조합 폭발이 일어난다. 그럼에도 불구하고, 우리는 최소지식의 법칙을 준수하여 열차전복 사고를 방지하기 위해서 추상 팩토리 메서드 패턴을 사용한다.

public class AmountCalculatorFactory implements PolicyFactory{ //팩토리는 단지 구상 전략 객체이다
	private final Money money;
	private AmountCalculator cache;
	private final Set<DiscountCondition> conditions = new HashSet<>();
	public AmountCalculatorFactory(Money money){this.money=money;}
	
	synchronized public Calculator getCalculator(){
		if(cache==null) cache= new AmountCalculator(money);
		return cache;
	}
	public void addCondition(DiscountCondition condition){conditions.add(condition);}
	public void removeCondition(DiscountCondition condition){conditions.remove(condition);}
	@Override
	public Money calculateFee(Money fee){return getCalculator().calculateFee(fee);}
	@Override
	public Set<DiscountCondition> getConditions(){return conditions;}
}

conditions라는 상태가 DiscountPolicy로 부터 팩토리로 이사왔고, 해당 상태를 다루는 add,remove 메서드와 반환하는 getConditions 메서드가 추가되었다. AmountCalculatorFactory가 제공하는 2가지는 Calculator 객체에게 위힘한 calculateFee 메서드와, 직접 전달하는 conditions set이 있다. 이렇게 여러가지 레이어의 객체를 반환하는 것을 추상 팩토리 메서드 패턴이라고 한다. 이를 사용하는 의유는 무엇일까?

바로 Factory 외에 다른 의존성을 가지지 않도록 의존성 관계를 줄이기 위해서이다. 이제 의존성은 전부 구상 팩토리로 이전하게 된 것이다. 따라서 우리는 여러가지 알고리즘을 구상 클래스로 변환하고 구상 클래스를 생성하는 코드를 클라이언트로 밀어냄으로써, 사용코드인 DiscountPolicy와 생성 코드를 분리할 수 있고 이것이 앞서 알아봤던 생성사용패턴 이 되는 것이다. 내부의 분기문이나 알고리즘등을 전부 클래스로 변환하여 클라이언트에서 생성하는 타입으로 공급받게 된다.

public class DiscountPolicy{
	private final PolicyFactory factory;
	public DiscountPolicy(PolicyFactory factory){this.factory = factory;}
	public Money calculateFee(Screeening screening, int count, Money fee){
		for(DiscountCondition conditions: factory.getConditions()){
			if(condition.isSatisfiedBy(screening, count)) {
				return factory.calculateFee(fee);
			}
		}
		return fee;
	}
}

그러나 한가지 문제가 있다. getConditions로 호출한 Set에 대해서 for의 이터러블을 호출하고 있기 때문에 이또한 열차 전복 이다. 즉, 팩토리가 가진 메서드가 반환하는 객체인 Set이 가지는 또다른 메서드이기 때문에, 한 단계를 건너서 열차 전복사고가 일어나는 것이다. 거기에다가, 이터러블이 반환하는 condition 객체의 isSatisfiedBy라는 메서드도 또한, DiscountCondition이 현재 클래스의 필드 또는, 메서드의 인자나 반환값이 아니기 때문에 당연히 열차전복 이다. 따라서 이 로직은, 모든 DiscountPolicy가 Factory와 관계없이 공통적으로 사용하던 로직이므로 팩토리로 이전해주자.

public interface PolicyFactory extends Calculator{
	public Money calculateFee(Screeening screening, int count, Money fee){
		for(DiscountCondition conditions: factory.getConditions()){
			if(condition.isSatisfiedBy(screening, count)) {
				return calculateFee(fee);
			}
		}
		return fee;
	}
	Set<DiscountCondition> getConditions();
}

따라서 이를 인터페이스의 디폴트로 구현하면 된다. calculateFee는 인터페이스를 구현하는 구상 팩토리에서 구현하게 하면, 모두 본인의 메서드와 필드만을 지식으로 가지게 되므로 최소 지식의 원칙을 준수하게 되었다. 템플릿 메서드가 팩토리로 이전하게 된 것을 알 수 있다. 이 템플릿 메서드 패턴은, 훅이 2개가 되므로,(getConditions(), calculateFee) ,추상 레이어가 2개이며 2개의 레이어의 구상객체를 각각 반환하는 추상 팩토리 메서드 패턴의 형태를 그대로 따르고 있음을 알고 있다. 구상 팩토리는 템플릿 메서드의 조합 폭발 문제를 떠안게 되었다.

따라서 DiscountPolicy의 최종형태는 다음과 같다.

public class DiscountPolicy{
	private final PolicyFactory factory;
	public DiscountPolicy(PolicyFactory factory){this.factory = factory;}
	public Money calculateFee(Screeening screening, int count, Money fee){
		return factory.calculateFee(screening, count, fee);		
	}
}

진정한 의미의 사용코드 밖에 남지 않았다. 우리가 팩토리로 calculateFee를 이전한 까닭은 지식을 팩토리가 가지게 하기 위함이다. 팩토리가 객체를 공급해준다고 해도, 공급한 객체를 사용하기 위해 또다른 지식을 알아야하고, 공급한 객체의 의존성을 똑같이 전부 파악해야 한다면 이는 구상 팩토리의 의존성 폭발을 사용코드에 그대로 가져오는 것이된다. 따라서 공급하는 객체에 대한 지식과, 이를 사용하기 위한 지식을 팩토리가 전부 가져가기 위해서, 추상 팩토리 메서드 패턴위임된 팩토리 패턴을 따르게 된다.

우리는 변화에 가장강하게 대응할 수 있도록 보호해야하는 객체가 누구인지 알아야 한다. 그 객체가 누구인지 찾으면, 이를 가장 변화에 강하게 대응할 수 있는 확정된 구상 서비스 코드 (객체 사용코드) 로 만든 다음 , 이를 기준으로 나머지 코드를 설계하고, 객체의 변화는 다른 객체가 전부 전부 담당하게 하는 것이 설계의 요령이다. 변화하지 않는 객체를 기준으로 의존성을 맺음으로써, 확정된 의존성을 기준으로 다른 객체가 작동하는 관계망을 구성할 수 있다. 모든 변화를 팩토리만 담당하게 하고, 변화하지 않는 DiscountPolicy에 대한 의존성을 가진 나머지 객체들은 변화로부터 격리하는 것이다.

우리가 의존성을 결속한 대상은 최대한 변화로부터 격리시켜 확정된 오퍼레이션을 가지게 해야 한다. 이를 위해 모든 것을 팩토리에게 위임한 형태가 바로 추상 팩토리 메서드 패턴이 되는 것이다. 이제 우리는 변하지 않는, 탄탄한 DiscountPolicy를 기반으로 이와 소통하는 수많은 객체를 만들 수 있다.

이러한 방식은 업무에서도 똑같이 적용된다고 할 수 있다. 우리가 팀에서 개발을 할 때, 우리에게 중요한 것은 우리 자신의 시간 이 아니다. 중요한 것은 남의 시간, 그리고 남의 계획 이다. 다른 사람의 시간계획이 확정되었을 때, 우리는 확정된 외부 환경을 바탕으로 변화가 없는 자신의 업무를 수행할 수 있는 것이다. 우리가 어떠한 객체를 개발하는데, 이를 위해 다른 객체로부터 도움이 필요하다면, 우리가 해야할 일은 개발할 객체를 완성하는 일이 아니라, 도움 받을 객체들을 전부 확정적인 객체 로 변화가 없게 만드는 것이다. 변화가 없는 객체를 확정한다면, 우리는 이를 토대로 동적 바인딩 의 원리를 통해 모든 변화를 위임할 수 있다.

변화가 없는 확정된 토대만이 우리의 코드를 수정여파로부터 구할 수 있다는 사실을 명심하자.

profile
inudevlog.com으로 이전해용

0개의 댓글