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

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

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

2. 설계

2.1 전체 구조

text
┌──────────────────────────────────────────────────────────────────────┐
│ Main: 조립 + 시나리오                                                   │
│   Repository<Member,Long> = new InMemoryRepository<>(Member::id)      │
│   DiscountPolicy policy = rate(10).onlyIf(Member::isVip)              │
│                           .plus(fixed(3,000).minSubtotal(50,000))     │
└───────┬──────────────────────┬───────────────────────┬───────────────┘
        ▼                      ▼                       ▼
┌────────────────┐   ┌───────────────────┐   ┌──────────────────────────┐
│ OrderService   │   │ StatisticsService │   │ ConcurrentOrderProcessor │
│ createOrder()  │   │ topMembers(n)     │   │ ExecutorService(threads) │
│ memberNameOf() │   │ salesByCategory() │   │ Future 수집 → Summary     │
│  (Optional 체인)│   │ monthlySales()    │   │ shutdown+awaitTermination│
└──┬─────┬───┬───┘   │ bestSeller()      │   └──────┬───────────┬───────┘
   │     │   │       │ ordersOver(money) │          │           │
   │     │   │       └─────────┬─────────┘          │           │
   │     │   └──────────────┐  │                    │           │
   ▼     ▼                  ▼  ▼                    ▼           ▼
┌──────────────┐  ┌─────────────────────┐  ┌────────────┐  ┌────────────────┐
│ Inventory    │  │ Repository<T, ID>   │  │DiscountPolicy│ │ PaymentGateway │
│ ConcurrentHM │  │  save/findById(Opt) │  │ @Functional │  │ charge() →     │
│ tryReserve() │  │  findAll/count      │  │ rate/fixed  │  │ PaymentResult  │
│  = compute   │  │ InMemoryRepository  │  │ plus/onlyIf │  │  (sealed)      │
└──────────────┘  │  <T,ID>(Function)   │  │ minSubtotal │  │  Approved      │
                  └─────────────────────┘  └────────────┘  │  Declined      │
                                                           └────────────────┘
도메인 (전부 record):
  Member(id, name, grade)          Product(id, name, category, price)
  OrderLine(product, quantity)     Order(id, member, lines, orderedAt, discount)
  Money(amount)

2.2 2단계 → 3단계 변경 요약

2단계 3단계 이유
저장소 인터페이스 3개 + 구현 3개 (6파일) Repository<T, ID> + InMemoryRepository<T, ID> (2파일) 타입만 다른 중복 제거. ID 추출은 Function<T, ID> 주입
T findById(id) → null Optional<T> findById(id) "없을 수 있음"을 타입에 표시
Money final 클래스 50줄 record Money(long amount) 22줄 equals/hashCode/toString/접근자 자동 생성
Product.stock 필드 + decreaseStock Inventory 클래스 (ConcurrentHashMap) 재고는 동시에 바뀌는 값 → 불변 record에서 분리, 원자적 갱신
Order.status 가변 + markPaid Order는 불변 record, 결제 결과는 PaymentResult 값 record는 상태 전이에 부적합. 결제 결과를 주문과 분리
InsufficientBalanceException PaymentResult.Declined(reason) 흔한 결과는 값으로. sealed로 경우의 수 고정
할인 없음 DiscountPolicy 함수형 인터페이스 람다 한 줄 정책 + default 메서드 조합
for + Map.merge 스트림 collect(groupingBy(...)) 선언적 집계, 정렬/제한 체인
단일 스레드 ExecutorService + AtomicInteger 동시 주문 처리

2.3 왜 재고를 Product에서 뺐는가

record는 불변이다. Product를 record로 만들면 stock을 바꿀 수 없다. 두 선택지가 있었다.

선택 방법 문제
Product를 class로 유지 synchronized decreaseStock 상품마다 락. 동시성 규칙이 도메인 객체에 섞임
재고를 Inventory로 분리 ConcurrentHashMap<Long, Integer>.compute 상품은 순수 데이터, 재고 동시성은 한 클래스가 전담

후자를 골랐다. "동시에 바뀌는 상태"와 "바뀌지 않는 데이터"를 분리하면 불변인 쪽은 스레드 걱정 없이 공유할 수 있고, 가변인 쪽만 집중해서 보호하면 된다. 이 분리는 4단계 배치에서 "집계 상태"와 "주문 레코드"를 나누는 것과 같은 원리다.

2.4 tryReserve가 원자적인 이유

text
스레드 A: tryReserve(109, 1)         스레드 B: tryReserve(109, 1)
   │                                    │
   ├─ compute(109, fn) ── 락 획득        │
   │    cur=1 ≥ 1 → return 0            ├─ compute(109, fn) ── 대기 ...
   ├─ 락 해제 → true                     │    락 획득. cur=0 < 1 → return 0 (그대로)
   │                                    ├─ 락 해제 → false

ConcurrentHashMap.compute(key, fn)은 같은 키에 대한 fn 실행을 직렬화한다. fn 안에서 "현재값 확인 → 새 값 계산"을 하므로 두 스레드가 같은 재고를 동시에 보는 일이 없다. synchronized 블록을 직접 쓰지 않고도 check-then-act를 원자화하는 표준 기법이다.

2.5 sealed + record + switch

text
PaymentResult (sealed)
├── Approved(approvalCode, amount)   record
└── Declined(reason)                 record

switch (result) {
    case Approved a -> ...
    case Declined d -> ...
}                     ← default 없음. 세 번째 구현이 생기면 컴파일 오류로 알려 줌

2단계의 PaymentMethod 인터페이스는 "누구나 구현 가능"이 장점이었다(상품권 추가). PaymentResult는 반대로 "승인/거절 둘뿐"임을 못 박는 것이 목적이다. 열어야 할 것과 닫아야 할 것이 다르다.

설계
  • 2.1 전체 구조
  • 2.2 2단계 → 3단계 변경 요약
  • 2.3 왜 재고를 Product에서 뺐는가
  • 2.4 tryReserve가 원자적인 이유
  • 2.5 sealed + record + switch
이전 섹션1 이번 단계의 목표2 / 6다음 섹션3 구현 따라가기