이 글에서 작성한 코드는 Spring framework를 기반으로 작성했으며 디자인 패턴 중 composite 패턴에 대한 이해가 있어야 읽기 쉬움을 알립니다.
지하철 미션을 진행하면서 거리에 따라 다른 요금 정책을 적용해야 했다.
요금 계산 방법
- 기본운임(10㎞ 이내): 기본운임 1,250원
- 이용 거리 초과 시 추가운임 부과
- 10km~50km: 5km 까지 마다 100원 추가
- 50km 초과: 8km 까지 마다 100원 추가
if문으로 쉽게 구현할 수 있지만 다음과 같은 확장의 가능성을 생각하면 수정이 어려울 것이다.
- 거리별 요금 정책이 추가, 수정되는 경우
- 거리별 정책 이외에 노선별, 승객의 나이별 요금 정책이 추가되는 경우
확장 가능성을 고려하여 객체를 분리하여 요금을 계산해보도록 하자.
composite 패턴
모든 요금 정책을 적용해서 요금을 계산하는 FareCalculator가 각 정책을 의존하고 있다.
현재는 거리별 정책밖에 없기 때문에 DistancePolicy만 의존하고 있다.
- DistancePolicy는 Leaf와 Composite의 공통 인터페이스인 Component이다
- 각 요금 정책을 가지고 있는 구현체들(BasicFarePolicy, FiveUnitFarePolicy, EightUnitFarePolicy)는 leaf로 DistancePolicy를 완성하는 부분이다.
- 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를 추가하여 사용하면 쉽게 중첩으로 요금 정책을 적용할 수 있을 것이다.
'개발 > JAVA' 카테고리의 다른 글
[JAVA] 스트림의 forEach 제거하기 (0) | 2023.09.13 |
---|---|
벤치마크로 병렬 스트림 성능 확인하기 (0) | 2023.04.08 |
JDK 21의 새로운 개념 JEP 431: Sequenced Collections (0) | 2023.03.21 |
[JCF] ArrayList와 LinkedList의 차이 (0) | 2023.02.17 |
일급 컬렉션 (First Class Collection) 이란? (0) | 2022.12.03 |