이번 포스트에서는 실제 프로젝트에서 데이터베이스 성능 최적화를 통해 TPS를 144.6에서 151로 향상시킨 과정을 공유해보려고 한다. 작은 개선처럼 보이지만, 실제로는 응답시간을 18%나 단축시키는 의미있는 성과였다.
🔍 성능 문제 진단
1. N+1 쿼리 문제 발견
가장 먼저 발견한 문제는 인기 상품 목록 조회 시 발생하는 N+1 쿼리 문제였다.
문제 상황:
- 인기 상품 100개를 조회할 때
- 각 상품의 카테고리 정보를 가져오기 위해 추가로 100번의 쿼리가 발생
- 총 101번의 쿼리 실행 (1번의 상품 조회 + 100번의 카테고리 조회)
해결 방법:
// 기존 코드 (N+1 문제 발생)
@Query("SELECT p FROM Product p ORDER BY p.ranking ASC")
List<Product> findTop100ByOrderByRankingAsc(Pageable pageable);
// 개선된 코드 (JOIN FETCH 사용)
@Query("SELECT p FROM Product p JOIN FETCH p.category ORDER BY p.ranking ASC")
List<Product> findTop100WithCategory(Pageable pageable);
이렇게 JOIN FETCH를 사용해서 상품과 카테고리 정보를 한 번의 쿼리로 가져오도록 개선했다.
2. 배치 처리 최적화
두 번째로 개선한 부분은 여러 데이터를 한 번에 저장할 때의 비효율성이었다.
문제 상황:
- 주문 시 여러 상품을 동시에 처리할 때
- 각 INSERT 쿼리가 개별적으로 전송됨
- 네트워크 오버헤드 발생
해결 방법:
# application.properties에 Hibernate 배치 처리 옵션 추가
spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true
이 설정으로 Hibernate가 쿼리를 모아서 한 번에 전송하도록 개선했다.
📊 성능 테스트 결과
최적화 이전 결과 (144.6 TPS)
/image.png)
최적화 이후 결과 (151 TPS)
최적화 작업 후 다시 성능 테스트를 실행했다:
./run-tps-test.sh
🚀 TryItOn TPS 기반 성능 테스트 시작
======================================
⏰ 테스트 시작 시간: Fri Jul 18 00:29:28 KST 2025
📊 테스트 단계:
Phase 1: 워밍업 - 50 TPS (2분)
Phase 2: 목표부하 - 100 TPS (10분)
Phase 3: 피크부하 - 200 TPS (5분)
Phase 4: 스트레스 - 500 TPS (3분)
총 예상 시간: 20분
테스트 결과는 다음과 같았다:
{
"전체 평균 TPS": 151.21,
"HTTP 요청/초 (RPS)": 453.62,
"API 성공률": "100%",
"평균 응답시간": "492ms",
"총 테스트 시간": "1208초",
"총 트랜잭션 (Iterations)": "182617건",
"총 HTTP 요청": "547851건",
"성공한 요청": "547851건",
"실패한 요청": "0건"
}
모니터링 결과 상세 분석
성능 테스트 동안 AWS CloudWatch를 통해 실시간으로 시스템 상태를 모니터링했다. 각 지표별로 어떤 변화가 있었는지 살펴보자.
ALB (Application Load Balancer) 성능 지표
/image1.png)
ALB의 응답시간과 처리량을 보여주는 그래프다. 최적화 후 전반적으로 안정적인 응답시간을 유지하면서도 처리량이 증가한 것을 확인할 수 있다.
EC2 인스턴스 CPU 사용률
/image2.png)
EC2 인스턴스의 CPU 사용률 변화다. N+1 쿼리 문제 해결로 불필요한 데이터베이스 호출이 줄어들면서 CPU 사용률도 더 효율적으로 변했다.
메모리 사용량 패턴
/image3.png)
메모리 사용량 그래프를 보면 배치 처리 최적화의 효과를 확인할 수 있다. 메모리 사용이 더 안정적이고 예측 가능한 패턴을 보여준다.
RDS 데이터베이스 성능
/image4.png)
RDS의 커넥션 수와 쿼리 성능 지표다. JOIN FETCH 적용으로 쿼리 수가 현저히 줄어든 것이 명확하게 보인다.
네트워크 I/O 패턴
/image5.png)
네트워크 입출력 패턴이다. 배치 처리 최적화로 네트워크 오버헤드가 줄어든 효과를 확인할 수 있다.
응답시간 분포도
/image6.png)
API 응답시간의 분포를 보여주는 그래프다. 최적화 후 응답시간이 더 일관되고 빨라진 것을 시각적으로 확인할 수 있다.
에러율 및 성공률
/image7.png)
테스트 전 과정에서 에러율은 0%를 유지했다. 성능 최적화가 안정성에 영향을 주지 않았다는 것을 보여준다.
전체 시스템 대시보드
/image8.png)
모든 지표를 종합한 대시보드 뷰다. 최적화 전후의 전반적인 시스템 상태 변화를 한눈에 볼 수 있다.
📈 개선 결과 비교
| 지표 | 최적화 이전 | 최적화 이후 | 개선율 |
|---|---|---|---|
| 평균 TPS | 144.6 TPS | 151 TPS | +4.4% |
| HTTP 요청/초 | 433.8 RPS | 453.62 RPS | +4.5% |
| 평균 응답시간 | 601ms | 492ms | -18% ⭐ |
| 총 트랜잭션 | 174,631건 | 182,617건 | +4% |
| API 성공률 | 100% | 100% | 유지 |
| 실패 요청 | 0건 | 0건 | 유지 |
가장 눈에 띄는 개선은 평균 응답시간이 18% 단축된 것이다. TPS 자체는 4.4% 향상되었지만, 사용자 경험 측면에서는 응답시간 개선이 더 큰 의미를 가진다.
🚨 Redis 캐싱 설정 충돌 해결
최적화 과정에서 예상치 못한 문제가 발생했다. 팀원이 구현한 추천 알고리즘과 내가 적용한 Redis 캐싱 설정이 충돌한 것이다.
문제 상황
- 추천 알고리즘: 단순한 String 방식의 직렬화/역직렬화 사용
- 내 캐싱 시스템: Jackson을 활용한 구조체 직렬화 방식 사용
- 두 방식이 충돌하면서 데이터 타입 오류 발생
해결 과정
1단계: TypeReference 적용으로 역직렬화 오류 수정
// 문제가 있던 코드
List<Product> products = getSharedData("trending", List.class);
// 개선된 코드
List<Product> products = getSharedData("trending", new TypeReference<List<Product>>() {});
2단계: 캐시 데이터 오염 방지
getTrendingProducts메소드:List<LambdaBatchDto>저장getTrendingProductsAsEntity메소드:List<Product>저장- 동일한 캐시 키(
shared:trending)에 서로 다른 타입 저장으로 충돌 발생 - 해결: 중복 저장 로직 제거
3단계: 안전한 타입 변환 적용
// 위험한 직접 형변환 (ClassCastException 발생)
List<LambdaBatchDto> data = (List<LambdaBatchDto>) rawData;
// 안전한 변환 방식
List<LambdaBatchDto> data = objectMapper.convertValue(rawData,
new TypeReference<List<LambdaBatchDto>>() {});
4단계: 오염된 캐시 데이터 수동 삭제
# EC2 인스턴스에서 Redis 접속
redis-cli
# 오염된 캐시 키 삭제
DEL shared:trending
💡 배운 점과 개선 포인트
1. 작은 최적화도 의미있는 결과를 만든다
4.4%의 TPS 향상은 작아 보이지만, 응답시간 18% 단축이라는 사용자 경험 개선으로 이어졌다.
2. 팀 작업에서는 설정 충돌을 미리 고려해야 한다
Redis 캐싱 설정 충돌은 예상치 못한 문제였다. 팀원들과 미리 기술 스택과 설정 방식을 공유했다면 피할 수 있었을 것이다.
3. N+1 쿼리 문제는 생각보다 흔하다
ORM을 사용할 때 자주 발생하는 문제이지만, 실제로 발견하고 해결하는 과정에서 많은 것을 배웠다.
4. 모니터링의 중요성
성능 테스트 결과를 시각적으로 확인할 수 있는 모니터링 시스템이 있어서 문제점을 빠르게 파악할 수 있었다.
🎯 다음 단계
이번 최적화로 기본적인 데이터베이스 성능 문제는 해결했지만, 더 큰 성능 향상을 위해서는:
- 인덱스 최적화: 자주 사용되는 쿼리에 대한 인덱스 분석
- 커넥션 풀 튜닝: 데이터베이스 커넥션 설정 최적화
- 캐싱 전략 고도화: 더 효율적인 캐싱 정책 수립
- 쿼리 최적화: 복잡한 쿼리들의 실행 계획 분석
작은 개선이지만 실제 서비스에서 의미있는 성능 향상을 이뤄낸 경험이었다. 특히 팀 작업에서 발생할 수 있는 기술적 충돌을 해결하는 과정에서 많은 것을 배웠다.
'Jungle' 카테고리의 다른 글
| 커버링 인덱스로 11초 쿼리를 100ms로 단축시키기: 실제 장애 해결 과정 (0) | 2025.08.02 |
|---|---|
| Redis 캐싱 도입기: 예상과 다른 결과와 문제 해결 과정 (151 → 146 TPS) (0) | 2025.08.01 |
| TIO 성능 테스트: EC2 업그레이드로 144 TPS 달성하기 (0) | 2025.07.29 |
| TIO 성능 테스트 결과 분석 (40명 동시 사용자) (0) | 2025.07.29 |
| TIO 개선 소요 파악 - 성능 최적화 (0) | 2025.07.29 |