Full output
GPT-5.6 Luna · xhigh 원문 결과물
할인 규칙 계산 서비스 리팩터링 비교 / OpenAI / GPT-5.6 Luna / 실행일 2026-08-02
luna-xhigh
구현 결과
DiscountRequestValidator로 쿠폰 유형별 입력 검증을 한 곳으로 모았다.DiscountCalculationSupport로 나눗셈 정밀도, 원 단위HALF_UP반올림, 할인액 하한·상한 클램핑, 응답 조립을 공통화했다.- 정률·정액·BOGO·구간별 할인 계산을 각각 독립된
DiscountCalculator구현체로 분리했다. DiscountService는 검증, 전략 선택, 공통 응답 변환만 수행하며 할인 유형별 계산 분기를 제거했다.- 최대 할인율, BOGO 최소 수량, 구간 임계값과 할인율, 반올림 관련 수치를 이름 있는 상수로 추출했다.
변경 파일
작업 디렉터리: verification/work/15-discount-rule-refactor/luna-xhigh
- 수정:
src/main/java/com/samebrief/discount/service/DiscountService.java - 추가:
DiscountCalculator.java - 추가:
DiscountRequestValidator.java - 추가:
DiscountCalculationSupport.java - 추가:
PercentageDiscountCalculator.java - 추가:
FlatDiscountCalculator.java - 추가:
BogoDiscountCalculator.java - 추가:
TieredDiscountCalculator.java
총 8개 파일이며, 기존 테스트 파일은 수정하지 않았다.
검증
실행 명령:
./gradlew clean test --rerun-tasks
결과: BUILD SUCCESSFUL
DiscountServiceTest: 12개 통과DiscountControllerTest: 5개 통과HarnessSmokeTest: 1개 통과- 총 18개 테스트 통과, 실패 0개
제거한 중복과 트레이드오프
기존 DiscountService의 네 개 if 블록에 반복되던 유형별 검증, 원 단위 반올림, 할인액 클램핑, 응답 조립을 공통 컴포넌트와 전략 구현체로 이동했다. 서비스의 무인자 생성자를 유지해 기존 단위 테스트 계약도 보존했다. 검증기 내부에는 쿠폰 유형별 필수 파라미터 차이를 표현하기 위한 단일 switch가 남아 있다.
코드 산출물
아래는 설명만 읽지 않고 리팩터링 전후의 실제 핵심 코드를 확인할 수 있도록 공개한 것이다. 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));
long discountedPrice = originalPrice - discountAmount;
return new DiscountResponse(couponType, originalPrice, discountAmount, discountedPrice);
}
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));
long discountedPrice = originalPrice - discountAmount;
return new DiscountResponse(couponType, originalPrice, discountAmount, discountedPrice);
}
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));
long discountedPrice = originalPrice - discountAmount;
return new DiscountResponse(couponType, originalPrice, discountAmount, discountedPrice);
}
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));
long discountedPrice = originalPrice - discountAmount;
return new DiscountResponse(couponType, originalPrice, discountAmount, discountedPrice);
}
throw new IllegalArgumentException("unsupported couponType: " + couponType);
}
}
Luna xhigh 결과
DiscountService.java — 검증·전략 선택·공통 응답 변환만 담당
@Service
public class DiscountService {
private final DiscountRequestValidator requestValidator;
private final Map<CouponType, DiscountCalculator> calculators;
public DiscountService() {
this(new DiscountRequestValidator(), defaultCalculators());
}
DiscountService(DiscountRequestValidator requestValidator, Map<CouponType, DiscountCalculator> calculators) {
this.requestValidator = requestValidator;
this.calculators = Map.copyOf(calculators);
}
public DiscountResponse calculate(DiscountRequest request) {
requestValidator.validate(request);
CouponType couponType = request.getCouponType();
DiscountCalculator calculator = Optional.ofNullable(calculators.get(couponType))
.orElseThrow(() -> new IllegalArgumentException("unsupported couponType: " + couponType));
return DiscountCalculationSupport.toResponse(
couponType,
request.getOriginalPrice(),
calculator.calculate(request)
);
}
private static Map<CouponType, DiscountCalculator> defaultCalculators() {
return Map.of(
CouponType.PERCENTAGE, new PercentageDiscountCalculator(),
CouponType.FLAT, new FlatDiscountCalculator(),
CouponType.BOGO, new BogoDiscountCalculator(),
CouponType.TIERED, new TieredDiscountCalculator()
);
}
}
DiscountRequestValidator.java — 네 유형의 입력 검증을 한 곳에서 담당
final class DiscountRequestValidator {
private static final BigDecimal ZERO = BigDecimal.ZERO;
private static final BigDecimal MAX_PERCENTAGE = BigDecimal.valueOf(80);
private static final int MIN_BOGO_QUANTITY = 2;
void validate(DiscountRequest request) {
if (request == null) {
throw new IllegalArgumentException("request is required");
}
CouponType couponType = request.getCouponType();
if (couponType == null) {
throw new IllegalArgumentException("couponType is required");
}
if (request.getOriginalPrice() <= 0) {
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 -> { }
}
}
}
DiscountCalculationSupport.java — 중간 나눗셈과 최종 원 단위 정규화를 공통 처리
final class DiscountCalculationSupport {
private static final int DIVISION_SCALE = 10;
private static final int CURRENCY_SCALE = 0;
private static final long MIN_DISCOUNT_AMOUNT = 0;
private static final RoundingMode ROUNDING_MODE = RoundingMode.HALF_UP;
static BigDecimal divide(BigDecimal dividend, BigDecimal divisor) {
return dividend.divide(divisor, DIVISION_SCALE, ROUNDING_MODE);
}
static DiscountResponse toResponse(CouponType couponType, long originalPrice, BigDecimal rawDiscountAmount) {
long discountAmount = rawDiscountAmount
.setScale(CURRENCY_SCALE, ROUNDING_MODE)
.longValue();
long boundedDiscountAmount = Math.max(
MIN_DISCOUNT_AMOUNT,
Math.min(discountAmount, originalPrice)
);
return new DiscountResponse(
couponType,
originalPrice,
boundedDiscountAmount,
originalPrice - boundedDiscountAmount
);
}
}
나머지 전략 클래스는 각각 원시 할인액만 계산한다.
service/
├── BogoDiscountCalculator.java
├── DiscountCalculationSupport.java
├── DiscountCalculator.java
├── DiscountRequestValidator.java
├── DiscountService.java
├── FlatDiscountCalculator.java
├── PercentageDiscountCalculator.java
└── TieredDiscountCalculator.java