애플리케이션 코드 최적화도 중요하지만, 그 전에 코드가 실행되는 환경부터 제대로 설정해야 한다. 아무리 좋은 코드를 작성해도 JVM, 웹 서버, 데이터베이스 커넥션 풀이 병목이 되면 성능이 나오지 않기 때문이다.
이번 포스트에서는 고성능 웹 애플리케이션을 위한 서버 리소스 최적화 설정 방법을 정리해보려고 한다.
📚 핵심 용어 정리
시작하기 전에 이 글에서 자주 등장하는 핵심 용어들을 정리해보자.
JVM 관련 용어
JVM (Java Virtual Machine)
- 자바 바이트코드를 실행하는 가상 머신
- 메모리 관리, 가비지 컬렉션, 스레드 관리 등을 담당
- 실생활 비유: 자바 프로그램이 돌아가는 "작업장"
힙 메모리 (Heap Memory)
- 객체들이 저장되는 메모리 영역
- 가비지 컬렉션의 대상이 되는 공간
- 실생활 비유: 물건들을 보관하는 "창고"
가비지 컬렉션 (Garbage Collection, GC)
- 사용하지 않는 객체들을 자동으로 메모리에서 제거하는 과정
- JVM이 자동으로 수행하는 메모리 정리 작업
- 실생활 비유: 창고에서 불필요한 물건들을 치우는 "대청소"
GC 일시정지 (GC Pause)
- 가비지 컬렉션이 실행되는 동안 애플리케이션이 잠시 멈추는 현상
- 사용자 요청 처리가 일시적으로 중단됨
- 실생활 비유: 대청소하는 동안 창고 이용이 잠시 중단되는 것
웹 서버 관련 용어
스레드 (Thread)
- 하나의 작업 단위, 동시에 여러 요청을 처리하기 위해 사용
- 각 사용자 요청을 처리하는 "일꾼"
- 실생활 비유: 식당의 "서빙 직원" (손님 한 명당 직원 한 명)
스레드 풀 (Thread Pool)
- 미리 생성해둔 스레드들의 집합
- 요청이 들어오면 놀고 있는 스레드를 할당해서 처리
- 실생활 비유: 식당의 "서빙 직원 팀"
TPS (Transactions Per Second)
- 초당 처리할 수 있는 트랜잭션(요청) 수
- 서버 성능을 측정하는 핵심 지표
- 실생활 비유: 식당에서 "분당 서빙할 수 있는 손님 수"
대기 큐 (Accept Count)
- 모든 스레드가 바쁠 때 요청을 임시로 대기시키는 공간
- 실생활 비유: 식당의 "대기 줄"
데이터베이스 관련 용어
커넥션 (Connection)
- 애플리케이션과 데이터베이스 간의 연결
- 데이터를 주고받기 위한 "통로"
- 실생활 비유: 은행과 고객을 연결하는 "창구"
커넥션 풀 (Connection Pool)
- 미리 생성해둔 DB 커넥션들의 집합
- 필요할 때마다 빌려주고 사용 후 반납받아 재사용
- 실생활 비유: 은행의 "창구 운영 시스템"
HikariCP
- Spring Boot에서 기본으로 사용하는 커넥션 풀 라이브러리
- 빠른 성능과 안정성으로 유명
- 실생활 비유: 효율적인 "창구 관리 시스템"
🔧 JVM 옵션 최적화
JVM이 성능에 미치는 영향
JVM(Java Virtual Machine)이란?
- 자바 프로그램이 실행되는 가상 환경
- 메모리 관리, 가비지 컬렉션, 스레드 관리 등을 자동으로 처리
- 실생활 비유: 자바 프로그램들이 일하는 "사무실 건물"
JVM은 다양한 옵션을 통해 메모리 관리 및 GC(Garbage Collection) 동작을 세밀하게 제어할 수 있다. 특히 고부하 상황에서는 JVM 설정이 성능을 크게 좌우한다.
왜 JVM 설정이 중요할까?
- 메모리 부족: 힙 크기가 작으면 OutOfMemoryError 발생
- GC 지연: 잘못된 GC 설정으로 응답 시간 증가
- 리소스 낭비: 과도한 메모리 할당으로 시스템 자원 낭비
핵심 JVM 옵션들
메모리 설정 옵션:
-Xms: JVM 시작 시 힙 메모리 크기 (Initial heap size)-Xmx: JVM이 사용할 수 있는 최대 힙 메모리 크기 (Maximum heap size)- 두 값을 동일하게 설정하는 이유: 런타임 중 힙 크기 조절로 인한 성능 저하 방지
실생활 비유:
-Xms: 사무실 개업 시 "기본 사무 공간 크기"-Xmx: 사무실이 "확장할 수 있는 최대 공간 크기"- 두 값을 같게 하는 이유: 확장 공사로 인한 업무 중단 방지
GC 알고리즘 설정:
-XX:+UseG1GC: G1(Garbage-First) GC 사용- G1 GC란?: 큰 힙 메모리 환경에서 짧은 GC 일시 중지 시간을 목표로 설계된 가비지 컬렉터
- 장점: 사용자 요청에 대한 응답 지연을 최소화
최적화된 JVM 옵션 예시
# 8GB 서버 환경에서의 권장 설정
java -Xms4g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar app.jar
설정값 선택 근거:
힙 메모리: -Xms4g -Xmx4g
전체 서버 메모리: 8GB
├── JVM 힙 메모리: 4GB (50%)
├── JVM 비힙 메모리: 1GB (메타스페이스, 스택 등)
├── OS 시스템: 2GB
└── 기타 프로세스: 1GB
- 50% 할당 이유: 안전한 메모리 사용률 유지
- 고정 설정 이유: 런타임 중 힙 크기 변경으로 인한 성능 저하 방지
GC 알고리즘: -XX:+UseG1GC
| GC 종류 | 장점 | 단점 | 적합한 상황 |
|---|---|---|---|
| Parallel GC | 높은 처리량 | 긴 일시정지 시간 | 배치 처리 |
| CMS GC | 짧은 일시정지 | 메모리 단편화 | 작은 힙 크기 |
| G1 GC ⭐ | 예측 가능한 일시정지 | 약간의 오버헤드 | 웹 애플리케이션 |
G1 GC 선택 이유:
- 4GB 이상 힙에서 최적 성능
- 웹 애플리케이션의 응답시간 중시
- 예측 가능한 GC 일시정지 시간
GC 일시정지 시간: -XX:MaxGCPauseMillis=100
사용자 체감 지연시간:
├── 50ms 이하: 즉시 반응 (이상적)
├── 100ms 이하: 거의 느끼지 못함 ← 목표 설정
├── 200ms 이하: 약간 느림 (G1 기본값)
└── 500ms 이상: 명확히 느림
- 100ms 선택 이유: 사용자 경험과 GC 효율성의 균형점
- 트레이드오프: 더 짧게 하면 GC 빈도 증가, 더 길게 하면 사용자 체감 지연
Tomcat 설정 최적화
Tomcat 스레드 풀의 중요성
Tomcat이란?
- Spring Boot에 내장된 웹 서버
- 사용자의 HTTP 요청을 받아서 처리하는 역할
- 실생활 비유: 웹 애플리케이션의 "접수 창구"
스레드 풀(Thread Pool)이란?
- 동시에 여러 요청을 처리하기 위해 미리 생성해둔 스레드들의 집합
- 요청이 들어오면 놀고 있는 스레드를 할당해서 처리
- 실생활 비유: 은행의 "창구 직원들"
왜 스레드 풀 설정이 중요할까?
- 스레드 부족: 설정이 너무 작으면 요청 대기 시간 증가
- 리소스 낭비: 설정이 너무 크면 메모리와 CPU 낭비
- 시스템 불안정: 부적절한 설정으로 서버 다운 위험
Spring Boot에 내장된 Tomcat은 스레드 풀을 사용해서 동시 요청을 처리한다. 스레드 풀 설정이 부적절하면 높은 부하에서 병목이 발생한다.
핵심 Tomcat 설정들
스레드 관련 설정:
max-threads: 동시에 처리할 수 있는 최대 요청(스레드)의 수- 실생활 비유: 은행의 "총 창구 직원 수"
accept-count: 모든 스레드가 사용 중일 때, 들어오는 요청을 대기시킬 수 있는 큐의 크기- 실생활 비유: 은행의 "대기 줄 최대 길이"
max-connections: 서버가 수락하고 유지할 수 있는 총 연결의 수- 실생활 비유: 은행 건물에 "동시에 들어올 수 있는 최대 고객 수"
각 설정의 역할:
사용자 요청 → max-connections (연결 수락) → max-threads (처리) → accept-count (대기)
최적화된 Tomcat 설정 예시
# application.properties
server.tomcat.threads.max=400
server.tomcat.accept-count=500
server.tomcat.max-connections=10000
설정값 계산 과정:
최대 스레드 수: max=400
1단계: 필요한 스레드 수 계산
필요 스레드 수 = 목표 TPS × 평균 응답시간(초)
= 200 TPS × 0.5초 = 100개
2단계: 안전 마진 적용
실제 설정값 = 계산값 × 안전 계수
= 100 × 4 = 400개
안전 계수 4배를 적용하는 이유:
- 응답시간 변동: 평균 0.5초이지만 최대 2초까지 가능
- 트래픽 급증: 순간적인 트래픽 스파이크 대응
- DB 지연: 데이터베이스 응답 지연 시 버퍼 역할
대기 큐 크기: accept-count=500
대기 큐 설정 기준:
├── 너무 작으면: 요청 거부 발생 (503 에러)
├── 적절한 크기: 일시적 부하 흡수 ← 500개 설정
└── 너무 크면: 메모리 낭비, 응답 지연 증가
500개 선택 근거:
- 최대 스레드 수(400)의 125% 수준
- 1-2초간의 트래픽 스파이크 흡수 가능
- 메모리 사용량과 응답성의 균형점
최대 연결 수: max-connections=10000
연결 수 vs 스레드 수:
├── 연결 수 > 스레드 수 (일반적)
├── Keep-Alive로 연결 재사용
└── 비활성 연결도 유지 필요
10000개 선택 근거:
- 스레드 수(400)의 25배 수준
- Keep-Alive 연결 고려
- 시스템 리소스 한계 내에서 여유 확보
🗄️ 데이터베이스 커넥션 풀 최적화
커넥션 풀이 필요한 이유
DB 커넥션(Connection)이란?
- 애플리케이션과 데이터베이스 간의 연결 통로
- 데이터를 주고받기 위해 반드시 필요한 연결
- 실생활 비유: 은행과 고객을 연결하는 "전용 창구"
커넥션을 매번 새로 만들면 안 되는 이유:
커넥션 생성 과정 (매우 비싼 작업):
1. 네트워크 연결 설정 (TCP 핸드셰이크)
2. 인증 정보 확인
3. 세션 초기화
4. 권한 확인
→ 총 소요시간: 수십~수백 밀리초
커넥션 풀(Connection Pool)이란?
- 미리 일정량의 DB 커넥션을 만들어두고 재사용하는 방식
- 요청이 들어올 때마다 빌려주고 반납받아 재사용
- 실생활 비유: 은행의 "창구 운영 시스템" (창구를 미리 열어두고 고객이 오면 배정)
커넥션 풀의 장점:
- 성능 향상: 커넥션 생성 시간 제거
- 리소스 절약: 불필요한 커넥션 생성 방지
- 안정성: 커넥션 수 제한으로 DB 보호
DB 커넥션을 생성하는 과정은 비용이 매우 높은 작업이다. 커넥션 풀은 미리 일정량의 DB 커넥션을 만들어두고, 요청이 들어올 때마다 빌려주고 반납받아 재사용하는 방식이다.
HikariCP 최적화 설정
HikariCP란?
- Spring Boot에서 기본으로 사용하는 커넥션 풀 라이브러리
- "빠르다"는 뜻의 일본어 "히카리(光)"에서 이름을 따옴
- 빠른 성능과 안정성으로 업계 표준으로 자리잡음
- 실생활 비유: 가장 효율적인 "창구 관리 시스템"
HikariCP의 장점:
- 빠른 성능: 다른 커넥션 풀 대비 2-3배 빠른 속도
- 가벼운 용량: 최소한의 메모리 사용
- 안정성: 커넥션 누수 방지 기능 내장
Spring Boot의 기본 커넥션 풀인 HikariCP의 성능을 극대화하기 위한 설정이다.
핵심 설정:
maximum-pool-size: 커넥션 풀이 가질 수 있는 최대 커넥션 수- 권장사항: 일반적으로 Tomcat의
max-threads수와 비슷하거나 약간 더 크게 설정
최적화된 HikariCP 설정 예시
# application.properties
spring.datasource.hikari.maximum-pool-size=400 # Tomcat max-threads와 맞춤
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
설정값 선택 근거:
최대 커넥션 수: maximum-pool-size=400
기본 원칙: Tomcat 스레드 수와 동일하게 설정
Tomcat 스레드 400개 = DB 커넥션 400개
├── 각 스레드가 DB 작업 시 커넥션 1개 필요
├── 스레드 > 커넥션: 커넥션 대기 발생
└── 커넥션 > 스레드: 불필요한 리소스 낭비
단, DB 인스턴스 제약 확인 필요:
| RDS 인스턴스 | 최대 커넥션 수 | 권장 설정 |
|---|---|---|
| db.t3.micro | ~80개 | 60개 |
| db.t3.small | ~150개 | 120개 |
| db.t3.medium | ~300개 | 250개 |
| db.m5.large | ~1000개+ | 400개 ✅ |
최소 유지 커넥션: minimum-idle=10
최소 커넥션 설정 기준:
├── 너무 적으면: 초기 요청 시 커넥션 생성 지연
├── 적절한 수준: 기본 트래픽 처리 가능 ← 10개
└── 너무 많으면: 불필요한 리소스 점유
10개 선택 이유:
- 평상시 트래픽(20-30 TPS) 처리 가능
- 전체 풀 크기(400)의 2.5% 수준으로 적절
커넥션 타임아웃: connection-timeout=30000 (30초)
타임아웃 설정 고려사항:
├── 너무 짧으면: 일시적 부하 시 에러 발생
├── 적절한 시간: 사용자 대기 한계 고려 ← 30초
└── 너무 길면: 장애 상황에서 복구 지연
유휴 커넥션 정리: idle-timeout=600000 (10분)
유휴 시간 = 사용되지 않는 커넥션을 정리하는 시간
├── 5분 이하: 너무 자주 정리 (오버헤드)
├── 10분: 적절한 균형점 ← 설정값
└── 30분 이상: 불필요한 커넥션 장시간 유지
커넥션 최대 생존시간: max-lifetime=1800000 (30분)
생존시간 설정 이유:
├── DB 서버의 wait_timeout 설정보다 짧게 설정
├── 네트워크 장비의 연결 해제 방지
└── 커넥션 누수 방지를 위한 강제 갱신
MySQL 기본 wait_timeout: 8시간
- HikariCP 설정: 30분 (안전 마진 확보)
- 주기적 커넥션 갱신으로 안정성 향상
🎯 설정 시 고려사항
1. 시스템 리소스와의 균형
메모리 계산:
전체 메모리 = JVM 힙 + JVM 비힙 + OS + 기타 프로세스
CPU 고려사항:
- 스레드 수가 CPU 코어 수보다 너무 많으면 컨텍스트 스위칭 오버헤드 발생
- I/O 대기가 많은 애플리케이션은 CPU 코어 수의 2-4배까지 가능
2. 데이터베이스 제약사항
RDS 인스턴스별 최대 커넥션 수:
db.t3.micro: 약 80개db.t3.small: 약 150개db.t3.medium: 약 300개
커넥션 풀 크기는 DB 인스턴스의 최대 커넥션 수를 초과하면 안 된다.
3. 애플리케이션 특성 반영
I/O 집약적 애플리케이션:
- 더 많은 스레드 필요 (I/O 대기 시간 동안 다른 요청 처리)
- 커넥션 풀 크기도 상대적으로 크게 설정
CPU 집약적 애플리케이션:
- 스레드 수를 CPU 코어 수에 맞춰 제한
- 메모리 사용량에 더 주의
📊 모니터링 포인트
JVM 모니터링
- GC 시간과 빈도: 너무 자주 발생하거나 시간이 길면 힙 크기 조정 필요
- 힙 메모리 사용률: 80% 이상 지속되면 메모리 부족 신호
- OutOfMemoryError: 힙 크기 증가 또는 메모리 누수 확인 필요
Tomcat 모니터링
- 활성 스레드 수: max-threads에 근접하면 스레드 부족 상황
- 큐 대기 시간: accept-count가 가득 차면 요청 거부 발생
- 응답 시간 증가: 스레드 경합이나 리소스 부족 신호
DB 커넥션 풀 모니터링
- 커넥션 풀 사용률: 90% 이상 지속되면 풀 크기 증가 고려
- 커넥션 대기 시간: connection-timeout 에러 발생 시 풀 크기 부족
- 커넥션 누수: 사용 후 반납되지 않는 커넥션 감지
적용
1. 단계별 적용
한 번에 모든 설정을 바꾸지 말고 단계별로 적용해서 효과를 측정하자:
- JVM 설정 변경 → 테스트
- Tomcat 설정 변경 → 테스트
- DB 커넥션 풀 설정 변경 → 테스트
2. 부하 테스트 필수
설정 변경 후에는 반드시 부하 테스트를 통해 효과를 검증하자:
- 목표 TPS 달성 여부
- 응답 시간 개선 여부
- 에러율 변화
- 리소스 사용률 변화
3. 환경별 설정 분리
개발, 스테이징, 프로덕션 환경별로 다른 설정을 사용하자:
# application-dev.properties (개발환경)
server.tomcat.threads.max=50
spring.datasource.hikari.maximum-pool-size=10
# application-prod.properties (프로덕션환경)
server.tomcat.threads.max=400
spring.datasource.hikari.maximum-pool-size=100
마무리
서버 리소스 최적화는 성능 튜닝의 기본기다. 이런 기본 설정이 제대로 되어 있어야 그 위에 애플리케이션 레벨의 최적화(쿼리 튜닝, 캐싱 등)가 효과를 발휘할 수 있다.
핵심 포인트 정리:
- JVM: 힙 크기와 GC 알고리즘이 성능의 기본
- Tomcat: 스레드 풀 크기가 동시 처리 능력을 결정
- DB 커넥션 풀: 데이터베이스 병목을 방지하는 핵심 설정
- 모니터링: 설정 효과를 측정하고 지속적으로 개선
'Jungle' 카테고리의 다른 글
| 정글 수료 후 작성하는 회고 (0) | 2025.08.03 |
|---|---|
| Spring Properties 파일 리팩터링 (0) | 2025.08.03 |
| JPQL 프로젝션으로 N+1 문제와 Over-fetching 해결하기 (0) | 2025.08.02 |
| 커버링 인덱스로 11초 쿼리를 100ms로 단축시키기: 실제 장애 해결 과정 (0) | 2025.08.02 |
| Redis 캐싱 도입기: 예상과 다른 결과와 문제 해결 과정 (151 → 146 TPS) (0) | 2025.08.01 |