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