4.3에서 발견한 문제 — OrderService.pay가 결제 수단을 먼저 호출하고 상태를 나중에 확인한다 — 를 고쳐라.
이미 PAID인 주문에 pay를 호출하면 결제 수단이 호출되기 전에 ValidationException("결제할 수 없는 상태: PAID")가 나야 한다. Order에 상태 확인용 메서드를 추가하고, 시나리오 4의 tryPay(service, "O-0003", card) 두 번째 호출 출력이 바뀌는지 확인하라.
// Order.java에 추가
public void ensurePayable() {
if (status != Status.CREATED) throw new ValidationException("결제할 수 없는 상태: " + status);
}
// OrderService.pay 수정
public PaymentMethod.Receipt pay(String orderId, PaymentMethod method) {
Order order = orderRepo.findById(orderId);
if (order == null) throw new NotFoundException("주문", orderId);
order.ensurePayable(); // 결제 수단 호출 전에 상태 확인
PaymentMethod.Receipt receipt = method.pay(order.total());
order.markPaid(receipt); // markPaid 안의 검사는 이중 안전장치로 유지
if (method.earnsPoint()) order.getMember().addPoint(order.total().percent(1));
return receipt;
}
// 바뀐 출력:
// 결제 실패: O-0003 [ValidationException] 결제할 수 없는 상태: PAID"외부 시스템(결제)을 호출하기 전에 내부 상태를 먼저 검증한다"는 원칙이다. 외부 호출은 되돌리기 어렵기 때문이다. 4단계 배치의 "검증 → 결제" 순서도 같은 이유다.
GiftCardPayment를 추가하라. 상품권은 번호와 잔액을 갖고, 계좌처럼 잔액 안에서만 승인되며, 승인번호 접두어는 GIFT, 포인트를 적립하지 않는다. OrderService와 Main의 결제 호출 코드는 수정하지 않고 시나리오에 한 줄만 추가해서 동작을 확인하라.
// GiftCardPayment.java
public class GiftCardPayment extends AbstractPaymentMethod {
private final String cardNo;
private Money balance;
public GiftCardPayment(String cardNo, Money balance) {
this.cardNo = cardNo;
this.balance = balance;
}
@Override public String name() { return "상품권(" + cardNo + ")"; }
@Override protected String prefix() { return "GIFT"; }
@Override public boolean earnsPoint() { return false; }
@Override
protected void validate(Money amount) {
if (balance.isLessThan(amount)) throw new InsufficientBalanceException(name(), balance, amount);
}
@Override
protected void doPay(Money amount) { balance = balance.minus(amount); }
}
// Main 시나리오에 추가 (O-0002가 취소되기 전, 4절 끝에):
// tryPay(service, "O-0002", new GiftCardPayment("G-777", Money.won(80_000)));
// 출력:
// 결제 성공: O-0002 → 상품권(G-777) 77,000원 (승인 GIFT-1004)
// (이후 5절의 cancel("O-0002")는 PAID 상태라 ValidationException으로 바뀐다)BankAccountPayment와 거의 같다. 중복이 보이면 "잔액형 결제 수단" 추상 클래스를 하나 더 끼워 넣을 수도 있지만, 두 개뿐이면 아직 이르다.
1단계 과제 3(병렬 배열로 카테고리별 매출)을 Map<String, Money>와 merge로 다시 작성하라. 결제 완료(PAID) 주문만 집계하고, 카테고리 이름 순으로 출력하라(TreeMap).
// Main에 추가
static void printSalesByCategory(OrderRepository orderRepo) {
Map<String, Money> byCategory = new java.util.TreeMap<>(); // 키 정렬
for (Order o : orderRepo.findAll()) {
if (o.getStatus() != Order.Status.PAID) continue;
for (OrderLine line : o.getLines()) {
byCategory.merge(line.getProduct().getCategory(), line.lineTotal(), Money::plus);
}
}
byCategory.forEach((category, sum) -> System.out.println("카테고리별 매출: " + category + " " + sum));
}
// 출력 (시나리오 데이터 기준):
// 카테고리별 매출: 문구 10,000원
// 카테고리별 매출: 생활 15,000원
// 카테고리별 매출: 전자 83,000원1단계 정답의 30줄이 8줄이 됐다. "이름 배열에서 위치 찾기 → 없으면 추가 → 합계 더하기"가 merge 한 줄이다. TreeMap으로 바꾸면 정렬까지 공짜다.