- 이번 프로젝트에서 주문(Order), 배송지(Address) 도메인 개발 및 통합 테스트를 담당하며 겪었던 고민과 성과, 그리고 튜터님의 피드백을 바탕으로 깨달은 점들을 정리한다.
🎯 1. 나의 핵심 구현 성과 (Keep)
- 내가 담당한 파트에서는 기능 작동을 넘어 '데이터 무결성'과 '보안 및 예외 상황 방어'에 가장 큰 공을 들였다.
📦 Order & Address 도메인 (비즈니스 및 상태 로직)
- 데이터 무결성 보장을 위한 스냅샷 패턴
- 메뉴 가격은 언제든 변동될 수 있다. 주문 시점의 가격을 OrderItem의 priceAtOrder 필드에 복사해 두는 스냅샷 방식을 적용하여, 추후 메뉴 가격이 오르더라도 과거 결제 데이터가 훼손되지 않도록 방어했다.
- 시간 의존적 비즈니스 룰 (5분 취소 로직)
- 고객은 주문 후 5분 이내에만 취소할 수 있다는 룰을 적용했다. Java의 Duration.between(createdAt, now)을 이용해 정확히 시간을 계산하고, 상태가 PENDING일 때만 취소를 허용하는 선제적 상태 가드 로직을 구현했다.
- 배송지 Soft Delete 및 연관관계 보호
- 주문 데이터는 배송지 정보를 바라본다. 배송지 삭제 시 DB에서 하드 딜리트(Hard Delete)를 하면 과거 주문 내역의 정합성이 깨지므로, isDeleted=true 처리하는 Soft Delete를 적용했다.
- 또한, 새로운 배송지를 기본으로 설정하면 기존 배송지를 찾아 자동으로 false로 바꿔주는 편의성 로직도 추가했다.
🧪 Integration Test (통합 테스트 아키텍처)
- Repository Fixture 기반 테스트 최적화
- 기존 통합 테스트는 기초 데이터(Given)를 세팅하기 위해 무수한 API(MockMvc)를 호출하여 너무 무거웠다.
- 이를 개선하기 위해 테스트 데이터는 Repository를 통해 Given 데이터를 직접 주입하는 Fixture 구조로 리팩토링하여 가독성과 테스트 속도를 개선했다.
- ReflectionTestUtils를 활용한 시간 조작 테스트
- "5분 경과 시 주문 취소 불가" 로직을 검증하기 위해 5분을 실제로 기다릴 수 없었다. 스프링의 ReflectionTestUtils를 활용해 엔티티의 createdAt을 강제로 6분 전으로 조작한 뒤 API를 호출하여 400 Bad Request 예외가 정상적으로 터지는지 검증했다.
🚧 2. 내가 구현한 코드의 한계점 (Problem)
- 실시간 인가(Authorization) 가드의 딜레마 (DB 병목 우려)
- 결제/주문 도메인의 무결성을 지키기 위해, JWT 토큰만 믿지 않고 매 요청마다 validateUserRoleFromDB를 호출하여 강제 탈퇴되거나 권한이 강등된 유저를 실시간으로 차단했다.
- 문제점: 기능적으로는 안전했지만, 대규모 트래픽 환경에서는 매 요청마다 발생하는 PK Select 쿼리가 병목이 될 가능성이 있다고 판단했다.
🧠 3. 부족했던 개념 및 새롭게 배운 점 (Learned / 피드백 기반)
- 튜터님의 피드백을 내 담당 파트와 그 외 시스템 아키텍처 파트로 나누어 분석해보았다.
💡 A. 내 담당 파트 (Order/Address)에 직결되는 피드백
- DB 조회 병목 현상과 인메모리 캐싱 (Redis)
- validateUserRoleFromDB 과정에서 발생하는 DB 조회 병목을 개선하기 위해 Redis 기반 캐싱 전략의 필요성을 느꼈다. 실시간 권한 정보나 토큰 상태를 매 요청마다 DB에서 조회하기보다 Redis와 같은 캐시에 올려두고 검증해야 대규모 트래픽을 견딜 수 있다는 점을 배웠다.
- MSA 전환을 고려한 도메인 결합도 낮추기 (DDD)
- 현재 OrderService에서 User 엔티티나 UserRepository를 직접 import하여 사용하고 있다. 모놀리식에서는 편하지만, 훗날 주문 서버와 회원 서버를 분리(MSA)할 때 큰 기술 부채가 된다. 도메인 간 참조는 엔티티가 아닌 식별자(ID)로 하거나, DTO/이벤트를 통한 통신으로 결합(Coupling)을 느슨하게 해야 함을 인지했다.
💡 B. 타 도메인 및 전체 아키텍처에 대한 피드백
- 내가 직접 짜진 않았지만 백엔드 개발자로서 추가로 학습이 필요하다고 느낀 인프라/보안 개념들이다.
- 외부 API (AI) 연동 시 회복 탄력성 (Resilience)
- 외부 API가 죽었다고 우리 비즈니스가 죽으면 안 된다. Resilience4j(서킷 브레이커)를 도입하여 장애를 격리하고, AI 호출이 실패해도 기본 응답으로 대체하는 폴백(Graceful Degradation) 처리가 필요하다는 점을 배웠다.
- 무거운 블랙리스트를 대체하는 JWT 토큰 버저닝 (Token Versioning)
- JWT 탈취 방지나 강제 만료를 위해 썼던 블랙리스트 방식은 메모리 부하가 심하다. 대신 DB 유저 테이블에 token_version을 두고 JWT 클레임과 비교하는 방식이 상태 동기화 유지에 훨씬 가볍고 안전하다.
- QueryDSL 최적화와 EXPLAIN 검증
- 코드로 조인(Join)을 줄였다고 끝이 아니라, 실제로 DB 콘솔에서 EXPLAIN 실행 계획을 까보고 인덱스를 제대로 타는지 눈으로 확인하고 수치로 증명하는 습관이 중요하다는 점을 배웠다.
🎯 4. 다음을 위한 보완 계획 (Try / Action Items)
- 이번 프로젝트의 피드백을 바탕으로, 다음 프로젝트나 리팩토링을 위해 아래 내용을 숙지해볼 것이다.
- Redis 인가 캐싱 구현: validateUserRoleFromDB 과정에서 매 요청마다 발생하는 DB 조회를 줄이기 위해, Redis 기반 캐싱 전략도 고려해볼 예정이다.
- 도메인 분리 리팩토링: Order와 User 도메인이 서로의 엔티티를 직접 참조하지 않도록 의존성을 끊어내고, 필요한 검증은 인터페이스를 통하도록 개선하기.
- EXPLAIN 결과 문서화: QueryDSL로 작성된 검색 쿼리들의 실행 계획을 캡처하여 인덱스 스캔 여부를 TIL로 꼼꼼히 기록하는 습관 들이기.
- 로깅 레벨업: System.out.println을 모두 없애고 Slf4j를 활용한 목적별 로깅, 그리고 AI 호출 시 상세한 상태(SUCCESS, FAIL, TIMEOUT)를 남기는 로그 테이블 설계 도입하기.