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

2단계: 객체지향 리팩터링 (중급 기술)

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

4. 실행 결과와 해설

4.1 Money — ==와 equals, 그리고 HashSet

text
a == b ? false, a.equals(b) ? true
HashSet 크기: 2 (10,000원 두 개는 하나로)

a와 b는 Money.won(10_000)을 두 번 호출한 서로 다른 객체라 ==는 false다. equals를 재정의했으므로 true. HashSet에 a, b, c를 넣으면 2개만 남는다 — hashCode가 같은 값을 돌려주고(Objects.hash(amount)) equals가 true이기 때문이다.

equals만 재정의하고 hashCode를 빼먹었다면 크기가 3이었을 것이다. 컬렉션 레슨의 규약이 실제로 작동하는 장면이다.

[ValidationException] 금액은 음수일 수 없습니다: -2000은 Money.minus가 아니라 생성자에서 던진 것이다. minus는 new Money(3000 - 5000)을 시도했을 뿐이다. 검증을 생성자 한 곳에 두면 모든 연산이 자동으로 보호된다.

4.2 주문 생성 — 다섯 종류의 실패

출력 던진 곳 잡은 곳
[재고 부족] 자바 입문서 3/5 Product.ensureStock catch (OutOfStockException e) — 필드 3개를 직접 꺼냄
[NotFoundException] 회원을(를) 찾을 수 없습니다: 9 OrderService.createOrder catch (ShopException e)
[NotFoundException] 상품을(를) 찾을 수 없습니다: 999 OrderService.createOrder 검증 루프 catch (ShopException e)
[ValidationException] 수량은 1 이상이어야 합니다: 0 OrderLine 생성자 catch (ShopException e)

tryCreate의 catch 순서에 주목하라. OutOfStockException을 먼저, ShopException을 나중에 잡는다. 순서를 바꾸면 컴파일 오류다(부모를 먼저 잡으면 자식 catch에 도달할 수 없다). 그리고 1단계에서는 실패했던 4번째 회원(최지우)이 O-0004를 만든다. LinkedHashMap에는 크기 제한이 없다.

999 실패 후 남은 재고: 무선 마우스=7은 1단계와 같은 값이다. ensureStock(검증)과 decreaseStock(차감)을 두 루프로 나눈 것이 여전히 "전부 아니면 전무"를 지키고 있다.

4.3 결제 — 같은 호출, 다른 결과

text
결제 성공: O-0001 → 카드(1234-****) 58,000원 (승인 CARD-1001)
결제 실패: O-0002 [InsufficientBalanceException] 계좌(110-222) 잔액 부족: 사용 가능 50,000원, 필요 77,000원
결제 실패: O-0002 [InsufficientBalanceException] 포인트(이영희) 잔액 부족: 사용 가능 12,000원, 필요 77,000원
결제 실패: O-0002 [InsufficientBalanceException] 카드(1234-****) 잔액 부족: 사용 가능 42,000원, 필요 77,000원

tryPay(service, "O-0002", X)를 세 번 부르는데 X만 다르다. OrderService.pay는 한 줄도 안 바뀌었는데 메시지의 "계좌/포인트/카드"와 가용액이 달라진다. 각 구현체의 validate가 자기 기준으로 검사했기 때문이다. 세 번째 줄의 사용 가능 42,000원은 카드 한도 100,000 − 첫 결제 58,000이다. CardPayment.used가 상태를 기억하고 있다.

O-0002는 결국 결제되지 않아 CREATED로 남고, 5절에서 취소된다.

text
결제 실패: O-0003 [InsufficientBalanceException] 카드(1234-****) 잔액 부족: 사용 가능 17,000원, 필요 25,000원

같은 O-0003을 카드로 두 번 결제하는 시나리오다. 기대한 실패는 Order.markPaid의 "결제할 수 없는 상태"였지만, 실제로는 카드 한도(17,000)가 먼저 걸렸다. pay의 순서가 method.pay(...) → order.markPaid(...)이기 때문이다.

이것은 버그다. 상태 검사를 결제보다 먼저 해야 한다 — 그렇지 않으면 이미 결제된 주문에 대해 카드가 한 번 더 긁힐 수 있다. 확장 과제 1에서 고친다. 실행 결과를 코드와 대조하며 읽을 때 이런 순서 문제가 보인다.

4.4 승인번호와 포인트

승인번호가 CARD-1001 → CARD-1002 → BANK-1003으로 이어진다. AbstractPaymentMethod.approvalSeq가 static이라 수단을 넘나들며 하나의 번호대를 쓴다.

text
회원별 매출: 김철수 58,000원 (포인트 5,580원)
회원별 매출: 최지우 25,000원 (포인트 3,250원)

김철수 5,000 + 580(카드 1%), 최지우 3,000 + 250(계좌 1%). 포인트 결제가 성공한 사례는 없지만, 있었다면 earnsPoint()가 false라 적립이 없었을 것이다. OrderService.pay의 if (method.earnsPoint()) 분기가 그 자리다.

4.5 취소와 재고 복구

text
취소 전 재고: 2, 16
[ValidationException] 결제 완료된 주문은 취소할 수 없습니다: O-0001
취소 후 재고: 3, 19

O-0002(자바 입문서 1, 텀블러 3)를 취소하니 103 재고 2→3, 104 재고 16→19. OrderService.cancel의 increaseStock 루프다.

O-0001은 PAID라 ValidationException. 상태 전이 규칙(R8)이 Order와 OrderService에 나뉘어 있는데, Order.cancel()은 "이미 취소됨"만, 서비스는 "결제됨"을 막는다. 규칙이 두 곳에 있는 것은 아쉽다 — 3단계에서 상태 전이를 정리한다.

4.6 집계 — 1단계 이중 루프와 비교

1단계 printSalesByMember는 회원 × 주문 이중 루프였다. 2단계는 주문을 한 번 돌면서 salesByMember.merge(o.getMember(), o.total(), Money::plus)로 회원별 합계를 쌓고, 출력할 때 getOrDefault(m, Money.ZERO)로 꺼낸다.

키가 Member 객체인데도 동작하는 것은 Member.equals/hashCode를 ID 기준으로 재정의했기 때문이다. Money::plus는 메서드 참조 — 고급 레슨에서 배우지만 merge의 세 번째 인자로 자연스럽게 등장한다.

취소된 O-0002는 if (o.getStatus() != Order.Status.PAID) continue;로 빠지므로 총 매출은 58,000 + 25,000 + 25,000 = 108,000이다.

4.7 1단계 대비 무엇이 좋아졌는가

관점 1단계 2단계 효과
저장 Member[3] + 카운터 LinkedHashMap<Long, Member> 크기 제한 없음, 조회 O(1), 확장 코드 0줄
조회 실패 return null → 호출자가 검사 throw new NotFoundException 검사를 잊어도 조용히 지나가지 않음
실패 종류 문자열 메시지만 예외 타입 4종 + 부모 호출자가 종류별 처리 가능, 부가 정보 접근
주문 항목 int[] × 2 (병렬 배열) List<OrderLine> 항목 수 제한 없음, 동기화 문제 없음
합계 계산 createOrder와 orderTotal에 중복 Order.total() 하나 공식 변경 시 한 곳
금액 int + format() Money (불변, 검증, toString) 음수 불가, 오버플로 완화, 서식 일관
결제 없음 인터페이스 + 구현 3개 수단 추가 시 서비스 무변경
재고 검증 Main이 getStock() 비교 Product.ensureStock() 검증 없는 차감 경로 제거
테스트 가능성 static 배열 — 불가 생성자 주입 — 가짜 저장소 가능 단위 테스트 가능
코드량 4파일 ≈ 270줄 23파일 ≈ 625줄 2.3배. 파일 수 6배

마지막 행이 솔직한 대가다. 코드는 늘었다. 그러나 늘어난 코드의 대부분은 "규칙을 한 곳에 적은 것"이고, 1단계는 그 규칙이 Main의 if문 속에 흩어져 있거나 아예 없었다.

실행 결과와 해설
  • 4.1 Money — ==와 equals, 그리고 HashSet
  • 4.2 주문 생성 — 다섯 종류의 실패
  • 4.3 결제 — 같은 호출, 다른 결과
  • 4.4 승인번호와 포인트
  • 4.5 취소와 재고 복구
  • 4.6 집계 — 1단계 이중 루프와 비교
  • 4.7 1단계 대비 무엇이 좋아졌는가
이전 섹션3 구현 따라가기4 / 6다음 섹션5 확장 과제