목차
1. 서론
- 1.1 테스트 목적
- 1.2 핵심 성과 요약 및 개선 범위
2. 부하 테스트 수행 및 문제점 분석
- 2.1 테스트 환경 및 시나리오
- 2.2 테스트 결과 요약
- 2.3 정상 요소
- 2.4 문제점 분석 (우선순위별)
- 2.4.1 우선순위 상: N+1 쿼리 문제 (병목 지점)
- 2.4.2 우선순위 중: 20초 Stop-The-World GC, 인덱스 미설정
- 2.4.3 우선순위 하: DB 커넥션 부족 현상
3. 성능 개선 구현 및 검증
- 3.1 우선순위 상 개선: N+1 문제 해결
- 3.2 우선순위 중 개선: GC 부하 감소 및 인덱스 설계
- 3.3 우선순위 하 개선: DB 커넥션 최적화
4. 성능 개선 효과 검증
- 4.1 개선 전후 성능 비교
- 4.2 목표 달성도 평가
- 4.3 API별 상세 개선 결과
5. 결론 및 향후 과제
- 5.1 개선 성과 종합
- 5.2 향후 개선 방향
- 5.3 1000명 이상의 동시 사용자 대비책
6. 부록
- 6.1 상세 테스트 데이터
- 6.2 쿼리 실행 계획 분석
- 6.3 트레이드오프 분석
1. 서론
1.1 테스트 목적
사용자가 게시판을 이용하는 시나리오를 가정하여 운영 환경의 성능 한계점을 식별하고 인프라 및 애플리케이션 전반의 문제점을 찾아 성능 개선 방안을 도출합니다.
- 동시 접속자 500명 환경에서의 시스템 안정성을 확인하고 그 이상의 인원이 활동할 시에 일어나는 상황을 대비
- 각 API별 성능 임계점 및 병목지점 식별
- 운영 환경 배포 전 성능 최적화 방향 수립
1.2 핵심 성과 요약 및 개선 범위
- 핵심 성과 요약
지표 개선 전 개선 후 배율 최대 동시 사용자 416명 1000명 2.4배 증가 최대 TPS 193 306 1.6배 증가 평균 TPS 89 205 2.3배 증가 성공한 HTTP 요청 65,000건 122,000건 1.9배 증가 평균 응답시간 1.2초 0.35초 3.4배 감소 글 수정 API 평균 응답 시간 (ms) 3,767 32 99% 이상 감소 요청 당 쿼리 수 23.7 쿼리/요청 2.7 쿼리/요청 10배 감소 - 개선 범위 : N+1 쿼리 해결, 인덱스 최적화, GC 튜닝, DB 커넥션 최적화
2. 부하 테스트 수행 및 문제점 분석
2.1 테스트 환경 및 시나리오
- 테스트 환경
- 테스트 대상 서버 : 실제 운영 예정인 백엔드 서버 AWS EC2 t2.micro (ubuntu)
- 메모리 950MB
- 스왑 메모리 2GB
- 파일 디스크립터 : 2000
- somaxconn : 1024
- backlog : 2048
- WAS : Tomcat
- 스레드 풀 최소 스레드 수: 25개
- 스레드 풀 최대 스레드 수 : 400개
- 대기열 크기 : 150
- 데이터베이스 : AWS RDS db.t4g.micro (Mysql)
- max_connection : 50개
- 기존 게시글 : 7000개
- 기존 댓글 : 6000개
- DBCP : HikariCP
- 커넥션 풀 최소 커넥션 수 : 10개
- 커넥션 풀 최대 커넥션 수 : 50개
- Load Generator : JMeter (Local)
- Monitoring : Scouter APM
- 파일 디스크립터 : 1024개
- 메모리 950MB
- 스왑 메모리 2GB
- 테스트 대상 서버 : 실제 운영 예정인 백엔드 서버 AWS EC2 t2.micro (ubuntu)
- 테스트 시나리오
- 테스트 방식 : 시나리오 기반 점진적 부하 테스트
- 부하 증가 패턴 : 10분간 1명에서 1000명까지 동시 접속자 증가
- 사용자 행동 패턴: 사용자는 각자 다른 행동을 다른 비율과 빈도로 수행합니다.
비율 빈도 글 열람 34% 2초 게시판 조회 13% 2초 댓글 작성 12% 4초 글 추천 10% 3초 글 작성 8% 4초 댓글 추천 8% 2초 글 수정 6% 5초 댓글 삭제 5% 6초 글 삭제 4% 6초
2.2 테스트 결과 요약
- 최대 TPS : 145
- HTTP 요청 합계 : 5만 2천건
- 오류율 : 0%
- 테스트 종료 시점 : 테스트 10분 이후
- 정상 동작 확인된 레벨:
- 네트워크: 8.58k 요청 100%성공 처리
- OS: SWAP 9.4% 사용, 파일 디스크립터 여유
- 인프라: ELB, RDS 모두 안정적 동작
- 하드웨어: t2.micro도 충분한 처리 능력 보유
- 시스템 성능 변화 타임라인
시간대 TPS 응답시간(90%) CPU(%) RDS연결 수 상태 부하 시작 0.13 0ms 2.89% 7개 정상 3분 경과 120 1,000ms 90%+ 35개 부하증가 5분 경과 145.27 8,526ms 100% 50개 임계점 테스트 종료 시점 81.53 7,706ms 100% 50개 성능저하
2.3 정상 요소
2.3.1 네트워크 - OS 레벨
- 결론 : 문제는 연결 가용성이 아니라 처리속도에 있습니다.
로드밸런서 지표 로드밸런서 값 요청 8.58k 요청 대상 200 응답 8.58k 응답 활성 연결 1.99k 한계 LCU 사용률 피크 50 (0.099%)
- LCU 사용률을 보아 로드밸런서의 용량에 문제가 없습니다.
- 모든 요청과 응답이 성공했음을 알 수 있습니다.
- 활성 연결은 1.99k에서 더 이상 증가하지 못하고 그 지점을 유지했습니다. 우분투의 파일 디스크립터 수가 2000이기 때문에 더이상 연결을 하지 못한 것 입니다.
하지만 파일 디스크립터 설정의 문제는 아닙니다. 이미 파일 디스크립터의 설정은 리소스 대비 상당히 높은편이고 연결을 추가로 하지 않아도 Connection refused가 발생하지 않았으며 모든 요청에 대해 응답이 성공했기 때문입니다.
1.99k의 최대 연결 상황에서 연결 종료 직후 대기큐에 있는 요청을 즉시 재사용했다면 지표에는 활성 연결이 한계에 다다랐지만 요청을 더 처리할 수 있습니다.
아직 성능을 개선할 수 있는 지점이 많기에 애플리케이션 로직의 처리 속도를 향상시켜 HTTP 순환 속도를 빠르게 하면 활성 연결의 문제도 해결할 수 있습니다.
- 우분투 CPU 지표
| 지표 | 값 |
| CPU 크레딧 밸런스 | 137개 |
| CPU 크레딧 사용량 | 0 → 4.97개 |
| CPU 사용률 | 50% → 100% |
- 우분투 SWAP 지표
| 시간 | SWAP 사용량 |
| 부하 시작 | 150MB |
| 5분 경과 | 151MB |
| 테스트 종료 시점 | 188MB |
CPU의 크레딧을 보아 우분투 서버에 병목이 있을 확률은 적습니다. CPU 사용률은 100%지만 우분투 서버의 문제보다는 애플리케이션의 로직의 문제일 확률이 높습니다.
스왑 메모리는 테스트 동안 38MB 증가했으며 여유롭습니다.
디스크는 RDS를 사용하고 있기에 큰 부하가 가해지지 않았습니다.
메타스페이스 영역도 112MB → 119MB로 소량 증가하였으며 정상적인 움직임을 보입니다.
- 네트워크 대역폭
네트워크 초기값 최종값
| Inbound | 1.058GB | 3.184GB |
| Outbound | 0.618GB | 2.463GB |
대역폭도 선형적으로 증가하고 있어 소켓 처리 지연이 없음을 확인할 수 있고 종합적으로 네트워크 - OS레벨에 병목이 없다는 것을 알 수 있습니다.
2.3.2 OS → TOMCAT 레벨
결론 : Tomcat은 느리지만 정상으로 동작하고 있습니다. 그리고 원인은 애플리케이션 로직에 있습니다.
| 시작 구간 | Net Time Wit | TPS | Queue Time |
| 테스트 시작 ~ 3분 후 | 0 | 0.13→88.7 | 0 |
| 3분 후 ~ 4분 후 | 2 | 90.33→128.13 | 0 |
| 4분 후 ~ 테스트 끝 | 0 | 129.3→81.53 | 0 |
테스트 중 1분 간 net time wait가 2가 되었다가 테스트가 끝날때까지 0으로 유지하였습니다.
만약 이것이 병목의 증상이었다면
- TPS는 감소해야했을것이지만 오히려 증가하였습니다
- Queue Time이 증가해야하지만 0을 유지하였습니다.
- 2라는 수치는 지나치게 작은 수치입니다.
따라서 정상적인 TCP 동작이라고 판단하였습니다.
Queue Time이 0인것을 보아 스레드풀에도 대기 큐가 없다는 것을 알 수 있습니다.
| 시간 | 성공 요청/min | Elapsed 90% | Queue Time |
| 테스트 시작 | 8 | 0ms | 0 |
| 5분 뒤 | 8,526 | 260ms | 0 |
| 테스트 끝 | 4,790 | 7,706ms | 0 |
테스트 후반에는 응답시간이 매우 느려졌지만 Queue Time은 여전히 0입니다.
Tomcat은 느리지만 정상으로 동작하고 있습니다. 그리고 원인은 애플리케이션 로직에 있습니다.
2.4 문제점 분석 (우선 순위 별)
- 병목 또는 문제가 있는 부분이 다수 있었고 테스트의 목표인 성능 개선 가능성을 지표로 삼아 우선순위 상, 중, 하로 구분하였습니다.
- 우선순위 상은 병목 지점입니다.
- 우선순위 중은 나머지 문제점 중에서 가장 급하게 해결할 과제이고 성능에 치명적인 요소입니다.
- 우선순위 하는 성능에 치명적이진 않지만 개선하면 성능 향상을 기대할 수 있는 요소입니다.
- 로직이 비효율적 이지만 성능에 크게 영향을 미치지 않는 요소는 개선을 해도 성능 향상을 기대할 수 없다고 판단하여 제외하였습니다.
- 또한 디스크(EC2 볼륨), RDS, 네트워크 등 문제가 없는 지표는 다루지 않았습니다.
| 문제점 | 비즈니스 임팩트 | 우선순위 | 예상 해결 비용 |
| N+1 문제 | 서비스 중단 위험 높음 | 상 | 낮음 |
| 20초의 Stop-The-World 발생 | 사용자 경험 저하 | 중 | 낮음 |
| 인덱스 미설정 | 응답 성능 저하 | 중 | 중간 |
| DB 커넥션 부족 현상 | 응답 성능 저하 | 하 | 낮음 |
2.4.1 우선순위 상 : N+1 쿼리 문제 (병목 지점)
- 문제 현상
-
API별 성능 분석 API 횟수 평균 응답시간(ms) 평균 CPU 사용시간(ms) 평균 메모리 사용량(KB) 평균 SQL 시간(ms) 글 수정 1,677 3,767 67 6,587 1,616 글 삭제 1,177 2,071 2 138 15 댓글 삭제 1,482 1,997 2 139 15 댓글 작성 4,811 1,925 4 333 36 글 작성 3,230 1,856 2 204 20 글 추천 4,952 1,737 3 223 18 글 열람 21,327 1,723 9 1,229 237 게시판 조회 8,276 1,632 6 716 161 댓글 추천 5,162 1,566 3 266 21 커넥션 생성 214 45 1 53 0 - 호출 횟수 상위 5개
쿼리 횟수 총 응답시간(ms) 평균 응답시간(ms) 댓글의 추천 수 조회 738,485 4,875,573 7 글의 추천 수 조회 103,597 524,821 5 유저의 댓글 추천 여부 조회 103,342 1,319,501 13 글의 댓글 수 조회 78,684 472,961 6 사용자 조회 46,849 224,192 5
상위 5개만 보아도 5만 2천건의 요청에 비하여 지나치게 많은 쿼리가 발생 했습니다.
1위인 댓글 추천 수 쿼리의 N+1 증폭률을 계산하면 댓글 추천 수를 요구하는 요청은 3만 7천회, 댓글 추천 수 조회 쿼리는 74만 회로 약 20배의 증폭이 발생했습니다.
- 원인 분석
- 게시글 상세 조회 API의 일부
-- 특정 댓글에 대한 좋아요 수 조회
SELECT COUNT(cl.id)
FROM comment_likes cl
WHERE cl.comment_id = ?; -- ex: 6825
SELECT COUNT(cl.id)
FROM comment_likes cl
WHERE cl.comment_id = ?; -- ex: 6779
SELECT COUNT(cl.id)
FROM comment_likes cl
WHERE cl.comment_id = ?; -- ex: 6738글의 댓글마다 추천 수 조회 요청이 일어나고 있습니다.
API 요청당 메모리점유

API 요청당 CPU점유

API 요청당 쿼리 소요시간

API 요청당 쿼리 발생 횟수

위 지표들에서 3개의 규칙적인 가로선이 확인됩니다. 특정 API가 메모리, CPU, 쿼리 등 모든 리소스에 문제를 일으키는 겁니다.
가장 높은 곳에 있는 선은 글 수정 API입니다. 글 수정 API는 모든 API중 가장 많은 리소스를 사용하고 있습니다.
- 글 수정 API 요약
API 횟수 평균 응답시간(ms) 평균 CPU 사용시간(ms) 평균 메모리 사용량(KBytes) 평균 SQL 시간(ms) 글 수정 1,677 3,767 67 6,587 1,616 - 글 수정 API 프로파일가장 큰 문제는 글과 댓글이 분리되지 않은 것 입니다. 글을 수정하지만 그것이 댓글에도 영향이 가고 있습니다. 글을 수정할 때 댓글마다 개별적으로 댓글의 추천 수, 사용자의 댓글 추천 여부를 조회하고 있습니다.또한 DTO생성도 댓글 수 만큼 발생하고 있습니다. 임시 객체가 대량으로 생성되면 GC의 빈도가 상승하고 메모리를 점유하여 서버의 리소스를 낭비합니다.
- 댓글이 1000개 있는 글을 수정하는데 2057개의 쿼리가 시행되었고 쿼리 처리에는 약 4초가 소모되었으며 비즈니스 로직은 8283회 시행되었고 cpu는 1.5초 점유하였으며 무려 140MB의 메모리를 사용했습니다.
► elapsed = 6,589 ms
► cpu=1,564 ms, kbytes=142959
► sqlCount=2057, sqlTime=3,866 ms
► profileCount=8283
-- 특정 댓글에 대해, 특정 사용자가 좋아요 했는지 조회
SELECT cl.id
FROM comment_likes cl
WHERE cl.comment_id = ?
AND cl.user_id = ?;
-- 특정 댓글의 좋아요 수 조회
SELECT COUNT(cl.id)
FROM comment_likes cl
WHERE cl.comment_id = ?;- 원인 확정
이로 인해 테스트 중 CPU사용량이 100%를 유지한 것의 원인이 글 수정 로직의 N+1의 영향이 크다는 것을 알 수 있습니다.
중간의 가로선은 게시글 조회 API인데 게시글 수정 API와 문제가 동일합니다.
- 비즈니스 임팩트 : 서비스 중단 위험 높음
2.4.2 우선순위 중: 20초 Stop-The-World GC, 인덱스 미설정
20초 Stop-The-World GC
- 문제 현상
- 20초 간의 Stop-The-World
최대 힙 메모리 사용 중 힙 메모리 gc count gc time 8분 22초 경 232 202 3 46 8분 24초 경 정지 정지 정지 정지 8분 46초 경 232 203 3 47 8분 48초 경 232 203 3 47 8분 50초 경 232 157 4 472
- 원인 분석
- 힙 메모리와 GC의 패턴을 보았을 때 메모리 누수는 없고 정상적으로 GC가 동작하였습니다.힙 메모리의 용량이 계속 늘면 GC Time도 늘어납니다. 그리고 SWAP까지 힙 메모리로 사용하면 서버가 죽었다고 판단해도됩니다. 그래서 SWAP까지 힙 메모리가 확장하지않도록 제한을 걸었습니다.처음엔 용량이 늘어난 힙 메모리에서 일어난 FullGC라고 생각했습니다. 하지만 정지 이전과 이후의 힙 메모리가 줄지 않았고 정작 힙 메모리를 제거한건 8분 50초 경에 일어난 GC였습니다.따라서 힙 메모리 확장요청이라고 생각합니다. 다만 제가 건 확장 제한 때문에 확장은 실패하였고 GC로 메모리를 정리했습니다.
- OOM이라고 판단하기에는 서버가 다시 회복하였습니다.
- 그리고 20초간 Stop-The-World가 발생했습니다.
- 4.2.1에서 다룬 글 수정 API는 단일 요청 만으로 140MB의 메모리를 사용하니 글 수정, 글 조회 API의 영향으로 힙 메모리에 부담이 심하게 갔다고 생각합니다.
- 힙 메모리에 가해지는 부담으로 전체 힙 메모리의 용량은 146MB부터 시작하여 230MB까지 상승하였습니다.
- 원인 확정
- 이 현상은 힙 메모리에 강한 부하가 일어났기 때문이고 강한 부하가 일어난 이유는 4.2.1의 N+1 문제가 원인입니다.
- 비즈니스 임팩트 : 사용자 경험 저하
인덱스 미설정
- 문제 현상 및 원인 분석
- 현재는 인덱스가 설정되어있지 않은 상황입니다. 따라서 쿼리의 호출 빈도에 맞게 인덱스를 설정하여 응답을 빠르게 할 필요가 있습니다.
- 비즈니스 임팩트 : 응답 성능 저하
2.4.3 우선순위 하: DB 커넥션 부족 현상
- 문제 현상 및 원인 분석이 현상이 일어난 근본적인 이유는 N+1 발생이 이유이기 때문에 RDS에는 여유가 있지만 최대 커넥션을 늘리는 것은 고려하지 않았습니다.
- RDS는 테스트 중 지속적으로 한계 설정인 50개의 커넥션 풀을 유지하였고 애플리케이션에서는 커넥션 생성을 하였지만 연결되지 못한 것이 214번 있었습니다. RDS의 지표는 여유로웠습니다.
- 비즈니스 임팩트 : 응답 성능 저하
3. 성능 개선 구현 및 검증
3.1 우선순위 상 개선: N+1 문제 해결
- 조치댓글 부분은 FetchType.LAZY를 활용하여 댓글 추천 테이블을 지연로딩시키고 QueryDSL을 활용하여 댓글 추천 수 조회 및 사용자의 댓글 추천 여부를 배치처리하여 N+1을 해결했습니다.
- 댓글과 글을 분리하여 글 수정이 댓글에 영향을 끼치지 못하게 분리하여 댓글이 1000개인 게시글 기준 2000개 이상의 쿼리가 실행되었지만 지금은 단 두개의 쿼리로 글 수정을 완료 하였습니다.
- 개선 효과
- 개선 전 후 글 수정 API 주요 지표
항목 개선 전 개선 후 변화율 호출 횟수 1,677 4,554 2.7배 증가 평균 응답 시간 (ms) 3,767 32 99% 이상 감소 평균 CPU 사용 시간 (ms) 67 2 97% 감소 평균 메모리 사용량 (KB) 6,587 123 98% 감소 평균 SQL 실행 시간 (ms) 1,616 2 99.9% 감소 - 개선 전 후 테스트 전체 SQL 주요 지표
개선 전에는 N+1의 영향과 인덱스 미설정으로 인하여 요청당 많은 쿼리가 발생하여 데이터베이스에서 응답지연이 발생했으나 개선 후에는 요청이 2배 늘었음에도 총 SQL 소요시간은 10배 감소하였습니다.항목 개선 전 개선 후 변화 내용 HTTP 요청 수 52,000 104,000 2배 증가 총 SQL 소요 시간 8,520초 858초 10배 감소 요청 당 쿼리 23.7 쿼리/요청 2.7 쿼리/요청 10배 감소 - 요청 당 쿼리 개수가 감소가 N+1 문제 해결의 직접적 증거는 아니지만, 실제로 N+1이 심각했던 상황에서는 유의미한 지표가 될 수 있으니 비교하자면 10배 감소하였습니다.
- 트레이드 오프
- LazyInitializationException 위험에 노출 될 수 있습니다 세션이 종료된 이후에 지연 로딩 시도 시 예외가 발생하기 때문입니다. 실제로 그로인해 비동기 로직에서 커넥션 누수도 일어났습니다. 따라서 트랜잭션 범위를 신중히 설정하고 비동기 로직에서 지연 로딩을 쓸 때 주의를 기울여야하며 지연 로딩된 엔티티를 반복문에서 접근할 때 N+1이 재발할 수 있기때문에 조심해야합니다. EntityGraph를 활용하여 명시적 Fetch Join과 배치 패치를 활용하여 이 부분을 보완할 수 있습니다.
3.2 우선순위 중 개선: GC 부하 감소 및 인덱스 설계
GC 부하 감소
- 조치
- 원인은 N+1을 동반한 비효율적인 쿼리의 리소스 점유 때문이었습니다. 전체적인 쿼리 및 API를 최적화 시킨 현재 힙 메모리에 가해지는 부담이 약해져 테스트 중 힙 메모리의 용량이 확장하지 않았습니다.
- 개선 효과Stop-The-World가 완전 제거 되었고 전체적인 메모리 부담 감소로 안정적인 GC 패턴을 확보했습니다.
- 힙 메모리가 과부하됨에 따라 JVM이 용량 확장을 시도한다는 것을 보았을 때 메모리의 부담이 줄었다는 것을 알 수 있습니다.
인덱스 설계
- 조치
- 글의 추천 수 집계 및 사용자 추천 여부 인덱스 생성
-- 사용자의 글 추천 여부 조회시 커버링 인덱스 활용. CREATE INDEX idx_post_notice_created ON post (is_notice, created_at DESC);- 게시글 추천 수 집계 실행계획

게시글의 추천 수와 유저의 게시글 추천 여부 조회를 한번에 하려면 post_id와 user_id를 통해서 인덱스 만으로 조회가 가능합니다. 실행계획에서 커버링 인덱스를 사용한 것을 확인할 수 있습니다.
- 게시판 조회 인덱스 생성
CREATE INDEX idx_comment_like_user_comment
ON comment_like (comment_id, user_id);게시판 조회는 유저정보와 Join하여 게시글을 가져오고 최신순으로 정렬하여 가져옵니다.
인덱스를 생성할 때 생성시간 기준 내림차순으로 생성하여 역순탐색을 방지하였습니다.
공지사항을 게시글 목록에서 분리하고 나머지 글을 내림차순으로 그대로 가져올 수 있게 최적화 하였습니다.
- 게시판 조회 실행계획

그러나 테스트 중 공지사항이 없던 바람에 옵티마이저가 복합인덱스를 쓰지 않고 created_at 단일 인덱스를 사용하였는데 이 인덱스가 오름차순으로 생성되어 있어 역순검색이 발생 했습니다. 해당 부분은 인덱스를 내림차순으로 다시 교체해야 합니다.
트레이드 오프
게시판 조회 인덱스의 경우 IN 리스트가 길면 풀스캔을 할 수도 있습니다. 그래서 게시글 보기를 50개까지 한계로 두어 IN 리스트에 제한을 걸었습니다.
- 댓글 데이터 조회 인덱스 생성
- 사용자의 댓글 추천 여부 조회시 커버링 인덱스 활용
CREATE INDEX idx_comment_like_user_comment
ON comment_like (comment_id, user_id);
CREATE INDEX idx_comment_closure_ancestor_depth
ON comment_closure (ancestor_id, depth);- 댓글 데이터 조회 실행 계획

댓글 데이터는 댓글과 댓글 마다의 추천 수, 유저의 댓글 추천 여부, 댓글 계층 정보를 하나의 쿼리로 가져옵니다. 커버링 인덱스를 활용하여 댓글 추천 테이블을 처리했고 댓글의 자손을 빠르게 가져올 수 있게하기 위하여 조상id, 깊이 순으로 복합 인덱스를 만들었습니다.
실행계획에서 커버링 인덱스를 활용하고 있는 것을 확인하였습니다.
- 개선 효과
- 개선 전 호출 횟수 상위 5개 쿼리
쿼리 횟수 총 응답시간(ms) 평균 응답시간(ms) 댓글의 추천 수 조회 738,485 4,875,573 7 글의 추천 수 조회 103,597 524,821 5 유저의 댓글 추천 여부 조회 103,342 1,319,501 13 글의 댓글 수 조회 78,684 472,961 6 사용자 조회 46,849 224,192 5 - 개선 후 호출 횟수 상위 5개 쿼리
쿼리 실행 횟수 (Count) 총 소요 시간 (ms) 평균 실행 시간 (ms) 비고 설명 글의 추천 수 집계 및 사용자 추천 여부 20,631 55,438 3 post_like 테이블, IN + GROUP BY 사용 전체 게시글 수 조회 (페이징) 20,631 104,710 5 Count(*) 단순 집계 게시판에서 글마다의 댓글 수 조회 20,631 70,283 3 댓글 테이블에서 IN과 Group By 사용 게시판 조회 20,631 60,676 3 게시글 + 유저 정보 + Join + 정렬 댓글 데이터 조회 19,088 77,910 4 댓글 +댓글 추천 + 댓글 계층 복합 조인 + 집계 + 정렬 - 모든 핵심 쿼리에서 커버링 인덱스 활용으로 디스크 I/O 최소화
- 게시판 조회에서 공지사항 분리 및 최신순 정렬 최적화
- 댓글 계층 구조 조회 성능 대폭 개선
- 트레이드 오프
- 인덱스를 생성하면 저장 공간 또한 증가하고 삽입, 삭제 쿼리가 느려집니다. 그래서 조회시 인덱스를 적용하면 성능이 급상승하는 꼭 필요한 부분에만 인덱스를 생성해야합니다. 대부분의 게시판 서비스는 글과 댓글을 작성하는 사람보다는 조회하는 사람이 더 많은 성격을 가지고 있습니다. 파티셔닝으로 인덱스 크기를 분산하고 부분 인덱스로 크기를 최적화하면 이 문제점을 극복할 수 있습니다. 또한 커버링 인덱스 대신에 Include컬럼을 활용하여 쓰기 비용을 최소화 할 수 있습니다. 현재는 꼭 필요한 부분에 인덱스를 적용하여 커버링인덱스로 효율을 냈기 때문에 현시점에는 이상이 없다고 생각합니다.
3.3 우선순위 하 개선: DB 커넥션 최적화
- 조치
- N+1 문제 해결로 쿼리 수 대폭 감소
- 효율적인 쿼리로 커넥션 점유 시간 단축
- 개선 효과개선 전에는 테스트 중 커넥션을 214회 생성하였지만 개선 후에는 동시사용자가 800명 이상을 돌파한 고부하 상황에 가끔 생성을 하였고 총 커넥션 생성 횟수는 19회에 그쳤습니다.
- N+1 문제로 수 많은 쿼리가 발생하고 긴 SQL Time으로 인해 커넥션을 오랜시간 점유함에 따라 애플리케이션은 커넥션을 계속 생성하며 리소스를 소비하고 RDS에는 max_connection에 막혀 연결되지 않는 악순환이 있었습니다.
4. 성능 개선 효과 검증
4.1 개선 전후 성능 비교
| 지표 | 개선 전 | 개선 후 | 배율 |
| 최대 동시 사용자 | 416명 | 1000명 | 2.4배 증가 |
| 최대 TPS | 193 | 306 | 1.6배 증가 |
| 평균 TPS | 89 | 205 | 2.3배 증가 |
| 성공한 HTTP 요청 | 65,000건 | 122,000건 | 1.9배 증가 |
| 평균 응답시간 | 1.2초 | 0.35초 | 3.4배 감소 |
| 글 수정 API 평균 응답 시간 (ms) | 3,767 | 32 | 99% 이상 감소 |
| 요청 당 쿼리 수 | 23.7 쿼리/요청 | 2.7 쿼리/요청 | 10배 감소 |
4.2 목표 달성도 평가
| 지표 | 개선 전 | 목표 | 개선 후 | 개선율 | 목표 달성 |
| 게시글 열람 평균 응답시간 | 1,723ms | 150ms | 159ms | 10.8배 향상 | 9ms 미달 |
| 댓글 작성 평균 응답시간 | 1,925ms | 200ms | 169ms | 11.4배 향상 | 달성 |
| 게시판 조회 평균 응답시간 | 1,632ms | 150ms | 155ms | 10.5배 향상 | 9ms 미달 |
| TPS | 145 | 300 | 315 | 2.2배 향상 | 달성 |
| 동시 접속자 | 480명 | 1,000명 | 1,000명 | 2.1배 향상 | 달성 |
저번 테스트의 지표와 목표 그리고 현재 테스트의 지표를 종합해보았을 때 모든 목표를 달성하지는 못했지만 달성하지 못한 지표도 근소한 차이이고 많은 개선이 있었기 때문에 만족할 만한 성과라고 생각합니다.
4.3 API별 상세 개선 결과
| API | 개선 전 쿼리 수 | 개선 후 쿼리 수 | 쿼리 수 감소 배수 | 개선 전 응답 시간 | 개선 후 응답 시간 | 응답 시간 감소 배수 |
| 게시글 열람 | 1,043개 | 3개 | 350배 감소 | 1,723ms | 159ms | 11배 감소 |
| 댓글 작성 | 5개 | 1개 | 5배 감소 | 1,925ms | 169ms | 11배 감소 |
| 게시판 조회 | 23개 | 4개 | 6배 감소 | 1,632ms | 155ms | 11배 감소 |
| 글 수정 | 100개 이상 | 1개 | 100배 이상 감소 | 3,767ms | 32ms | 118배 감소 |
5. 결론 및 향후 과제
5.1 개선 성과 종합
- 우선순위 - 상 (병목 지점)
- N+1 쿼리 문제 해결 : 20배 쿼리 증폭 정상화
- 우선순위 - 중
- 20초 Stop-The-World GC 해결
- 인덱스 최적화 완료
- 우선순위 - 하
- DB 커넥션 부족 현상 해결 : 커넥션 생성 91% 감
5.2 게시판 시나리오 향후 개선 방향
- 인덱스 최적화 : 파티셔닝과 부분 인덱스 활용
- 게시판 조회 인덱스 최적화 : 공지사항이 없을때에 생성일자 단일 인덱스를 선택하는 것에 대비하여 인덱스를 내림차순으로 생성해야합니다.
- 지연 로딩 N+1 재발 방지 : EntityGraph를 활용한 명시적 Fetch Join과 배치 Fetch
- 쿼리 힌트 적용 : 옵티마이저가 잘못된 인덱스를 선택하는 경우 명시적 힌트 추가
- Key-set Pagination 도입 : OFFSET 사용 시 페이지가 깊어질수록 성능 저하가 일어나기 때문에 WHERE created_at < ?로 변경 가능합니다/
5.3 1000명 이상의 동시 사용자 대비책
- 이미 최적화가 상당히 이루어진 상태이므로 가장 간단한 방법은 스케일 아웃과 스케일 업이 있습니다.
구분 비용 비고 스케일 아웃 월 약 15,000원, 예약 인스턴스 활용 시 약 5,000원 비용 효율적이며 간단한 확장 방법 스케일 업 스케일 아웃과 동일한 효과를 얻으려면 월 50,000원의 비용이 소모 비용이 높아 스케일 아웃에 비해 효율이 낮음 - 또한 만약 RDS에서 병목이 발생하면 데이터베이스 읽기/쓰기 분리를 할 수 있습니다. RDS 인스턴스를 하나 더 사용해야 하기 때문에 마찬가지로 비용이 드는 작업입니다. 게시판은 작성보다 조회 요청이 훨씬 많습니다. 데이터베이스를 Master와 Replica로 구성하여 읽기 요청을 Replica로 분산시키면, 쓰기 성능에 영향을 주지 않으면서 전체적인 읽기 처리량을 크게 늘릴 수 있습니다.
- 서버의 리소스를 늘리지 않는 방법은 CDN활용이 있습니다.
- 정적 컨텐츠를 위한 CDN 활용 : 게시글에 포함된 이미지, 첨부파일 등 정적 콘텐츠를 CDN을 통해 제공하면 사용자들은 더 빠르게 콘텐츠를 로딩할 수 있고, 애플리케이션 서버의 트래픽 부하를 크게 줄일 수 있습니다. CDN도 비용이 들지만 소규모 사이트의 경우 거의 무료로 사용할 수 있습니다.
'트러블슈팅과 고민' 카테고리의 다른 글
| 아키텍처 관점에서 Repository 계층의 접근 범위에 대한 고민 (2025년 6월 15일) (0) | 2025.08.07 |
|---|---|
| 알림 읽음 처리에 대한 고민 (2025년 4월 29일) (0) | 2025.08.07 |
| 롤링페이퍼 유저 상호작용 부하 테스트 및 성능 개선 (0) | 2025.08.07 |
| 추가적인 보안 관련 고민 (2025년 4월 26일) (0) | 2025.08.07 |
| 실시간 알림 시스템 기술 선택 관한 고민 (2025년 4월 13일) (0) | 2025.08.07 |