공공부하자개발 · 영어 학습 노트
자바
미니 프로젝트쇼핑몰 정산 시스템 4단계0/4 완료
  • 011단계: 콘솔 주문 관리 (초급 기술)
  • 022단계: 객체지향 리팩터링 (중급 기술)
  • 033단계: 모던 자바 적용 (고급 기술)
  • 044단계: 일마감 정산 배치 (배치 기술)
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 미니 프로젝트 › 03 / 4

3단계: 모던 자바 적용 (고급 기술)

섹션 6진행 0 / 4
1이번 단계의 목표2설계3구현 따라가기4실행 결과와 해설5확장 과제6다음 단계로‹ 이전다음 ›

5. 확장 과제

과제 1: 상품별 매출 상위 N — 스트림

StatisticsService에 topProducts(int n)을 추가하라. 상품별 매출액(lineTotal 합계) 내림차순 상위 n개를 Map<Product, Money>(순서 유지)로 돌려준다. 동률이면 상품 ID 오름차순.

정답 보기
java
// StatisticsService에 추가
public Map<Product, Money> topProducts(int n) {
    return orders.findAll().stream()
            .flatMap(o -> o.lines().stream())
            .collect(Collectors.groupingBy(OrderLine::product,
                    Collectors.reducing(Money.ZERO, OrderLine::lineTotal, Money::plus)))
            .entrySet().stream()
            .sorted(Map.Entry.<Product, Money>comparingByValue(Comparator.reverseOrder())
                    .thenComparing(e -> e.getKey().id()))
            .limit(n)
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
                    (a, b) -> a, LinkedHashMap::new));
}
// Main에서: stats.topProducts(3).forEach((p, s) -> System.out.println("  " + p.name() + " " + s));
// 출력 (샘플 12건 기준):
//   기계식 키보드 356,000원
//   알고리즘 도서 114,000원
//   무선 마우스 100,000원

topMembers와 구조가 같다. flatMap 한 단계가 추가됐을 뿐이다. 두 메서드의 공통 부분("Map을 값 내림차순으로 정렬해 상위 n개")을 제네릭 메서드 <K> Map<K, Money> topN(Map<K, Money>, int, Comparator<K>)으로 뽑아낼 수도 있다.

과제 2: 결제 결과에 Pending 추가

PaymentResult에 Pending(String transactionId)를 추가하라(계좌 이체가 승인 대기인 경우). PaymentGateway.charge는 BANK 타입이고 금액이 100,000원 이상이면 Pending을 돌려준다. 그 후 컴파일해 보고, 어디에서 컴파일 오류가 나는지 확인한 뒤 고쳐라.

정답 보기
java
// PaymentResult.java
public sealed interface PaymentResult
        permits PaymentResult.Approved, PaymentResult.Declined, PaymentResult.Pending {
    record Approved(String approvalCode, Money amount) implements PaymentResult {}
    record Declined(String reason) implements PaymentResult {}
    record Pending(String transactionId) implements PaymentResult {}
    default boolean isApproved() { return this instanceof Approved; }
}

// PaymentGateway.charge에 추가 (0원/한도 검사 뒤):
if (type == Type.BANK && !amount.isLessThan(Money.won(100_000)))
    return new PaymentResult.Pending("TX-" + seq.incrementAndGet());

// 컴파일 오류가 나는 곳 2군데 — "the switch expression does not cover all possible input values":
//   1) ConcurrentOrderProcessor.handle의 switch
//   2) Main 6절의 switch 표현식
// 각각 case 추가:
case PaymentResult.Pending p -> pending.incrementAndGet();          // Summary에 pending 필드 추가
case PaymentResult.Pending p -> "대기 " + p.transactionId();

이것이 sealed의 가치다. 결과 종류가 늘었을 때 처리해야 할 모든 곳을 컴파일러가 찾아 준다. 예외 기반이었다면 catch를 빠뜨린 곳은 운영 중에 발견됐을 것이다.

과제 3: 안전하지 않은 재고로 경쟁 상태 재현

Inventory와 같은 인터페이스를 갖되 내부가 HashMap + "get으로 확인 후 put으로 차감"인 UnsafeInventory를 만들어라. OrderService가 Inventory 대신 이것을 쓰게 하고(인터페이스로 추출하거나 상속), 8스레드 시나리오를 여러 번 돌려 승인 수가 60을 넘거나 재고가 음수가 되는 것을 관찰하라. 그 후 synchronized만으로 고쳐 보라.

정답 보기
java
// 1) 재현용 — 의도적으로 잘못된 구현
public class UnsafeInventory extends Inventory {
    private final java.util.Map<Long, Integer> stock = new java.util.HashMap<>();

    @Override public void put(Long id, int qty) { stock.put(id, qty); }
    @Override public int get(Long id) { return stock.getOrDefault(id, 0); }

    @Override
    public boolean tryReserve(Long id, int qty) {
        int cur = stock.getOrDefault(id, 0);      // (1) 읽기
        if (cur < qty) return false;              // (2) 확인
        Thread.yield();                           // 경쟁을 잘 보이게 다른 스레드에 양보
        stock.put(id, cur - qty);                 // (3) 쓰기 — (1)에서 읽은 값 기준. 그사이 남이 바꿨어도 모름
        return true;
    }

    @Override public void release(Long id, int qty) { stock.merge(id, qty, Integer::sum); }
}
// 8스레드 실행 예 (실행마다 다름):
//   스레드 8: 승인 71, 재고부족 29, 거절 0, 남은 재고 -3, ...

// 2) synchronized로 고치기 — 메서드 전체를 하나의 임계 구역으로
@Override
public synchronized boolean tryReserve(Long id, int qty) {
    int cur = stock.getOrDefault(id, 0);
    if (cur < qty) return false;
    stock.put(id, cur - qty);
    return true;
}

synchronized는 모든 상품의 예약을 하나의 락으로 직렬화하므로 정확하지만, 상품 A와 B의 주문이 서로를 기다린다. ConcurrentHashMap.compute는 키별로 직렬화하므로 다른 상품끼리는 병렬이다. 정확성은 같고 처리량이 다르다.

확장 과제
  • 과제 1: 상품별 매출 상위 N — 스트림
  • 과제 2: 결제 결과에 Pending 추가
  • 과제 3: 안전하지 않은 재고로 경쟁 상태 재현
이전 섹션4 실행 결과와 해설5 / 6다음 섹션6 다음 단계로