데이터베이스 액세스 최적화로 TPS 4.4% 향상시키기 (144.6 → 151 TPS)

2025. 8. 1. 16:35·Jungle

이번 포스트에서는 실제 프로젝트에서 데이터베이스 성능 최적화를 통해 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. 모니터링의 중요성

성능 테스트 결과를 시각적으로 확인할 수 있는 모니터링 시스템이 있어서 문제점을 빠르게 파악할 수 있었다.

🎯 다음 단계

이번 최적화로 기본적인 데이터베이스 성능 문제는 해결했지만, 더 큰 성능 향상을 위해서는:

  1. 인덱스 최적화: 자주 사용되는 쿼리에 대한 인덱스 분석
  2. 커넥션 풀 튜닝: 데이터베이스 커넥션 설정 최적화
  3. 캐싱 전략 고도화: 더 효율적인 캐싱 정책 수립
  4. 쿼리 최적화: 복잡한 쿼리들의 실행 계획 분석

작은 개선이지만 실제 서비스에서 의미있는 성능 향상을 이뤄낸 경험이었다. 특히 팀 작업에서 발생할 수 있는 기술적 충돌을 해결하는 과정에서 많은 것을 배웠다.

728x90

'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
'Jungle' 카테고리의 다른 글
  • 커버링 인덱스로 11초 쿼리를 100ms로 단축시키기: 실제 장애 해결 과정
  • Redis 캐싱 도입기: 예상과 다른 결과와 문제 해결 과정 (151 → 146 TPS)
  • TIO 성능 테스트: EC2 업그레이드로 144 TPS 달성하기
  • TIO 성능 테스트 결과 분석 (40명 동시 사용자)
ahpicl64
ahpicl64
in the clouds
  • ahpicl64
    구름
    ahpicl64
  • 전체
    오늘
    어제
    • 분류 전체보기 (95)
      • WIL (4)
      • Jungle (36)
      • AWS (2)
      • SQL (2)
      • CS:APP (17)
      • Algorithm (10)
      • K8s (7)
      • 자료 구조 (10)
      • Spring (4)
      • React (0)
      • 운영체제 (1)
      • 기타등등 (2)
      • 이야기 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • github
  • 공지사항

  • 인기 글

  • 태그

    컴퓨터시스템
    DevOps
    DB
    부하테스트
    CloudFront
    Spring
    CSAPP
    자료구조
    알고리즘
    github actions
    IAM
    S3
    어셈블리
    Spring boot
    queue
    트러블슈팅
    EC2
    python
    k8s
    AWS
  • 02-21 08:19
  • hELLO· Designed By정상우.v4.10.3
ahpicl64
데이터베이스 액세스 최적화로 TPS 4.4% 향상시키기 (144.6 → 151 TPS)
상단으로

티스토리툴바