TIL - 20251118

juni·2025년 11월 18일

TIL

목록 보기
181/316

1118 Spring Boot 성능 최적화: JVM, GC, nGrinder


✅ 1. JVM 메모리 구조의 이해

  • Java 애플리케이션(Spring Boot 포함)은 JVM(Java Virtual Machine) 위에서 동작하며, JVM은 OS로부터 할당받은 메모리를 자신만의 규칙에 따라 여러 영역으로 나누어 관리합니다. 이 구조를 이해하는 것은 메모리 누수(Memory Leak)나 성능 문제를 분석하는 데 필수적입니다.

➕ JVM 메모리의 주요 영역

  1. 메서드 영역 (Method Area):

    • 클래스의 메타데이터(클래스 정보, static 변수, 상수 등)가 저장되는 공간입니다.
    • JVM이 시작될 때 생성되며, 모든 스레드가 공유합니다.
  2. 힙 영역 (Heap Area):

    • 객체(Object)와 배열이 생성되고 저장되는 공간입니다.
    • 가비지 컬렉션(GC)이 발생하는 주된 영역이며, 모든 스레드가 공유합니다.
    • 힙 영역은 다시 여러 세대(Generation)로 나뉩니다.
      • Young Generation: 새롭게 생성된 객체들이 위치하는 곳. 이곳에서 발생하는 GC를 Minor GC라고 합니다.
        • Eden: 객체가 최초로 생성되는 공간.
        • Survivor 0 / 1: Eden 영역에서 살아남은 객체들이 잠시 머무는 공간.
      • Old Generation: Young Generation에서 여러 번의 Minor GC를 거치고도 살아남은, "오래된" 객체들이 이동하는 곳. 이곳에서 발생하는 GC를 Major GC(또는 Full GC)라고 합니다.
  3. 스택 영역 (Stack Area):

    • 메서드 호출과 관련된 정보(지역 변수, 매개변수, 리턴 주소 등)가 저장되는 공간입니다.
    • 스레드마다 별도의 스택을 가집니다. (공유되지 않음)
    • 메서드가 호출되면 스택 프레임(Stack Frame)이 쌓이고, 메서드가 종료되면 해당 프레임이 제거됩니다.

✅ 2. 가비지 컬렉션 (GC - Garbage Collection)

  • GC힙(Heap) 영역에서 더 이상 어떤 곳에서도 참조되지 않는 "쓰레기(Garbage)" 객체들을 찾아내어, 이들이 차지하고 있던 메모리를 자동으로 회수하는 JVM의 핵심 기능입니다.

  • GC의 기본 원리 (Stop-the-World):

    • GC가 실행되는 동안에는, GC를 실행하는 스레드를 제외한 모든 애플리케이션 스레드가 일시적으로 중단됩니다. 이를 "Stop-the-World"라고 합니다.
    • 이 중단 시간(pause time)이 길어지면 애플리케이션의 응답 지연(latency)이 발생하여 성능에 직접적인 영향을 미칩니다.

➕ 세대별 가설 (Generational Hypothesis)과 GC 동작

  • GC는 "대부분의 객체는 금방 죽는다"와 "오래된 객체는 새로운 객체를 거의 참조하지 않는다"는 가설을 기반으로, 힙 영역을 Young/Old 세대로 나누어 효율적으로 동작합니다.

    1. 새로운 객체는 Eden 영역에 생성됩니다.
    2. Eden 영역이 꽉 차면 Minor GC가 발생하고, 살아남은 객체들은 Survivor 영역으로 이동합니다.
    3. 이 과정을 반복하며, 여러 번의 Minor GC에서 살아남아 일정 "나이"를 먹은 객체들은 Old Generation으로 승격(Promotion)됩니다.
    4. Old Generation 영역이 꽉 차면, Major GC(Full GC)가 발생합니다. Major GC는 Minor GC보다 훨씬 더 많은 메모리를 검사하므로, Stop-the-World 시간이 더 깁니다.
  • 성능 튜닝의 목표: 애플리케이션의 특성에 맞게 JVM 옵션을 조절하여, Major GC(Full GC)의 발생 빈도와 Stop-the-World 시간을 최소화하는 것이 일반적인 GC 튜닝의 목표입니다.


✅ 3. 부하 테스트 도구: nGrinder

  • nGrinder는 네이버에서 개발한 오픈소스 부하 테스트(Load Test) 플랫폼입니다. 애플리케이션이 실제 운영 환경에서 어느 정도의 부하(사용자 수, 요청 수)를 견딜 수 있는지 성능을 측정하고, 병목 지점을 식별하기 위해 사용됩니다.

➕ nGrinder의 주요 구성 요소

  1. Controller:

    • 테스트를 관리하고 제어하는 중앙 서버입니다.
    • 사용자는 웹 UI를 통해 Controller에 접속하여 테스트 시나리오를 작성하고, 테스트를 시작/중지하며, 결과를 모니터링합니다.
  2. Agent:

    • 실제로 부하를 발생시키는 주체입니다.
    • Controller의 지시에 따라, 여러 가상 사용자(Virtual User)를 생성하여 타겟 서버(우리의 Spring Boot 애플리케이션)에 대량의 요청을 보냅니다.
    • 여러 대의 서버에 Agent를 설치하여 대규모 테스트를 수행할 수 있습니다.

➕ nGrinder를 이용한 성능 테스트 흐름

  1. 환경 구축: Controller와 Agent를 서버(e.g., EC2 인스턴스)에 설치하고 실행합니다.
  2. 스크립트 작성: Groovy 또는 Jython 스크립트를 사용하여 테스트 시나리오를 작성합니다. (e.g., "1초에 10번씩 /api/products/1을 GET 요청한다.")
  3. 성능 테스트 설정: Controller의 웹 UI에서 가상 사용자 수, 테스트 시간, 테스트 스크립트 등을 설정합니다.
  4. 테스트 실행 및 모니터링: 테스트를 시작하면, Controller는 Agent에게 명령을 내려 부하를 발생시키고, 실시간으로 TPS(Transactions Per Second, 초당 처리량), 응답 시간, 에러율 등의 성능 지표를 수집하여 그래프로 보여줍니다.
  5. 결과 분석: 테스트가 끝난 후, 상세 리포트를 통해 애플리케이션의 성능 한계와 병목 지점을 분석하고, 이를 바탕으로 코드 수정, DB 튜닝, 인프라 확장 등의 최적화 작업을 진행합니다.

📌 요약

  • Spring Boot 애플리케이션의 성능을 이해하려면, 그 기반이 되는 JVM 메모리 구조(Heap, Stack)를 알아야 합니다.
  • GC(가비지 컬렉션)는 힙 영역의 메모리를 자동으로 관리해주지만, 특히 Major GC(Full GC)"Stop-the-World"를 유발하여 성능 저하의 주된 원인이 될 수 있습니다.
  • nGrinder와 같은 부하 테스트 도구를 사용하여, 애플리케이션에 의도적으로 부하를 가하고 TPS, 응답 시간 등의 성능 지표를 측정해야만, 코드나 인프라의 병목 지점을 과학적으로 식별하고 개선할 수 있습니다.

0개의 댓글