본문 바로가기

카테고리 없음

[GC편 #4] GC 튜닝 전/후 로그 비교 사례: 숫자로 증명하는 성능 개선

들어가며

앞선 글에서 우리는

  • GC 로그를 수집하는 방법
  • 로그 한 줄을 해석하는 방법

을 살펴봤습니다. 이번 편에서는 그 지식을 실제로 활용해

GC 튜닝 전과 후가 로그에서 어떻게 달라지는지

를 비교 분석합니다.

이 글의 목표는 단순합니다.

  • GC 튜닝이 실제로 효과가 있는지
  • 어떤 변경이 어떤 수치 변화를 만드는지
  • 로그를 통해 개선 여부를 어떻게 판단하는지

를 명확히 보여주는 것입니다.


1. 사례 개요

시스템 환경

  • ASP.NET Core API 서버
  • 8 Core / 16 Thread
  • Server GC 활성화
  • 동시 요청 수: 약 500

증상

  • 5~10분 간격으로 응답 지연 발생
  • 평균 응답 시간 200ms → 최대 2초

2. 튜닝 전 GC 로그 분석

주요 로그 발췌

GC(0) Pause 6ms
GC(1) Pause 18ms
GC(2) Pause 820ms

Heap Size: 3200MB -> 3050MB
LOH Size: 2100MB

문제점 정리

  • Gen 2 GC 발생 주기: 약 8분
  • Pause Time: 800ms 이상
  • LOH가 힙의 대부분 차지
  • GC 후에도 힙이 거의 줄지 않음

장수 대형 객체가 누적되는 전형적인 패턴입니다.


3. 원인 코드 분석

문제 코드

public byte[] HandleRequest()
{
    // 요청마다 대형 버퍼 생성
    return new byte[200_000];
}
  • 매 요청마다 LOH 객체 생성
  • 멀티스레드 환경에서 빠르게 누적

4. 1차 튜닝: ArrayPool 적용

개선 코드

public byte[] HandleRequest()
{
    var buffer = ArrayPool<byte>.Shared.Rent(200_000);
    try
    {
        return Process(buffer);
    }
    finally
    {
        ArrayPool<byte>.Shared.Return(buffer);
    }
}

5. 튜닝 후 GC 로그 변화

로그 발췌

GC(0) Pause 5ms
GC(1) Pause 14ms
GC(2) Pause 95ms

Heap Size: 1800MB -> 1200MB
LOH Size: 350MB

변화 요약

항목튜닝 전튜닝 후

Gen2 Pause 820ms 95ms
LOH Size 2.1GB 350MB
Heap 감소폭 미미 명확

➡ 로그만 봐도 문제가 해결됐다는 것이 명확합니다.


6. 2차 튜닝: 객체 수명 단축

기존 코드

static List<byte[]> Cache = new();
  • 불필요한 장수 객체

개선 코드

using var cache = new List<byte[]>();
  • 요청 단위 생명주기 적용

7. 최종 로그 결과

GC(2) Pause 60ms
Heap Size: 1400MB -> 900MB
  • Gen 2 GC 주기 증가
  • Pause Time 안정화
  • 응답 지연 현상 제거

8. GC 튜닝 효과를 판단하는 기준

GC 튜닝이 성공했는지는 다음 질문으로 판단할 수 있습니다.

✔ Gen 2 GC가 줄었는가?
✔ Pause Time이 100ms 이하인가?
✔ LOH가 관리 가능한 수준인가?
✔ GC 후 힙이 눈에 띄게 줄어드는가?

하나라도 아니면 추가 튜닝이 필요합니다.


마무리

GC 튜닝은 감으로 하는 작업이 아닙니다.

반드시 로그로 전과 후를 비교해야 합니다.

숫자가 바뀌지 않았다면,

  • 코드는 깔끔해졌을지 몰라도
  • 성능은 바뀌지 않았을 가능성이 큽니다.
반응형