T_era

설문 프로젝트 모니터링 및 부하 테스트 + 성능 개선 본문

Programing/Spring

설문 프로젝트 모니터링 및 부하 테스트 + 성능 개선

블스뜸 2025. 8. 7. 21:53

1. 개요

1.1 운영 환경

  • EC2: vCPU 2개, 메모리 4GB
  • RDS: vCPU 1개, 메모리 2GB
  • 테스트 환경: Docker 컨테이너를 통한 운영 환경 시뮬레이션

1.2 성능 목표

  • 처리량: 40~60 RPS
  • 응답시간: p95 < 100ms
  • CPU 점유율: 최대 30%
  • 에러율: < 0.1%

1.3 부하 테스트 시나리오

  • 테스트 도구: k6
  • 총 테스트 시간: 약 30분
  • 가상 사용자: 10명 시작, 10명씩 2분마다 증가, 최대 100명
  • 중단 조건: 에러율 5% 초과 또는 응답시간 수용 불가

2. 1차 부하 테스트 결과

2.1 테스트 설정

# Tomcat 스레드 풀
server:
  tomcat:
    threads:
      max: 20
      min-spare: 10

# HikariCP 커넥션 풀
hikari:
  minimum-idle: 5
  maximum-pool-size: 10
  connection-timeout: 5000
  idle-timeout: 600000
  max-lifetime: 1800000

# HTTP 클라이언트 타임아웃
RequestConfig:
  connection-request-timeout: 3초
  connect-timeout: 5초
  response-timeout: 10초

# HTTP 커넥션 풀
PoolingHttpClientConnectionManager:
  max-total: 20
  default-max-per-route: 20

2.2 성능 지표 분석

  • Max RPS: 19.7 req/s (목표 40~60 RPS 미달성)
  • Max Latency (p95): No data (모니터링 설정 미완료)
  • Max CPU Usage: 1.10% (목표 30% 이하 달성)
  • Max Error Rate: 81.2% (목표 0.1% 초과, 심각한 문제)

  • CPU Usage: 평균 0.6%, 최대 26.5% (정상 범위)
  • JVM Memory: 사용량 280MiB, 최대 2.23GiB (여유)
  • GC Pause: 평균 0~20ms, 최대 40ms (정상)
  • Threads: Live 29개, Peak 40개 (안정적)
  • HikariCP Pool: Active 10개, Idle 0개 (풀 고갈)

2.3 문제점 분석

2.3.1 스레드 연쇄 대기 현상

증상: 동시 요청 수가 Tomcat 스레드 풀 최대치(20개)에 근접할 때 대규모 타임아웃 발생

원인 분석:

  1. 도메인 A의 API 처리 중 RestClient로 도메인 B 호출
  2. RestClient 요청 처리에 새로운 Tomcat 스레드 필요
  3. 모든 스레드가 초기 요청 처리 중으로 여분 스레드 부재
  4. 스레드 1이 무한 대기 후 타임아웃으로 실패
  5. 다중 스레드에서 동시 발생으로 시스템 마비

로그 분석:

# 정상 요청 (부하 적음)
=== 전체 메서드 완료 - 총 실행시간: 10ms (DB: 2ms, API: 7ms, 변환: 0ms) ===

# 타임아웃 요청 (부하 많음)
=== 외부 API 호출 실패 - API 실행시간: 5022ms, 전체 실행시간: 5023ms, 에러: 500 ===

3. 최적화 전략 수립

3.1 해결 방안 검토

       
방안 장점 단점 우선순위
캐싱 도입 구현 간단, 즉시 효과 데이터 정확성 저하, 캐시 스탬피드 1순위
조회용 테이블 근본적 해결, 안정적 구현 복잡도 중간 2순위
WebFlux 전환 비동기 처리 완전 해결 아키텍처 변경 대규모 3순위
서버 증설 즉시 해결 비용 증가, 최후 수단 제외

3.2 캐싱 방법 비교

         
구분 @Cacheable ConcurrentMap Caffeine Redis
구현 복잡도 낮음 매우 낮음 낮음 중간
성능 높음 높음 매우 높음 중간
분산 지원 제한적 없음 없음 완전
운영 관리 간단 어려움 간단 중간

선택: @Cashable (단일 서버 환경에서 압도적 성능)

4. 2차 부하 테스트: 캐싱 적용

4.1 캐싱 구현

@Override
@Cacheable(value = "participationCounts", key = "#surveyIds.toString()")
public ParticipationCountDto getParticipationCounts(String authHeader, List<Long> surveyIds) {
    ExternalApiResponse participationCounts = participationApiClient.getParticipationCounts(authHeader, surveyIds);
    Map<String, Integer> rawData = (Map<String, Integer>)participationCounts.getData();
    return ParticipationCountDto.of(rawData);
}

4.2 성능 지표 분석

그라파나 결과 스냅샷

  • Max RPS: 100 req/s (목표 40~60 RPS 초과 달성)
  • Max Latency (p95): No data (모니터링 설정 미완료)
  • Max CPU Usage: 0.817% (목표 30% 이하 달성)
  • Max Error Rate: No data (에러 없음, 목표 달성)

  • CPU Usage: 평균 1.0%, 최대 29.6% (정상 범위)
  • JVM Memory: 사용량 302MiB, 최대 2.23GiB (여유)
  • GC Pause: 평균 0~10ms, 최대 40ms (정상)
  • Threads: Live 39개, Peak 41개 (안정적)
  • HikariCP Pool: Active 1-2개, Idle 8-10개 (여유)

4.3 캐싱 한계점 분석

4.3.1 고려되지 않은 사항

  1. 캐시 만료 시간 미설정: 참여자 수는 변동이 많아 최소 만료시간 필요
  2. 캐시 스탬피드: 캐시 만료 시 갑작스러운 부하 대응 미흡
  3. 캐싱 데이터 활용도: 참여자 수만 캐싱으로는 근본적 해결 한계
  4. 외부 API 호출 범위: 설문 목록 조회 외 다른 API에서도 동일 문제 발생 가능

4.3.2 결론

캐싱은 성능 향상에 유의미한 효과를 보였으나, 근본적인 스레드 연쇄 대기 문제의 완전한 해결책이 되지 못함. 더 근본적이고 안정적인 해결책 필요.

5. 3차 부하 테스트: 조회용 테이블 구축

5.1 MongoDB 기반 조회용 테이블 설계

5.1.1 엔티티 설계

@Document(collection = "survey_summaries")
public class SurveyReadEntity {
    @Id
    private String id;

    @Indexed
    private Long surveyId;

    @Indexed
    private Long projectId;

    private String title;
    private String description;
    private String status;
    private Integer participationCount;

    private SurveyOptions options;
    private List<QuestionSummary> questions;
}

5.1.2 동기화 전략

// 설문 생성 시 비동기 동기화
@Async
@Transactional
public void surveyReadSync(SurveySyncDto dto) {
    // MongoDB에 설문 데이터 저장
}

// 질문 생성 시 비동기 동기화
@Async
@Transactional
public void questionReadSync(Long surveyId, List<QuestionSyncDto> dtos) {
    // MongoDB에 질문 데이터 저장
}

// 참여자 수 배치 동기화 (5분 간격)
@Scheduled(fixedRate = 300000)
public void batchParticipationCountSync() {
    // 외부 API에서 참여자 수 조회 후 MongoDB 업데이트
}

5.2 성능 지표 분석

그라파나 결과 스냅샷

  • Max RPS: 99.8 req/s (목표 40~60 RPS 초과 달성)
  • Max Latency (p95): 60.5 ms (목표 p95 < 100ms 달성)
  • Max CPU Usage: 0.836% (목표 30% 이하 달성)
  • Max Error Rate: No data (에러 없음, 목표 달성)
  • Rate: 0~90 ops/s 범위에서 점진적 증가 후 안정화
  • Errors: No data (에러 없음)
  • Duration:
    • HTTP - AVG: 6.54 ms (평균 응답시간)
    • HTTP - MAX: 68.8 ms (최대 응답시간)
    • 18:45경 450ms 스파이크 발생 후 안정화(테스트 데이터 삽입 이슈)

  • CPU Usage: 평균 0.8%, 최대 29.9% (정상 범위)
  • JVM Memory: 사용량 333MiB, 최대 2.23GiB (여유)
  • GC Pause: 평균 0~30ms, 최대 50ms (정상)
  • Threads: Live 44개, Peak 45개 (안정적)
  • HikariCP Pool: Active 10개, Idle 0개 (풀 고갈)

6. 최적화 효과 분석

6.1 성능 개선 비교

         
지표 1차 테스트 2차 테스트 3차 테스트 목표
Max RPS 19.7 req/s 100 req/s 99.8 req/s 40~60 req/s
Max Latency (p95) 측정 불가 측정 불가 60.5 ms < 100ms
Max CPU Usage 1.10% 0.817% 0.836% < 30%
Max Error Rate 81.2% 0% 0% < 0.1%

6.2 최적화 단계별 효과

6.2.1 1차 → 2차 (캐싱 적용)

  • RPS: 19.7 → 100 req/s (약 400% 향상)
  • 에러율: 81.2% → 0% (완전 해결)

6.2.2 2차 → 3차 (조회용 테이블)

  • RPS: 19.7 → 99.8 req/s (약 400% 향상)
  • 에러율: 81.2% → 0% (완전 해결)
  • p95 응답시간: 측정 불가 → 60.5 ms (목표 달성)

6.3 남은 과제

6.3.1 DB 커넥션 풀 최적화

  • HikariCP Pool: Active 10개로 풀 고갈 상태
  • 고려사항: maximum-pool-size 증가 또는 쿼리 최적화

6.3.2 입력 요청 최적화

  • 초기 테스트 데이터 삽입 시 CPU 사용률 스파이크 현상: 대량 데이터 입력 시 시스템 리소스에 부하 발생
  • 고려사항:
    • 처리 방식 개선으로데이터 삽입 최적화
    • 입력 요청에 대한 비동기 처리 도입 검토
    • 데이터 입력 시 리소스 사용량 모니터링 환경 구축

7. 결론 및 권장사항

7.1 최적화 성과

  1. 처리량: 목표 40~60 RPS를 99.8 RPS로 초과 달성
  2. 응답시간: p95 60.5ms로 목표 < 100ms 달성
  3. 안정성: 에러율 0%로 완전 해결
  4. 리소스 효율성: CPU 사용률 목표 달성

7.2 핵심 해결책

  1. 캐싱 도입: 외부 API 호출 감소로 즉시 성능 향상 - 롤백
  2. 조회용 테이블: MongoDB 기반으로 근본적 문제 해결 - fix
  3. 비동기 동기화: 데이터 정확성과 성능의 균형 달성 - fix

7.3 MongoDB선택 이유

  1. 프로젝트의 상황 : 현 프로젝트에는 데이터 구조가 복잡하여 (VO, List 등)의 이유로 직렬화가 복잡해진다
    1. 레디스는 복잡한 객체를 저장하기 어렵다
  2. 복합 인덱스 : projectId surveyId 등 복합적인 인덱싱이 가능하고 효율적인 페이징 처리가 가능하다
    1. 레디스는 인메모리 기반이라 속도는 빠르지만 설문 목록 조회 등의 기능을 사용하면 모든 키를 조회해야한다
    2. 또한 페이징, 커서 처리가 상대적으로 많이 복잡하다
  3. 결론
    1. 복잡한 설문 구조와 중첩된 객체 저장에 유리하다
    2. 쿼리 패턴이 상대적으로 쉬운 편이다
    3. 직관적인 json 구조를 사용하여 확장성 좋고 유연하다
    4. MongoDB는 json구조를 통해 조회에 매우 유리한 성능을 가지고 있다

7.4 최종 평가

조회용 테이블 구축을 통한 최적화로 모든 목표 성능을 초과 달성했으며, 시스템의 안정성과 확장성을 크게 향상시키고, 특히 p95 응답시간 60.5ms 달성으로 사용자 경험을 크게 개선.