이 글에서 작성한 코드는 Spring framework를 기반으로 작성했으며 디자인 패턴 중 composite 패턴에 대한 이해가 있어야 읽기 쉬움을 알립니다.

지하철 미션을 진행하면서 거리에 따라 다른 요금 정책을 적용해야 했다.


요금 계산 방법

  • 기본운임(10㎞ 이내): 기본운임 1,250원 
  • 이용 거리 초과 시 추가운임 부과
    • 10km~50km: 5km 까지 마다 100원 추가
    • 50km 초과: 8km 까지 마다 100원 추가

 

if문으로 쉽게 구현할 수 있지만 다음과 같은 확장의 가능성을 생각하면 수정이 어려울 것이다.

  1. 거리별 요금 정책이 추가, 수정되는 경우
  2. 거리별 정책 이외에 노선별, 승객의 나이별 요금 정책이 추가되는 경우


확장 가능성을 고려하여 객체를 분리하여 요금을 계산해보도록 하자.


composite 패턴


모든 요금 정책을 적용해서 요금을 계산하는 FareCalculator가 각 정책을 의존하고 있다.
현재는 거리별 정책밖에 없기 때문에 DistancePolicy만 의존하고 있다.

 

  1. DistancePolicy는 Leaf와 Composite의 공통 인터페이스인 Component이다
  2. 각 요금 정책을 가지고 있는 구현체들(BasicFarePolicy, FiveUnitFarePolicy, EightUnitFarePolicy)는 leaf로 DistancePolicy를 완성하는 부분이다.
  3. DistanceFare는 DistancePolicy의 여러 구현체들을 가지고 전체를 구성하는 Composite이다.


코드로 작성해보자.

다음은 Component인 DistancePolicy이다. 각 leaf와 composite가 수행해야 하는 기능을 명세하고 있다.

public interface DistancePolicy {  
	int calculate(int distance);  
}



각 요금 정책에 대한 구현체들이다.

@Component  
public class BasicFarePolicy implements DistancePolicy {  
	private static final int BASIC_FARE = 1250;  
  
	@Override  
	public int calculate(final int distance) {  
		return BASIC_FARE;  
	}  
}



기본 요금으로 무조건 1250원을 반환한다.

@Component  
public class FiveUnitFarePolicy implements DistancePolicy {  
	private static final int UNIT = 5;  
	private static final int FARE = 100;  
	private static final int MIN_CALCULATE_RANGE = 10;  
	private static final int MAX_CALCULATE_RANGE = 50;  
  
	@Override  
	public int calculate(final int distance) {  
		if (distance >= MAX_CALCULATE_RANGE) {  
			return (int) ((Math.ceil((MAX_CALCULATE_RANGE - MIN_CALCULATE_RANGE - 1) / UNIT) + 1) * FARE);  
		}  
		if (distance <= MIN_CALCULATE_RANGE) {  
			return 0;  
		}  
		return (int) ((Math.ceil((distance - MIN_CALCULATE_RANGE - 1) / UNIT) + 1) * FARE);  
	}  
}



5km의 거리마다의 요금을 계산하는 클래스이다. 거리가 10km~50km 사이인 부분만 계산한다.

@Component  
public class EightUnitFarePolicy implements DistancePolicy {  
	private static final int UNIT = 8;  
	private static final int FARE = 100;  
	private static final int MIN_CALCULATE_RANGE = 50;  
	  
	@Override  
	public int calculate(final int distance) {  
		if (distance <= MIN_CALCULATE_RANGE) {  
			return 0;  
		}  
		return (int) ((Math.ceil((distance - MIN_CALCULATE_RANGE - 1) / UNIT) + 1) * FARE);  
	}  
}



8km 거리마다의 요금을 계산한다. 거리가 50km 초과인 부분만 계산한다.

다음은 Composite이다. Leaf인 DistancePolicy의 리스트를 가지고 있다. 
위에서 정의했던 각 Policy들을 리스트에 초기화해준 다음 calculate에서 bulk연산을 수행한다.

public class DistanceFare implements DistancePolicy {  
  
	private final List<DistancePolicy> distancePolicies;  
	  
	public DistanceFare(final List<DistancePolicy> distanceChains) {  
		this.distancePolicies = distanceChains;  
	}  
	  
	@Override  
	public int calculate(final int distance) {  
		return distancePolicies.stream()  
		.mapToInt(distancePolicy -> distancePolicy.calculate(distance))  
		.sum();  
	}  
}



각 Policy들이 자신의 거리에 해당하는 부분만 계산하고 있기 때문에 결과를 모두 더해주면 거리별 요금 정책이 
적용된 결과를 얻을 수 있다.

다음은 모든 Composite를 초기화하는 Configuration클래스이다.
Leaf들을 @Component로 빈으로 등록했기 때문에 생성자 주입으로 Leaf의 리스트를 초기화한다.

@Configuration  
public class FareConfiguration {  
  
	private final List<DistancePolicy> distancePolicies;  
	  
	public FareConfiguration(final List<DistancePolicy> distancePolicies) {  
		this.distancePolicies = distancePolicies;  
	}  
	  
	@Bean  
	public DistancePolicy distancePolicy() {  
		return new DistanceFare(distancePolicies);  
	}  
}



완성된 Composite를 빈으로 등록한다.

최종으로 모든 요금 정책을 적용하여 요금을 계산하는 FareCalculator에서는 빈으로 등록했던 Composite를 의존성 주입 받아서 사용한다.

@Component  
public class FareCalculator {  
	private final DistancePolicy distancePolicy;  
	  
	public FareCalculator(final DistancePolicy distancePolicy) {  
		this.distancePolicy = distancePolicy;  
	}  
	  
	public int calculate(final int distance) {  
		return distancePolicy.calculate(distance);  
	}  
}

결론

  • composite 패턴을 사용하여 거리별 요금 계산이라는 알고리즘을 추상화할 수 있었다.
  • bulk연산을 하는 Composite도 같은 DistancePolicy를 구현하기 때문에 bulk 연산까지 추상화하여 알고리즘을 감출 수 있다.
  • DistancePolicy 인터페이스에 의존하고 있기 때문에 거리별 요금 정책이 추가되거나 변경되더라도 기존의 코드는 변경이 없고, 각 Leaf를 수정하거나 추가해주면 되기 때문에 변경에 닫혀있고 확장에 열려있다.(OCP)
  • FareCalculator에서 DistancePolicy이외에 다른 요금 정책이 추가되더라도 해당 Componet를 추가하여 사용하면 쉽게 중첩으로 요금 정책을 적용할 수 있을 것이다.

 

+ Recent posts