비교 요약으로 돌아가기

Full output

GPT-5.6 Terra · medium 원문 결과물

할인 규칙 계산 서비스 리팩터링 비교 / OpenAI / GPT-5.6 Terra / 실행일 2026-08-02

할인 규칙 계산 리팩터링 결과

모델: gpt-5.6-terra 추론: medium

공개 API와 기존 계산 결과를 유지한 채 DiscountService를 전략 기반 구조로 리팩터링했습니다. 컨트롤러와 기존 테스트는 수정하지 않았습니다.

변경 파일 (7개)

  • service/DiscountService.java — 요청 검증 호출, 전략 선택, 원 단위 반올림·할인액 클램핑·응답 조립을 한 흐름으로 통합
  • service/DiscountRequestValidator.java — 공통 기본 검증과 유형별 파라미터 검증을 단일 지점으로 통합
  • service/DiscountCalculator.java — 할인 계산 전략 인터페이스
  • service/PercentageDiscountCalculator.java — 정률 할인 계산
  • service/FlatDiscountCalculator.java — 정액 할인 계산
  • service/BogoDiscountCalculator.java — 1+1 할인 계산
  • service/TieredDiscountCalculator.java — 구간별 누진 할인 계산

검증

실행 명령:

./gradlew test

결과: BUILD SUCCESSFUL (18 tests, 0 failures, 0 errors)

제거한 중복과 트레이드오프

  • DiscountRequestValidator에서 네 할인 유형의 요청 검증을 관리한다.
  • DiscountService.roundToWon()에서 원 단위 HALF_UP 반올림을 한 번만 수행하고, 이어서 할인액 하한·상한 클램핑과 응답 조립도 공통화했다.
  • 각 계산기는 원시 할인 금액만 계산하므로 유형별 로직이 서로 독립적이다.
  • 전략 클래스가 늘어 파일 수는 증가했지만, 새 할인 유형을 추가할 때 기존 계산 분기를 수정하는 대신 독립 전략을 추가할 수 있다.

코드 산출물

아래는 설명만 읽지 않고 리팩터링 전후의 실제 핵심 코드를 확인할 수 있도록 공개한 것이다. DiscountController와 DTO·enum은 변경하지 않았으며, 아래 작업 사본의 코드와 테스트를 기준으로 한다.

리팩터링 전

DiscountService.java — 시작 하네스의 원본

@Service
public class DiscountService {

    public DiscountResponse calculate(DiscountRequest request) {
        CouponType couponType = request.getCouponType();
        if (couponType == null) {
            throw new IllegalArgumentException("couponType is required");
        }
        long originalPrice = request.getOriginalPrice();
        if (originalPrice <= 0) {
            throw new IllegalArgumentException("originalPrice must be positive");
        }

        if (couponType == CouponType.PERCENTAGE) {
            BigDecimal value = request.getValue();
            if (value == null || value.compareTo(BigDecimal.ZERO) < 0 || value.compareTo(new BigDecimal("80")) > 0) {
                throw new IllegalArgumentException("percentage value must be between 0 and 80");
            }
            BigDecimal raw = BigDecimal.valueOf(originalPrice)
                .multiply(value)
                .divide(new BigDecimal("100"), 10, RoundingMode.HALF_UP);
            long discountAmount = raw.setScale(0, RoundingMode.HALF_UP).longValue();
            discountAmount = Math.max(0, Math.min(discountAmount, originalPrice));
            return new DiscountResponse(couponType, originalPrice, discountAmount, originalPrice - discountAmount);
        }

        if (couponType == CouponType.FLAT) {
            BigDecimal value = request.getValue();
            if (value == null || value.compareTo(BigDecimal.ZERO) < 0) {
                throw new IllegalArgumentException("flat value must be zero or positive");
            }
            long discountAmount = value.setScale(0, RoundingMode.HALF_UP).longValue();
            discountAmount = Math.max(0, Math.min(discountAmount, originalPrice));
            return new DiscountResponse(couponType, originalPrice, discountAmount, originalPrice - discountAmount);
        }

        if (couponType == CouponType.BOGO) {
            Integer quantity = request.getQuantity();
            if (quantity == null || quantity < 2) {
                throw new IllegalArgumentException("quantity must be at least 2 for BOGO");
            }
            BigDecimal unitPrice = BigDecimal.valueOf(originalPrice)
                .divide(BigDecimal.valueOf(quantity), 10, RoundingMode.HALF_UP);
            BigDecimal raw = unitPrice.multiply(BigDecimal.valueOf(quantity / 2));
            long discountAmount = raw.setScale(0, RoundingMode.HALF_UP).longValue();
            discountAmount = Math.max(0, Math.min(discountAmount, originalPrice));
            return new DiscountResponse(couponType, originalPrice, discountAmount, originalPrice - discountAmount);
        }

        if (couponType == CouponType.TIERED) {
            BigDecimal raw = BigDecimal.valueOf(originalPrice).multiply(tieredRateOf(originalPrice));
            long discountAmount = raw.setScale(0, RoundingMode.HALF_UP).longValue();
            discountAmount = Math.max(0, Math.min(discountAmount, originalPrice));
            return new DiscountResponse(couponType, originalPrice, discountAmount, originalPrice - discountAmount);
        }

        throw new IllegalArgumentException("unsupported couponType: " + couponType);
    }
}

Terra medium 결과

DiscountService.java — 검증 호출, 전략 선택, 최종 원 단위 정규화만 담당

@Service
public class DiscountService {

    private static final int WON_SCALE = 0;
    private static final long MINIMUM_DISCOUNT_AMOUNT = 0L;

    private final DiscountRequestValidator requestValidator = new DiscountRequestValidator();
    private final Map<CouponType, DiscountCalculator> calculators = new EnumMap<>(CouponType.class);

    public DiscountService() {
        calculators.put(CouponType.PERCENTAGE, new PercentageDiscountCalculator());
        calculators.put(CouponType.FLAT, new FlatDiscountCalculator());
        calculators.put(CouponType.BOGO, new BogoDiscountCalculator());
        calculators.put(CouponType.TIERED, new TieredDiscountCalculator());
    }

    public DiscountResponse calculate(DiscountRequest request) {
        requestValidator.validate(request);

        CouponType couponType = request.getCouponType();
        long originalPrice = request.getOriginalPrice();
        BigDecimal rawDiscount = calculators.get(couponType).calculate(request);
        long discountAmount = clampToOriginalPrice(roundToWon(rawDiscount), originalPrice);
        return new DiscountResponse(couponType, originalPrice, discountAmount, originalPrice - discountAmount);
    }

    private long roundToWon(BigDecimal rawDiscount) {
        return rawDiscount.setScale(WON_SCALE, RoundingMode.HALF_UP).longValue();
    }

    private long clampToOriginalPrice(long discountAmount, long originalPrice) {
        return Math.max(MINIMUM_DISCOUNT_AMOUNT, Math.min(discountAmount, originalPrice));
    }
}

DiscountRequestValidator.java — 네 유형의 입력 검증을 한 곳에서 담당

final class DiscountRequestValidator {

    private static final long MINIMUM_ORIGINAL_PRICE_EXCLUSIVE = 0L;
    private static final BigDecimal MINIMUM_VALUE = BigDecimal.ZERO;
    private static final BigDecimal MAXIMUM_PERCENTAGE = new BigDecimal("80");
    private static final int MINIMUM_BOGO_QUANTITY = 2;

    void validate(DiscountRequest request) {
        CouponType couponType = request.getCouponType();
        if (couponType == null) {
            throw new IllegalArgumentException("couponType is required");
        }
        if (request.getOriginalPrice() <= MINIMUM_ORIGINAL_PRICE_EXCLUSIVE) {
            throw new IllegalArgumentException("originalPrice must be positive");
        }

        switch (couponType) {
            case PERCENTAGE -> validatePercentage(request.getValue());
            case FLAT -> validateFlat(request.getValue());
            case BOGO -> validateBogo(request.getQuantity());
            case TIERED -> { }
        }
    }
}

PercentageDiscountCalculator.java — 전략은 원시 할인액만 계산

final class PercentageDiscountCalculator implements DiscountCalculator {

    private static final BigDecimal PERCENTAGE_DIVISOR = new BigDecimal("100");
    private static final int CALCULATION_SCALE = 10;

    @Override
    public BigDecimal calculate(DiscountRequest request) {
        return BigDecimal.valueOf(request.getOriginalPrice())
            .multiply(request.getValue())
            .divide(PERCENTAGE_DIVISOR, CALCULATION_SCALE, RoundingMode.HALF_UP);
    }
}

나머지 전략도 동일하게 원시 할인액만 계산한다.

service/
├── BogoDiscountCalculator.java
├── DiscountCalculator.java
├── DiscountRequestValidator.java
├── DiscountService.java
├── FlatDiscountCalculator.java
├── PercentageDiscountCalculator.java
└── TieredDiscountCalculator.java