지난 주차까지 도메인-애플리케이션 레이어의 유닛 테스트를 마쳤고, 이번 주는 인프라스트럭처 구현과 통합 테스트 과제를 진행했다.
통합 테스트는 두 개 이상의 계층이 실제로 상호작용한다는 점에서 유닛 테스트와는 성격이 달랐다. 그래서 가장 먼저 고민한 건 테스트 커버리지를 어떻게 설정할 것인가였다.
이미 도메인과 서비스 레이어의 유닛 테스트는 충분히 작성되어 있었기 때문에, 중복되지 않도록 범위를 잘 구분해야 했다. 단순히 JPA가 제공해주는 조회 기능 같은 건 제외하고, 인프라스트럭처와 애플리케이션 레이어 간의 상호작용에 집중해서 테스트 케이스를 리스트업했다.
그 과정에서 파사드 계층도 테스트할 필요가 있을까? 싶은 의문이 생겼고, 실제로 몇 개 테스트 코드를 작성해 보니 결국 흐름만 체크하게 되더라. 그래서 파사드 쪽은 흐름만 확인하는 정도로 간소화하고, 핵심은 서비스 ↔ 인프라스트럭처 간 유기적인 결합이 잘 작동하는가에 집중해서 테스트를 구성했다.
테스트 환경도 따로 분리했다. 프로덕션 DB와의 의존성을 줄이기 위해 docker-compose로 MySQL 컨테이너를 구성하고, schema.sql , data.sql 로 초기 스키마 및 데이터를 세팅해 테스트를 수행했다. 이 환경 덕분에 통합 테스트를 CI 환경에서도 안정적으로 돌릴 수 있었다.
이번 주에는 쿠폰 발급과 주문 생성에 대해 의도적으로 실패하는 동시성 테스트를 구성했다. 이 테스트들의 목적은 성공 여부가 아니라, 경합 상황에서 데이터 정합성이 깨지는 구조적 결함을 드러내는 것이었다.
예를 들어, 쿠폰 발급 테스트에서는 10명의 사용자가 동시에 같은 쿠폰을 요청하고, 주문 테스트에서는 3명이 동시에 재고 10짜리 상품을 각각 5개씩 주문했다. 이런 상황에서 최대 2명만 성공해야 정상이지만, 테스트 결과에서는 초과 발급이나 재고 마이너스 같은 문제가 발생했다.
특히 동시성 테스트의 신뢰도를 확보하기 위해 @BeforeEach에서 @Transactional(propagation = Propagation.NOT_SUPPORTED)를 명시해 초기 데이터 세팅은 트랜잭션 없이 DB에 즉시 반영되도록 처리했다. 그래야 실제 테스트 스레드들이 트랜잭션 캐시 없이 공통된 실데이터를 읽고 쓰는 상황이 재현되기 때문이다. 이 설정 없이는 테스트 결과가 왜곡될 수 있다.
이번 테스트를 통해 트랜잭션 전파 방식, 데이터 커밋 타이밍, 경합 상황에서의 예외 처리 등을 전반적으로 검증할 수 있었고, 동시에 락이나 동시성 제어가 없는 상태에서는 정합성이 어떻게 무너질 수 있는지를 실증할 수 있었다. 실패는 계획된 것이었고, 그 실패를 통해 많은 걸 검증할 수 있었다.
이번 주에는 인기 상품 조회 API(/api/v1/products/popular)의 성능 병목을 사전에 분석하고, 실행 계획과 인덱스 전략을 기반으로 성능을 개선하는 작업도 함께 진행했다. 단순 구현이 아니라 데이터가 많아졌을 때 실제로 병목이 발생하는 지점을 사전에 파악하고 최적화 포인트를 확보하는 게 목적이었다.
SELECT *
FROM product_statistics
WHERE stat_date BETWEEN ? AND ?
ORDER BY sales_count DESC
LIMIT 10;
stat_date: 날짜 범위 필터sales_count DESC: 정렬 기준LIMIT 10: 상위 N개 추출이 조합은 대용량 데이터에서 전형적인 성능 저하 구조다.
초기에는 인덱스 없이 실행해서 Full Scan, filesort, post-filter 등 병목이 모두 발생했다.
stat_date 기준 인덱스를 추가해 Index Range Scan 유도CREATE INDEX idx_stat_date_sales ON product_statistics(stat_date, sales_count DESC)filesort 제거 가능성 확보CREATE INDEX idx_stat_date_sales_covering ON product_statistics(stat_date, sales_count DESC, product_id, sales_amount)| 단계 | Access 방식 | filesort | 실행 시간 |
|---|---|---|---|
| 인덱스 없음 | Full Table Scan | ✅ 발생 | 2.5ms |
| 복합 인덱스 | Index Range Scan | ⚠️ 남아있음 | 2.2ms |
| Covering Index | Index Only Scan | ⚠️ 남아있음 (매우 빠름) | 0.84ms ✅ |
Full Scan 제거 → Index Range Scan 성공테이블 접근 제거까지 성공 (Covering Index)단순히 인덱스를 "건다"는 수준이 아니라, 실제 데이터 분포와 쿼리 조건에 따라 어떤 필드 조합으로 인덱스를 구성해야 하는지를 직접 체감했다.
또한 EXPLAIN ANALYZE를 통해 실제 실행 계획을 보고, Row 수, 정렬 처리 방식, 실행 시간까지 분석하는 연습은 정확한 병목 지점 파악과 설계적 근거 확보에 굉장히 유효했다.
이번 주차는 구현을 넘어서서 설계된 구조가 실제 운영 환경에서 어떤 문제를 만들 수 있는지, 그 문제를 어떻게 미리 설계적으로 방어할 수 있는지에 대한 경험을 쌓을 수 있었던 시간이었다.
다음 주는 트랜잭션 격리 수준과 락 전략을 실제 재고 차감 로직에 적용해보며, 동시성 제어 구조를 개선할 계획이다.