서버 리소스 최적화 가이드: JVM, Tomcat, DB 커넥션 풀 설정

2025. 8. 2. 18:13·Jungle

애플리케이션 코드 최적화도 중요하지만, 그 전에 코드가 실행되는 환경부터 제대로 설정해야 한다. 아무리 좋은 코드를 작성해도 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. 단계별 적용

한 번에 모든 설정을 바꾸지 말고 단계별로 적용해서 효과를 측정하자:

  1. JVM 설정 변경 → 테스트
  2. Tomcat 설정 변경 → 테스트
  3. 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 커넥션 풀: 데이터베이스 병목을 방지하는 핵심 설정
  • 모니터링: 설정 효과를 측정하고 지속적으로 개선
728x90

'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
'Jungle' 카테고리의 다른 글
  • 정글 수료 후 작성하는 회고
  • Spring Properties 파일 리팩터링
  • JPQL 프로젝션으로 N+1 문제와 Over-fetching 해결하기
  • 커버링 인덱스로 11초 쿼리를 100ms로 단축시키기: 실제 장애 해결 과정
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
  • 공지사항

  • 인기 글

  • 태그

    CSAPP
    트러블슈팅
    알고리즘
    CloudFront
    queue
    python
    k8s
    Spring boot
    자료구조
    AWS
    github actions
    컴퓨터시스템
    DevOps
    S3
    Spring
    EC2
    DB
    IAM
    부하테스트
    어셈블리
  • 02-21 15:43
  • hELLO· Designed By정상우.v4.10.3
ahpicl64
서버 리소스 최적화 가이드: JVM, Tomcat, DB 커넥션 풀 설정
상단으로

티스토리툴바