Conversation
요구사항, 시퀀스다이어그램, 클래스다이어그램, ERD 개선
기존 Example/Member 도메인을 제거하고, 이커머스 핵심 도메인(User, Brand, Product, Like, Cart, Order, Stats)을 Layered Architecture(interfaces → application → domain ← infrastructure) 패턴으로 구현. 주요 변경사항: - User: 회원 가입/로그인/비밀번호 변경 (BaseStringIdEntity 기반 UUID PK) - Brand: 브랜드 CRUD, 소프트 삭제, display_status 관리 - Product: 상품 CRUD, revision 이력 관리, sale_status, 재고(CAS 기반 hold/release) - Like: 상품 좋아요 등록/취소 (멱등, 복합 PK) - Cart: 장바구니 CRUD, 주문 연계 복원 (멱등성 보장) - Order: 주문 생성(DIRECT/CART), 취소, 만료 스케줄러 (상태 머신 패턴) - Stats: 운영 통계 (주문 현황, 인기 상품) - Admin API: 관리자 전용 엔드포인트 (AdminAuthInterceptor 기반 인증) - 설계 문서: 아키텍처, 클래스 다이어그램, ERD, 레이어 구성 문서 추가 - 전체 레이어별 단위/통합/E2E 테스트 작성
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📌 Summary
아키텍처 구성도
멘토링 이전
멘토링 이후
graph TB subgraph INTERFACES["1. 인터페이스 레이어 (interfaces)"] direction LR API["<b>고객 API</b><br/>UserV1Controller<br/>BrandV1Controller<br/>ProductV1Controller<br/>LikeV1Controller<br/>CartV1Controller<br/>OrderV1Controller"] ADMIN["<b>관리자 API</b><br/>AdminBrandV1Controller<br/>AdminProductV1Controller<br/>AdminOrderV1Controller<br/>AdminCartV1Controller<br/>AdminStatsV1Controller<br/>AdminAuthInterceptor"] DTO["<b>공통</b><br/>ApiResponse<T><br/>PageResponse<T><br/>ApiControllerAdvice<br/>WebMvcConfig<br/>*V1Dto (Request/Response)"] end subgraph APPLICATION["2. 애플리케이션 레이어 (application)"] direction LR FACADE["<b>Facade (복잡한 도메인)</b><br/>ProductFacade<br/>CartFacade<br/>OrderFacade"] APPSERVICE["<b>AppService (단순 도메인)</b><br/>UserAppService<br/>BrandAppService<br/>LikeAppService<br/>StatsAppService<br/><b>AppService (복잡한 도메인 보조)</b><br/>ProductAppService<br/>CartAppService<br/>OrderAppService"] INFO["<b>Info DTO</b><br/>UserInfo<br/>BrandInfo<br/>ProductInfo / ProductRevisionInfo<br/>LikeInfo<br/>CartInfo<br/>OrderInfo<br/>StatsInfo"] end subgraph DOMAIN["3. 도메인 레이어 (domain)"] direction LR SERVICE["<b>Service</b><br/>UserService<br/>BrandService<br/>ProductService<br/>StockService<br/>LikeService<br/>CartService<br/>OrderService<br/>StatsService"] MODEL["<b>Model (Entity)</b><br/>UserModel<br/>BrandModel<br/>ProductModel<br/>ProductStockModel<br/>ProductRevisionModel<br/>LikeModel<br/>CartItemModel<br/>OrderModel<br/>OrderItemModel<br/>OrderCartRestoreModel"] REPO_IF["<b>Repository (Interface)</b><br/>UserRepository<br/>BrandRepository<br/>ProductRepository<br/>ProductStockRepository<br/>ProductRevisionRepository<br/>LikeRepository<br/>CartItemRepository<br/>OrderRepository<br/>OrderItemRepository<br/>OrderCartRestoreRepository<br/>StatsRepository"] end subgraph INFRA["4. 인프라스트럭처 레이어 (infrastructure)"] direction LR REPO_IMPL["<b>RepositoryImpl</b><br/>UserRepositoryImpl<br/>BrandRepositoryImpl<br/>ProductRepositoryImpl<br/>ProductStockRepositoryImpl<br/>ProductRevisionRepositoryImpl<br/>LikeRepositoryImpl<br/>CartItemRepositoryImpl<br/>OrderRepositoryImpl<br/>OrderItemRepositoryImpl<br/>OrderCartRestoreRepositoryImpl<br/>StatsRepositoryImpl"] JPA_REPO["<b>JpaRepository</b><br/>UserJpaRepository<br/>BrandJpaRepository<br/>ProductJpaRepository<br/>ProductStockJpaRepository<br/>(CAS UPDATE @Query)<br/>ProductRevisionJpaRepository<br/>LikeJpaRepository<br/>CartItemJpaRepository<br/>OrderJpaRepository<br/>(CAS 상태 전이 @Query)<br/>OrderItemJpaRepository<br/>OrderCartRestoreJpaRepository"] end subgraph SUPPORT["5. 서포트 (support)"] direction LR ERR["CoreException<br/>ErrorType (enum)<br/>GlobalExceptionHandler"] ENUM["DisplayStatus<br/>ProductSaleStatus<br/>ProductSortType<br/>OrderType / OrderStatus<br/>ProductRevisionAction<br/>UnavailableReason<br/>RestoreReason / RestoreTriggerSource"] UTIL["ProductAvailabilityChecker"] end subgraph BATCH["6. 배치 (batch)"] SCHED["OrderExpiryScheduler<br/>(1분 주기 만료 처리)"] end %% 의존 방향 (depends on) API -->|depends on| APPSERVICE API -->|depends on| FACADE ADMIN -->|depends on| APPSERVICE ADMIN -->|depends on| FACADE DTO -->|depends on| INFO APPSERVICE -->|depends on| SERVICE FACADE -->|depends on| SERVICE SERVICE -->|depends on| REPO_IF SERVICE -->|depends on| MODEL BATCH -->|depends on| FACADE %% 구현 관계 (implements) REPO_IMPL -.->|implements| REPO_IF JPA_REPO -.->|delegates| REPO_IMPL %% 인프라 → 도메인 Model 의존 REPO_IMPL -->|depends on| MODEL JPA_REPO -->|depends on| MODEL %% 스타일 style INTERFACES fill:#4a90d9,color:#fff,stroke:#2c5f8a style APPLICATION fill:#7bc96f,color:#fff,stroke:#4a8a3f style DOMAIN fill:#f5a623,color:#fff,stroke:#c47d12 style INFRA fill:#9b59b6,color:#fff,stroke:#6c3483 style SUPPORT fill:#95a5a6,color:#fff,stroke:#7f8c8d style BATCH fill:#e74c3c,color:#fff,stroke:#c0392b리뷰 포인트
저번 멘토링 이후, 제가 지금까지 익혀 온 구현 방식과 표현 방식이 적절한지 다시 점검해보는 시간을 가졌습니다.
제가 이해한 애플리케이션 레이어는 “사용자의 업무 시나리오(UseCase)를 실행시키는 오케스트레이터 레이어”입니다.
그리고 단순한 로직의 경우에는 굳이 애플리케이션 레이어를 두지 않고, 인터페이스(Controller)에서 도메인 서비스를 호출한 뒤 응답 DTO로 변환해 반환해도 된다고 생각했었습니다.
이 관점을 가지고 프로젝트를 구현해보니, 아래 고민이 생겼습니다.
(이때 변환 DTO를 도메인 쪽에 두는 방식도 일부 사용했습니다.)
멘토링 이후에는, 애플리케이션 레이어를 “애플리케이션 레이어답게” 설계해보자는 방향으로 개선해보았습니다.
다만 이렇게 구조를 바꿔 구현을 진행하다 보니, 다시 다음 궁금증이 생겼습니다.
이번 멘토링에서는 위 고민을 중심으로, 제가 구현한 애플리케이션 레이어 소스코드를 기준으로
현재 구조의 장단점과 개선 방향(필요시 유지/분리/축소 기준) 을 리뷰받고 싶습니다.
감사합니다.
🧭 Context & Decision
문제 정의
선택지와 결정
1. 재고 관리 동시성 전략
SELECT … FOR UPDATE) — 단순하지만 락 대기로 처리량 저하 우려. 트랜잭션이 길어질수록 병목이 심해진다.hold:UPDATE stock SET reserved = reserved + :qty WHERE product_id = :id AND (on_hand - reserved) >= :qtyrelease:UPDATE stock SET reserved = reserved - :qty WHERE product_id = :id AND reserved >= :qtyCoreException(STOCK_NOT_ENOUGH)즉시 발생2. Application 레이어 패턴 (AppService vs Facade)
Controller → AppService → ServiceController → Facade(생성/취소 등) + AppService(조회/삭제 등) → 여러 Service3. 엔티티 PK 전략 (BaseStringIdEntity)
BaseStringIdEntity적용.@UuidGenerator로 36자 UUID 자동 생성. PK 컬럼명은 서브클래스에서 직접 정의(user_id, brand_id, product_id, order_id).4. 주문 상태 머신 설계
OrderStatusenum에PENDING_PAYMENT,CANCELLED,EXPIRED3개 상태만 정의.UPDATE orders SET status = :to WHERE order_id = :id AND status = :fromcanCancel()헬퍼로 전이 가능 여부를 enum에 캡슐화.OrderExpiryScheduler가 1분 주기로expiresAt < NOW()대상을 조회하여EXPIRED로 전이.PAID,PAYMENT_FAILED상태를 추가하고, PG 웹훅 연동 시SHIPPED,DELIVERED확장 가능.5. 장바구니 복원 멱등성
DataIntegrityViolationException— 중복 INSERT 시 예외를 잡아 무시. DB 예외에 의존하므로 의도가 불명확하다.order_cart_restore테이블existsById명시적 확인 — 복원 전 이력 존재 여부를 먼저 확인하고, 없으면 복원 + 이력 INSERT.OrderCartRestoreModel의 PK를orderId로 설정하여 주문당 1회 복원을 DB 레벨에서 보장. DIRECT 주문만 복원 대상 (CART 주문은 장바구니 유지).RestoreReason(USER_CANCELLED, EXPIRED 등)과RestoreTriggerSource(CANCEL_API, EXPIRE_JOB 등)를 기록하여 추적성도 확보했다.6. 소프트 삭제와 연쇄 처리
softDelete()시 해당 브랜드의 모든 상품도 연쇄 소프트 삭제.del_yn='Y'+deletedAt이중 관리로 삭제 여부를 명확히 표현. 복구(restore())는 멱등 처리.del_yn='N' AND display_status='ACTIVE'로 필터링하여 삭제/숨김 상품이 노출되지 않도록 보장.7. 테스트 전략
*Test): Mockito + AssertJ. 비즈니스 규칙 검증에 집중.*RepositoryImplTest):@DataJpaTest+ Testcontainers MySQL. 실제 SQL 동작 검증.*E2ETest):@SpringBootTest+ MockMvc +@Transactional. 전체 레이어 관통 테스트.StockConcurrencyTest):ExecutorService+CountDownLatch로 CAS 재고 경합 시나리오 검증.FullOrderFlowIntegrationTest): 좋아요 → 장바구니 → 주문 → 취소 전체 흐름을 하나의 시나리오로 검증.🏗️ Design Overview
변경 범위
apps/commerce-api— 132 파일 신규/수정 (프로덕션), 54 파일 (테스트)modules/jpa,modules/redis,modules/kafka— 20 파일 수정 (인프라 설정, BaseEntity 개선)supports/jackson,supports/logging,supports/monitoring— 13 파일 수정docs/design/— 설계 문서 8 파일 추가/수정BaseStringIdEntity— UUID 기반 String PK 엔티티 베이스 클래스/api-admin/v1) — 브랜드·상품·장바구니·주문·통계 관리OrderExpiryScheduler— PENDING_PAYMENT 주문 만료 배치ProductAvailabilityChecker— 장바구니 항목 주문 가능 여부 판별 유틸주요 컴포넌트 책임
도메인 모델 & 서비스
UserModel/UserService: 회원가입, 인증, 비밀번호 변경. 이름 마스킹, 비밀번호 규칙 검증을 도메인 모델에 캡슐화BrandModel/BrandService: 브랜드 CRUD, 소프트 삭제. display_status 기반 노출 관리ProductModel/ProductService: 상품 CRUD, sale_status 관리, 수정/삭제/복구 시 ProductRevision 이력 자동 기록ProductStockModel/StockService: CAS 기반 재고 hold/release. 오버셀 방지 조건부 UPDATELikeModel/LikeService: 좋아요 등록/취소 (멱등). 복합 PK(userId + productId)CartItemModel/CartService: 장바구니 CRUD, 수량 제한(최대 10개). 복합 PK(userId + productId)OrderModel/OrderService: 주문 생성(DIRECT/CART), 취소, 만료. 상태 머신(PENDING_PAYMENT → CANCELLED/EXPIRED)StatsService: 주문 현황·인기 상품 집계 쿼리 (Projection → Info 변환)애플리케이션 레이어
UserAppService,BrandAppService,LikeAppService,StatsAppService: 단순 도메인 — 단일 서비스 호출 + Model → Info 변환ProductFacade,CartFacade,OrderFacade: 복잡한 도메인 — 여러 서비스 조합(재고 hold/release, 장바구니 복원 등)인프라스트럭처
*RepositoryImpl→*JpaRepository위임. 도메인 Repository 인터페이스 구현BCryptPasswordEncoder: 도메인PasswordEncoder인터페이스의 BCrypt 구현체 (DIP)지원 모듈
ErrorTypeenum 확장: 도메인별 에러 코드 29종 정의UnavailableReasonenum: 장바구니 항목 주문 불가 사유를 정적 팩토리로 판별OrderStatus/ProductSaleStatus: 상태 전이 규칙을 enum에 캡슐화 (canCancel(),isOrderable())🔁 Flow Diagram
Main Flow — 주문 생성 (DIRECT)
Main Flow — 주문 취소 (장바구니 복원 포함)
sequenceDiagram autonumber participant Client participant Controller as OrderV1Controller participant Facade as OrderFacade participant OrderSvc as OrderService participant StockSvc as StockService participant CartSvc as CartService participant DB Client->>Controller: POST /api/v1/orders/{id}/cancel Controller->>Facade: cancelOrder(orderId, userId) Facade->>OrderSvc: cancel(orderId) [CAS 상태 전이] OrderSvc->>DB: UPDATE order SET status = CANCELLED WHERE status = PENDING_PAYMENT DB-->>OrderSvc: OrderModel Facade->>StockSvc: release(productId, qty) [CAS] StockSvc->>DB: UPDATE stock SET reserved -= qty WHERE reserved >= qty Facade->>CartSvc: restore (DIRECT 주문만, 멱등) CartSvc->>DB: INSERT cart_item (if not exists in order_cart_restore) Facade-->>Controller: void Controller-->>Client: ApiResponse<success>좋아요 → 장바구니 → 주문 전체 흐름
flowchart LR A[좋아요 등록] --> B[장바구니 담기] B --> C{주문 유형} C -->|DIRECT| D[바로 주문] C -->|CART| E[장바구니 주문] D --> F[재고 hold - CAS] E --> F F --> G[PENDING_PAYMENT] G -->|사용자 취소| H[CANCELLED] G -->|배치 만료| I[EXPIRED] H --> J[재고 release + 장바구니 복원] I --> J