카테고리 없음

[만들면서 공부하는 디자인 패턴] 2 : 요금 계산은 어떻게 할까? - 요구사항과 적용 패턴 분석, 전략패턴 적용

공대키메라 2024. 4. 6. 21:51

지난 시간에 이어서 이번에는 요금을 어떤 디자인 패턴을 적용하면 좋을지 공부할 것이다.

 

지난 내용 여기 클릭!

 

우선 필자가 만든 프로젝트의 흐름을 간략하게 설명하겠다.

 

덩그러니 올라가 있는 프로젝트는 객실 예약 시스템인데... fee_start 라는 branch에서 공부를 진행해 볼 예정이다.

 

1탄에서는 요구사항과 적용할 패턴을 분석하고, 각각의 패턴에 대해서 알고 넘어갈 것이다.

 

1. 요구사항

객실을 예약한다고 한다면, 객실을 예약하는 순간 사용자는 일정 기간 특정 방을 예약하면 그 방에 따라 요금이 측정되야 할 것이다.

 

요금 생성에 반영되는 요인은 여러가지가 있다.

 

주간 평일보다는 주말에 가까울 수록 가격이 비쌀것이다. 그리고 성수기이면 주말 가격에 더해서 가격이 오를 것이다. 

 

이러한 경우 금, 토, 일을 다른 일에 비해 비싸다고 가정을 하자.

 

1001번 객실을 목요일, 금요일, 토요일을 예약한다면,

 

또 목요일은 8만원이 하루 숙박 비용이라 한다면, 금요일은 10만원일 수도,  토요일은 11만원일 수도 있어서 3일 숙박 비용은 3박 4일 기준 총 29만원인 것이다.

 

다시 말하면 목요일 하루만 예약한다면 8만원이고 목요일과 금요일을 함께 예약한다면 18만원, 목요일과 금요일 그리고 토요일을 예약한다면 29만원이다.

 

목요일만 예약 : 80,000원
목요일 + 금요일 예약 : 80,000 + 100,000 = 180,000원
목요일 + 금요일 + 토요일 예약 : 80,000 + ,100,000 + 110,000 = 290,000원

 

여기서 예약자에게 다양한 할인이 들어갈 수 있다.

 

특별 기간 할인이 들어갈 수 있고, 고객 할인이 들어갈 수 있고, 또 자주 이용하는 고객에 한해서 프리미엄 할인이 들어갈 수 있다. 

 

쿠폰도 이벤트성으로 판매해서 적용할 수 도 있고, 사장님이 직접 입력을 해서 할인 처리를 해 줄 수도 있다.

 

여기서 주의해야 하는 것이 우리가 숙박을 가면 갑자기 더 좋은 방을 주는 경우도 생기는데, 이벤트 성으로 주인장 맘대로

(엿장수 맘데로!)  할인을 직접 입력하는 경우도 있을 것이다. 

 

임시로 퍼센트 관련해서는 다음과 같이 우선 작성하겠다.

고객 할인 : 고객인 경우 10% 할인
기간 특별 할인 : 특정 기간의 경우 10% 할인
자주 이용하는 고객 할인 : 자주 이용하는 고객의 경우 5% 할인
쿠폰 할인 : 특정 방에 대해서 30% 할인
주인 할인 : 고객이 맘에 들어서(?) 혹은 광고성인 마음대로 할인
장기 투숙 할인 : 10일 이상의 장기 투숙객에게는 20% 할인

 

우선 생각나는 대로 적어 보았다.

무엇을 좋아할지 몰라서 다 준비해봤어...

 

2. 적용 패턴 추리기

그러면 위와 같은 요금을 적용하는데 당장 떠오르는 주의점은 다음과 같다.

 

  • 숙박 일수에 맞춰서 일별 요금이 생성되는데, 각각의 요금이 다르다. 현재는 그렇지만, 나중에는 성수기 구분 없이 적용할 수 도 있을 것 같다.
  • 총 가격에서 다양한 할인 정책을 반영할 수 있다. 여기서 할인 정책은 아무리 많아도 2개까지만 적용할 수 있어야 한다고 본다. 특별한 할인들(여기서는 주인 할인)을 제외하고는 최대 할인 정책을 두 개만 적용이 가능하다. 

 

여기서 계속 생각하면 할 수록 어려운 부분이 있는데, 할인 정책을 처음에는 2개까지 적용만 할 수 있었지만, 나중에는 3개 혹은 최대 40퍼센트 까지만 적용할 수 있게끔 수정할 수 있게 하고 싶다. 

 

왜냐하면 회사의 정책이 변경될 수 있는것이고, 이에 대해 대비를 하고 싶은 것이다. (너가 뭘 좋아할지 모르겟어...)

 

 

그러면.. 어떤 패턴을 적용 적용해야 할 지 고민이 된다.

 

 

현재 다양한 요금 할인 정책을 아무리 추가해도 문제가 없어야 하고, 이 할인 적용 갯수도 제한할 수 가 있다.(2개에서 3개로 변할 수 도 있다!)

 

 

필자는 이를 위해서 전략 패턴을 먼저 사용하려고 한다. 

 

그리고 이러한 전략들을 선택함에 있어서 다양한 경우 편하게 관리해 사용할 수 있도록 팩토리 패턴을 사용할 것이다.

 

마지막으로는 현재 전략 패턴과 팩토리 패턴을 수정하지 않고 데코레이터 패턴으로 원래 기능에 뭔가 추가를 할 예정이었지만...

 

현재 생각으로는 굳이 데코레이터도 효용성이 현재 상황에서는 없는 것 같고, 팩토리도 억지로 끼워넣은 느낌이라서 이정도까지만이 한계인 것 같다. 

 

3. 전략 패턴

전략 패턴(strategy pattern) 또는 정책 패턴(policy pattern)은 실행 중(runtime)에 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴이다.

전략 패턴은 특정한 계열의 알고리즘들을 정의하고 각 알고리즘을 캡슐화하며 이 알고리즘들을 해당 계열 안에서 상호 교체가 가능하게 만든다.

출처 : https//ko.wikipedia.org/wiki/전략_패턴

 

 

다시 말하면 객체의 행동을 변경해야 할 때 직접 코드를 수정하지 않고도, 행동 부분만 교체하여 유연하게 확장활 수 있게 하는 디자인 패턴이다. 

 

알고리즘(행동 방식)을 객체의 일부로 만들고, 실행 중에 그 알고리즘을 교체할 수 있게 해주는 패턴이다. 

 

Open-Closed Principle(개방-폐쇄 원칙)과 동일한 내용이기는 하다.

 

 

예를 들어서 설명하면 동물이 소리를 낸다고 해보자.

 

동물이 소리를 내는 행동 방식(알고리즘) 이 있다면 개, 고양이는 각각 "멍멍", "야옹"(각 알고리즘을 캡슐화)으로 소리를 낼 것이다. 

 

전략 패턴의 Class Diagram은 다음과 같다.

 

 

Context : Strategy에 있는 참조자를 관리한다. 즉, Strategy의 서브클래스 인스턴스를 가지고 있음으로써 구체화한다. 

 

ImplA, ImplB : Strategy 인터페이스의 공통으로 묶어놓은 알고리즘을 실제로 구현한다.

 

Strategy(Compositor) : 제공하는 모든 알고리즘에 대한 공통의 연산들을 인터페이스화, Context는 인터페이스화된 Strategy를 통해서 구체화된 실제 알고리즘을 사용한다.

 

* 장점

장점이 궁금하면 다음을 펼쳐서 보시오! (문을 여시오~)

 

더보기

1. 교체 가능성과 유연성

전략 패턴은 실행 중인 애플리케이션의 행동을 쉽게 변경할 수 있게 한다.

이는 다양한 상황에 대응하는 유연성을 제공한다.

 

2. 확장성

새로운 전략을 추가하기 위해 기존 코드를 수정하지 않고도, 새로운 클래스를 추가하기만 하면 됩니다. 이는 개방-폐쇄 원칙(OCP)을 준수한다.

 

3. 코드 재사용과 분리

전략 패턴은 관련 알고리즘들을 각각의 클래스로 분리하여, 코드 중복을 방지한다.

 

4. 단일 책임 원칙(SRP)의 준수

각 전략이 하나의 알고리즘을 캡슐화하기 때문에, 각 클래스는 하나의 이유로만 변경된다.

 

5. 코드의 유지보수 용이성

알고리즘의 변경이 필요한 경우, 해당 전략 클래스만 수정하면 되므로 유지보수가 편리하다.

 

6. 테스트 용이성

각 전략을 독립적으로 테스트할 수 있다. 이는 특히 단위 테스트를 작성할 때 유용하다.

 

* 단점

단점이 궁금하면 다음을 펼쳐서 보시오! (문을 여시오~)

 

더보기

1. 클래스의 수 증가

전략 패턴은 많은 수의 전략 클래스를 생성할 수 있으며, 이는 시스템의 복잡성을 증가시킬 수 있다.

 

2. 클라이언트의 복잡성

클라이언트는 전략 객체를 관리해야 하며, 어떤 전략을 사용해야 할지 알아야 합니다. 이는 클라이언트 코드를 복잡하게 만들 수 있습니다.

 

3. 전략의 사용과 이해

개발자가 모든 전략을 이해하고 올바르게 사용하는 방법을 알아야 합니다.

 

4. 초기 설정 오버헤드

전략 패턴을 사용하기 위해서는 약간의 초기 설정 작업이 필요합니다. 적은 수의 알고리즘 변형이 있는 경우, 이 패턴이 오버엔지니어링처럼 느껴질 수 있습니다.

 

5. 컨텍스트와 전략 간의 통신 오버헤드

전략과 컨텍스트 간에 데이터를 전달하기 위한 인터페이스가 필요할 수 있습니다. 이는 때때로 통신 오버헤드를 발생시킬 수 있습니다.

 

필자가 생각하기에 초기 설정 오버헤드와 양이 많아지는것은 사실 어느 코드를 만들더라도 요구사항이 많아지면 어쩔수 없는 현상이라고 생각한다.

 

그렇기에 더더욱 유지보수가 용이한 코드를 만들기 위해서 디자인 패턴을 도입하려는 것이다.

 

그러면 어떻게 이것을 적용할지 고민을 해볼까?

 

4. 적용해보기 - 다양한 pricing 전략

 

폴더 구조 : fee/domain/service/pricing

 

 

우선 다양한 어떤 상황에 추가 요금이 적용이 될 지 몰라서 필자는 위의 구조처럼 패키지를 설정했다.

 

Strategy와 구현체들에 대해 설명을 하겠다.

 

SurchargingStrategy.java

public interface SurchargingStrategy {
    PriceVO surchargeFee(MoneyInfo moneyInfo);
}

 

 

해당 interface는 요금을 추가 하는 interface이다. surchrage라는 단어로 그대로 이름을 지었다.

 

 

여기서 SurchargingStrategy는 peak(피크), season(계절)로 구현체를 나누었다.

 

여기서 키메라는 또 고정 요금 추가와 퍼센트 요금 추가로 나누었다.

 

PeakSurchargedByFixedAmountImpl.java

public class PeakSurchargeByFixedAmountImpl implements SurchargingStrategy {
    @Override
    public PriceVO surchargeFee(MoneyInfo moneyInfo) {
        BigDecimal addedPrice = new BigDecimal("20000");
        moneyInfo.addAmount(addedPrice);
        return PriceVO.builder()
                .surchargedPrice(addedPrice)
                .finalApplyPrice(moneyInfo.getSalesAmount())
                .build();
    }
}

 

PeakSurchargedByRateImpl.java

public class PeakSurchargeByRateImpl implements SurchargingStrategy {
    @Override
    public PriceVO surchargeFee(MoneyInfo moneyInfo) {
        BigDecimal newAddedAmount = moneyInfo.calculateAmountByPercent("1.2");
        return PriceVO.builder()
                .surchargedPrice(newAddedAmount)
                .finalApplyPrice(moneyInfo.getSalesAmount())
                .build();
    }
}

 

아직 덜 완성된 것이긴 한데, 여기서 실제로 성수기에는 얼만큼의 요금이 고정요금시에는 이만큼, 퍼센트 요금 시에는 이만큼 정해야 하는데 그것은 잠시 더 생각을 하고 진행하려고 한다.

 

하여간, 여기서 다시 구조를 살펴보고 가려고 한다.

 

* 전략 패턴 구조

 

* 키메라가 적용한 전략 패턴 구조

 

얼추 구조가 비슷하지 않나?

 

이런 식으로 다른 추가 요금 로직은 SurchargingStrategy를 이용했다.

 

 

* FeeDomainServiceImpl.java -> createTempDailyFee()

@Override
@Transactional
public List<DailyFeeDTO> createTempDailyFee(FeeSearchDTO feeSearchDTO) {

    List<DailyFeeDTO> result = new ArrayList<>();
    Fee fee = queryFeeRepository.findOneFeeWithConditions(feeSearchDTO);

    if (fee == null)
        throw new IllegalArgumentException("fee is not exist");
    LocalDate enterRoomDate = feeSearchDTO.getEnterRoomDate();
    LocalDate leaveRoomDate = enterRoomDate.plusDays(feeSearchDTO.getStayDayCnt());

    List<Calender> calenders = calenderService.selectCalenderInfoBySolarDateBetween(enterRoomDate, leaveRoomDate);
    List<PricingHistory> pricingHistoryList = new ArrayList<>();
    List<PricingHistoryDTO> pricingHistoryDTOList = new ArrayList<>();

    for (Calender calender : calenders) {

        TempDailyFee tempDailyFee = tempDailyFeeFactory.create(fee, calender);
        applySeasonalPricing(calender, tempDailyFee).ifPresent(pricingHistoryList::add);
        applyPeakPricing(calender, tempDailyFee).ifPresent(pricingHistoryList::add);
        applyEventPricing(calender, tempDailyFee).ifPresent(pricingHistoryList::add);

        DailyRoomFee newDailyRoomFee = DailyRoomFee.builder()
                .occurDate(calender.getSolarDate())
                .fee(fee)
                .pricingHistoryList(pricingHistoryList)
                .moneyInfo(tempDailyFee.getMoneyInfo())
                .currentCode(feeSearchDTO.getCurrentCode())
                .build();

        dailyRoomFeeRepository.save(newDailyRoomFee);

        for (PricingHistory pricingHistory : pricingHistoryList) {
            pricingHistoryDTOList.add(PricingHistoryDTO.builder()
                .appliedPrice(pricingHistory.getAppliedPrice())
                .pricingType(pricingHistory.getPricingType().toString())
                .applyReason(pricingHistory.getApplyReason())
                .build());
        }

        DailyFeeDTO dailyFeeDTO = DailyFeeDTO.builder()
                .feeName(fee.getFeeName())
                .salesAmount(newDailyRoomFee.getMoneyInfo().getSalesAmount())
                .addedAmount(newDailyRoomFee.getMoneyInfo().getAddedAmount())
                .discountAmount(newDailyRoomFee.getMoneyInfo().getDiscountAmount())
                .occurDate(calender.getSolarDate())
                .currentCode(newDailyRoomFee.getCurrentCode())
                .productAmount(fee.getFeeAmount())
                .pricingHistoryDTOList(pricingHistoryDTOList)
                .build();

        result.add(dailyFeeDTO);
    }

    return result;
}

 

* FeeDoaminServiceImpl.java -> applySeasonalPricing()

private Optional<PricingHistory> applySeasonalPricing(Calender calender, TempDailyFee tempDailyFee) {
    if (calender.getSeasonDivCd().equals("Y")) {
        SurchargingStrategy surchargingStrategy = null;
        if (shouldUseFixedAmountStrategy()) {
            surchargingStrategy = new SeasonSurchargeByFixedAmountImpl();
        } else {
            surchargingStrategy = new SeasonSurchargeByRateImpl();
        }

        PriceVO surchargedPrice = surchargingStrategy.surchargeFee(tempDailyFee.getMoneyInfo());

        PricingHistory pricingHistory = PricingHistory.builder()
                .tempDailyFee(tempDailyFee)
                .applyReason("Seasonal Surcharge")
                .pricingType(ChargeEnum.CHARGE)
                .appliedPrice(surchargedPrice.getSurchargedPrice())
                .build();

        return Optional.of(pricingHistoryRepository.save(pricingHistory));
    }

    return Optional.empty();
}

 

 

여기서 보면 SurchargingStrategy를 어떤 구현체를 적용해야 하는지 하는 아주 이상한 코드가 들어가 있다.

 

이것을 헤드퍼스트 디자인 패턴에서 말하길 '간단한 팩토리' 정의(page 151)로 알아서 작동하도록 변경하겠다.

 

* SurgingStrategyFactory.java 추가

 

 

@Service
@RequiredArgsConstructor
public class SurgingStrategyFactory {

    private final PeakSurchargeByFixedAmountImpl peakFixed;
    private final PeakSurchargeByRateImpl peakRate;
    private final SeasonSurchargeByFixedAmountImpl seasonFixed;
    private final SeasonSurchargeByRateImpl seasonRate;

    public SurchargingStrategy getPeakStrategy() {
        return true ? peakFixed : peakRate;
    }

    public SurchargingStrategy getSeasonalStrategy() {
        return true ? seasonFixed : seasonRate;
    }
}

 

현재 그냥 고정 요금으로 적용중이지만, 고정인지 아니면 요금인지 동적으로 관리하고 싶다면 데이터에 저장해서 해당 로직 호출시 어떤 구현체를 세팅하도록 로직을 구성하면 될 것이다.

 

그래서 완성된 코드는 다음을 보면 된다.

 

* FeeDomainSeviceImpl.java 완성본

 

package org.reservation.system.fee.domain.service.impl;

import lombok.RequiredArgsConstructor;
import org.reservation.system.calander.application.service.CalenderService;
import org.reservation.system.calander.application.service.enums.DayDivEnum;
import org.reservation.system.calander.domain.Calender;
import org.reservation.system.fee.application.dto.DailyFeeDTO;
import org.reservation.system.fee.application.dto.FeeSearchDTO;
import org.reservation.system.fee.application.dto.PricingHistoryDTO;
import org.reservation.system.fee.application.enums.ChargeEnum;
import org.reservation.system.fee.application.vo.PriceVO;
import org.reservation.system.fee.domain.TempDailyFeeFactory;
import org.reservation.system.fee.domain.model.*;
import org.reservation.system.fee.domain.repository.FeeRepository;
import org.reservation.system.fee.domain.service.FeeDomainService;
import org.reservation.system.fee.domain.service.pricing.SurchargingStrategy;
import org.reservation.system.fee.domain.service.pricing.impl.peak.PeakSurchargeByFixedAmountImpl;
import org.reservation.system.fee.domain.service.pricing.impl.peak.PeakSurchargeByRateImpl;
import org.reservation.system.fee.infrastructure.persistence.*;
import org.reservation.system.fee.value.MoneyInfo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.swing.text.html.Option;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Service
@RequiredArgsConstructor
public class FeeDomainServiceImpl implements FeeDomainService {

    private final FeeRepository feeRepository;
    private final DailyRoomFeeRepository dailyRoomFeeRepository;
    private final QueryFeeRepository queryFeeRepository;
    private final CalenderService calenderService;
    private final PricingHistoryRepository pricingHistoryRepository;
    private final TempDailyFeeRepository tempDailyFeeRepository;
    private final TempDailyFeeFactory tempDailyFeeFactory;
    private final EventRepository eventRepository;
    private final SurgingStrategyFactory surgingStrategyFactory;

    @Override
    public DailyFeeDTO createDailyFee(Fee fee, Calender calender) {


        return null;
    }

    @Override
    @Transactional
    public List<DailyFeeDTO> createTempDailyFee(FeeSearchDTO feeSearchDTO) {

        List<DailyFeeDTO> result = new ArrayList<>();
        Fee fee = queryFeeRepository.findOneFeeWithConditions(feeSearchDTO);

        if (fee == null)
            throw new IllegalArgumentException("fee is not exist");
        LocalDate enterRoomDate = feeSearchDTO.getEnterRoomDate();
        LocalDate leaveRoomDate = enterRoomDate.plusDays(feeSearchDTO.getStayDayCnt());

        List<Calender> calenders = calenderService.selectCalenderInfoBySolarDateBetween(enterRoomDate, leaveRoomDate);
        List<PricingHistory> pricingHistoryList = new ArrayList<>();
        List<PricingHistoryDTO> pricingHistoryDTOList = new ArrayList<>();

        for (Calender calender : calenders) {

            TempDailyFee tempDailyFee = tempDailyFeeFactory.create(fee, calender);
            applySeasonalPricing(calender, tempDailyFee).ifPresent(pricingHistoryList::add);
            applyPeakPricing(calender, tempDailyFee).ifPresent(pricingHistoryList::add);
            applyEventPricing(calender, tempDailyFee).ifPresent(pricingHistories -> pricingHistoryList.addAll(pricingHistories));

            DailyRoomFee newDailyRoomFee = DailyRoomFee.builder()
                    .occurDate(calender.getSolarDate())
                    .fee(fee)
                    .pricingHistoryList(pricingHistoryList)
                    .moneyInfo(tempDailyFee.getMoneyInfo())
                    .currentCode(feeSearchDTO.getCurrentCode())
                    .build();

            dailyRoomFeeRepository.save(newDailyRoomFee);

            for (PricingHistory pricingHistory : pricingHistoryList) {
                pricingHistoryDTOList.add(PricingHistoryDTO.builder()
                        .appliedPrice(pricingHistory.getAppliedPrice())
                        .pricingType(pricingHistory.getPricingType().toString())
                        .applyReason(pricingHistory.getApplyReason())
                        .build());
            }

            DailyFeeDTO dailyFeeDTO = DailyFeeDTO.builder()
                    .feeName(fee.getFeeName())
                    .salesAmount(newDailyRoomFee.getMoneyInfo().getSalesAmount())
                    .addedAmount(newDailyRoomFee.getMoneyInfo().getAddedAmount())
                    .discountAmount(newDailyRoomFee.getMoneyInfo().getDiscountAmount())
                    .occurDate(calender.getSolarDate())
                    .currentCode(newDailyRoomFee.getCurrentCode())
                    .productAmount(fee.getFeeAmount())
                    .pricingHistoryDTOList(pricingHistoryDTOList)
                    .build();

            result.add(dailyFeeDTO);
        }

        return result;
    }

    private Optional<PricingHistory> applySeasonalPricing(Calender calender, TempDailyFee tempDailyFee) {
        if (calender.getSeasonDivCd().equals("Y")) {
            SurchargingStrategy surchargingStrategy = surgingStrategyFactory.getSeasonalStrategy();
            PriceVO surchargedPrice = surchargingStrategy.surchargeFee(tempDailyFee.getMoneyInfo());
            PricingHistory pricingHistory = PricingHistory.builder()
                    .tempDailyFee(tempDailyFee)
                    .applyReason("Seasonal Surcharge")
                    .pricingType(ChargeEnum.CHARGE)
                    .appliedPrice(surchargedPrice.getSurchargedPrice())
                    .build();
            return Optional.of(pricingHistoryRepository.save(pricingHistory));
        }

        return Optional.empty();
    }

    private Optional<PricingHistory> applyPeakPricing(Calender calender, TempDailyFee tempDailyFee) {
        if (DayDivEnum.isPeakOfWeek(calender.getDayDivCd())) {
            SurchargingStrategy surchargingStrategy = surgingStrategyFactory.getPeakStrategy();

            PriceVO surchargedPrice = surchargingStrategy.surchargeFee(tempDailyFee.getMoneyInfo());

            PricingHistory pricingHistory = PricingHistory.builder()
                    .tempDailyFee(tempDailyFee)
                    .applyReason("Peak Surcharge")
                    .pricingType(ChargeEnum.CHARGE)
                    .appliedPrice(surchargedPrice.getSurchargedPrice())
                    .build();
            return Optional.of(pricingHistoryRepository.save(pricingHistory));
        }

        return Optional.empty();
    }

    private Optional<List<PricingHistory>> applyEventPricing(Calender calender, TempDailyFee tempDailyFee) {
        List<PricingHistory> result = new ArrayList<>();
        List<Event> eventList = eventRepository.findByDateBetween(calender.getSolarDate());
        MoneyInfo moneyInfo = tempDailyFee.getMoneyInfo();
        for (Event event : eventList) {

            MoneyInfo updatedMoneyInfo = updateMoneyInfo(moneyInfo, event.getChargeAmount(), event.getChargeDivCd());

            BigDecimal chargeAmount = event.getChargeAmount();
            if (ChargeEnum.isAddingPrice(event.getChargeDivCd())) {
                moneyInfo.addAmount(event.getChargeAmount());
            } else {
                moneyInfo.subtractAmount(event.getChargeAmount());
            }

            PricingHistory pricingHistory = PricingHistory.builder()
                    .tempDailyFee(tempDailyFee) // 앞서 생성된 TempDailyFee의 참조 설정
                    .applyReason("Event Pricing")
                    .pricingType(event.getChargeDivCd())
                    .appliedPrice(chargeAmount)
                    .build();

            moneyInfo = updatedMoneyInfo;

            result.add(pricingHistoryRepository.save(pricingHistory));
        }

        return result.isEmpty() ? Optional.empty() : Optional.of(result);
    }

    private MoneyInfo updateMoneyInfo(MoneyInfo moneyInfo, BigDecimal chargeAmount, ChargeEnum chargeEnum) {
        return chargeEnum == ChargeEnum.CHARGE ? moneyInfo.addAmount(chargeAmount) : moneyInfo.subtractAmount(chargeAmount);
    }

    @Override
    public List<DailyFeeDTO> applyDiscountPolicy(Fee fee) {
        return null;
    }
}

 

현재 완성이라고 했지만 아직 미흡한 부분이 있는 점 양해 바란다. (ㅠㅠ)

 

다음에는 여기서 데코레이터 패턴을 어떻게 적용해야 하는지, 그리고 팩토리 패턴에 대해 좀 더 자세히 배우도록 하겠다.

 

5. 코드 리뷰 - Optional

이번 기회에사실 평소에 잘 사용하지 않던 Optional을 사용해보았다.

 

Optional을 사용한 이유는 다음 코드를 통해 설명하겠다.

 

* FeeDomainServiceImpl.java -> applyPrices()

private void applyPrices(Calender calender, TempDailyFee tempDailyFee, List<PricingHistory> pricingHistoryList) {
    applySeasonalPricing(calender, tempDailyFee).ifPresent(pricingHistoryList::add);
    applyPeakPricing(calender, tempDailyFee).ifPresent(pricingHistoryList::add);
    applyEventPricing(calender, tempDailyFee).ifPresent(pricingHistories -> pricingHistoryList.addAll(pricingHistories));
}

 

 

처음에는 pricingHistoryList에 결과가 어떻든 각각 pricing의 결과를 add하는 식으로 구성이 되었다.

 

그런데 그렇게 되면 pricing들의 정책에 부합하지 않아서 아무 요금도 반영되지 않았는데 빈 element인 null이 반환되어 pricingHistoryList에 추가되는 버그가 발생했다.

 

이게 좀 더 복잡했으면 찾아내는데 엄청 애를 먹었을 것이다.

 

그래서 이를 방지하고나 반영된 것이 없는 경우에는 Optional.isEmpty()를 반환하고, 값이 존재하면 Optional.of()로 감싸서 반환한 다음, 받는 applyPrices측에서는 ifPresent, 즉 반환값이 실제로 존재하는 경우에는 history에 추가가 되도록 구성했다.

 

썩 좋은 방법이지 아니한가?!?

 


 

Java 8버전 부터 람다식을 도입하게 되면서 사실 간단한 전략의 경우에는 복잡하게 구성할 필요 없이 바로 바로 구성해서 사용이 가능하다. 

 

그래서 여기서는 사실 사용하지 않아도 크게 문제는 없는것으로 판단이 되긴 한다. (아직 로직이 복잡하지 않기 때문에)

 

하지만 필자의 생각으로는 우선은... 이렇게 작성하고 복잡하면 그대로 쓰면 되고, 아니어도 사실 크게 코드가 더럽다고 생각이 들지 않기에 괜찮다고 생각한다.

 

 

그리고 데코레이터 패턴을 하려고 해도 이미 글쎄... 내가 보기에는 여기 부분에서는 더 뭐가 들어가는 순간 오버엔지니어링이라는 생각이 들었다.

 

그러니까 굳이 이미 돌아가게끔 작성을 다 했는데 여기에 데코레이터를 낑겨 넣어야 하는가 하는 의구심이 들었다. 

 

그래서 다른 부분에서 사용 시에 데코레이터가 사용될 일이 있으면 사용을 하도록 하겠다.

 

사실 생각을 해보면... 데코레이터가 그렇게 쓰일 일이 별로 없을거라는 생각이 들긴 한다. 

 

필자는 event의 요금 생성 구조에서 반복문으로 여러 요금을 구현했는데, 이거 사실 데코레이터를 어떻게든 쓰면 가능은 했을텐데... 이미 이것이  계산을 잘 할수 있도록 구성을 했는데 다시 이것을 데코레이터 쓰고 싶어 ㅠㅠ 하면서 고치기에는... 굳이 그래야 하는 생각이 든다.

 

 

정리하자면... 다른 곳에서 쓸 일이 있으면 사용을 하겠다는 말이다.

 

해당 프로젝트가 공부를 진행하는 프로젝트이니 프로젝트와는 무관한 예시 코드를 oopstudy_solid 패키지 안에 구성할 예정이다.

 

 

 

팩토리 패턴도 사실 어디에 적용을 할지 감이 안잡힌다.

 

우선은 더욱 프로젝트를 고도화 하면서 추후에 적용을 해보도록 하겠다.

 

 

 

참고 :

[디자인패턴] 전략 패턴 (Strategy Pattern)

[10분 테코톡] 완태의 전략패턴

Strategy Pattern (전략 패턴) - YeongUng Kim

헤드퍼스트 디자인 패턴(개정판)