왜 JFR 대응 절차가 필요한가

Java 서비스 장애에서 가장 아까운 순간은 원인을 확인하기 전에 프로세스를 재시작해 버리는 때입니다. CPU 급등, 응답 지연, 메모리 증가, 반복적인 Full GC는 모두 그 순간의 증거를 남깁니다. Java Flight Recorder, 즉 JFR은 JVM에 내장된 이벤트 기록 기능이고, jcmd는 실행 중인 JVM에 진단 명령을 보내는 기본 도구입니다. 둘을 운영 표준으로 묶어두면 별도 에이전트 없이도 재시작 전 CPU 샘플, GC, 스레드 대기, 파일·소켓 I/O, 객체 할당 흐름을 기록할 수 있습니다.

JFR은 OpenJDK에 포함된 기능이며 JDK Mission Control로 시각화할 수 있습니다. 하지만 현장에서는 분석 도구보다 먼저 대상 프로세스, 기록 시간, 저장 경로, 실행 권한을 정해두는 것이 중요합니다.

1단계: 대상 JVM을 찾고 짧게 녹화하기

장애 대응용 첫 JFR은 길게 잡지 않습니다. 보통 2분에서 5분이면 CPU 과다 사용, 락 경합, 외부 호출 지연, GC 압박의 방향을 확인하기에 충분합니다. profile 설정은 default보다 더 많은 프로파일링 정보를 수집하므로 장애 순간 분석에 적합하지만, 장시간 상시 사용보다는 제한된 시간으로 실행하는 편이 안전합니다.

jcmd

# PID 18421이 대상 Java 애플리케이션이라고 가정
jcmd 18421 JFR.start   name=incident-cpu   settings=profile   duration=180s   filename=/var/log/myapp/jfr/incident-$(date +%Y%m%d-%H%M%S).jfr

# 현재 녹화 상태 확인
jcmd 18421 JFR.check name=incident-cpu

# 필요 시 즉시 덤프 후 종료
jcmd 18421 JFR.dump name=incident-cpu filename=/var/log/myapp/jfr/manual-dump.jfr
jcmd 18421 JFR.stop name=incident-cpu

명령은 Java 프로세스를 실행한 사용자와 같은 권한으로 수행해야 합니다. 저장 디렉터리는 쓰기 권한과 보관 기간을 확인하고, 파일명에는 서비스명, 호스트명, 시간, 장애 유형을 넣어둡니다.

2단계: CPU와 응답 지연을 같은 시간축에서 보기

CPU 사용률이 높다는 사실만으로는 원인이 부족합니다. 바쁜 계산, JSON 직렬화, 정규식 백트래킹, 압축·암호화, GC, 재시도 루프는 모두 다르게 대응해야 합니다. JFR에서는 Hot Methods와 Method Profiling을 먼저 보고, 같은 시간대에 Garbage Collection, Java Monitor Blocked, Socket Read 이벤트가 겹치는지 확인합니다. CPU 샘플 상위가 비즈니스 로직인지 라이브러리 내부인지, 특정 스레드 풀에 몰려 있는지도 함께 봐야 합니다.

응답 지연은 “일하고 있는 지연”인지 “기다리는 지연”인지 구분합니다. 작업 스레드가 Socket Read에 오래 머물면 외부 API나 DB 대기가 의심되고, Java Monitor Blocked가 길게 쌓이면 synchronized 블록, 커넥션 풀, 캐시 갱신 락이 병목일 수 있습니다.

3단계: 메모리 증가와 GC 압박 확인하기

메모리 문제는 누수와 순간적인 할당 폭증을 구분해야 합니다. JFR의 Object Allocation Sample, GC Heap Summary, Garbage Collection 이벤트를 보면 어떤 타입이 계속 할당되는지, Old 영역이 회수되는지, Full GC가 반복되는지 확인할 수 있습니다. 힙 덤프는 파일이 크고 서비스에 부담을 줄 수 있으므로, 먼저 JFR과 힙 요약으로 방향을 잡은 뒤 필요한 경우에만 남기는 순서가 안전합니다.

# 부담이 낮은 1차 확인 명령
jcmd 18421 GC.heap_info
jcmd 18421 GC.class_histogram

# systemd 서비스라면 JFR 저장 경로를 미리 준비
sudo install -o myapp -g myapp -m 0750 -d /var/log/myapp/jfr

GC.class_histogram은 객체 타입별 인스턴스 수와 메모리 사용량을 보여줍니다. 특정 DTO, byte 배열, 문자열, 세션 객체, 캐시 엔트리가 비정상적으로 많다면 누수 후보를 좁힐 수 있습니다.

4단계: 업무 이벤트를 직접 남기기

JFR은 JVM 내부 이벤트뿐 아니라 애플리케이션 이벤트도 기록할 수 있습니다. 결제 승인, 파일 변환, 대량 엑셀 업로드, 외부 API 호출처럼 장애 분석 가치가 높은 경계에 커스텀 이벤트를 넣어두면 JVM 지표와 업무 흐름을 같은 타임라인에서 볼 수 있습니다. 민감정보와 토큰은 절대 기록하지 말고, 제공자명, 상태 코드, 내부 작업 ID처럼 최소 정보만 남깁니다.

import jdk.jfr.Event;
import jdk.jfr.Label;
import jdk.jfr.Name;

@Name("com.company.ExternalApiCall")
@Label("External API Call")
class ExternalApiCallEvent extends Event {
    @Label("Provider") String provider;
    @Label("Status Code") int statusCode;
}

public String callPartnerApi(String provider) {
    var event = new ExternalApiCallEvent();
    event.provider = provider;
    event.begin();
    try {
        String result = partnerClient.request(provider);
        event.statusCode = 200;
        return result;
    } catch (RuntimeException e) {
        event.statusCode = 500;
        throw e;
    } finally {
        event.commit();
    }
}

운영 체크리스트

  • 재시작 전에 최소 3분 JFR을 남기는 원칙을 정합니다.
  • JFR 저장 경로, 권한, 디스크 보관 정책을 미리 검증합니다.
  • 상시 관찰은 default, 장애 분석은 profile처럼 목적별 설정을 나눕니다.
  • CPU, 락, I/O, GC, 할당 이벤트를 같은 시간축에서 함께 해석합니다.
  • 외부 호출과 배치 단계에는 민감정보 없는 커스텀 JFR 이벤트를 추가합니다.
  • JFR 파일과 함께 장애 시각, 배포 버전, 트래픽 변화, 외부 API 상태를 기록합니다.

정리하면 JFR과 jcmd는 Java 장애를 추측이 아니라 증거로 바꾸는 기본 도구입니다. 중요한 것은 기능을 아는 데서 끝내지 않고, 명령과 권한과 저장 위치를 운영 문서에 고정해 두는 것입니다. 이 절차가 준비되어 있으면 다음 장애 때 단서를 지우지 않고 원인을 재현 가능한 데이터로 좁혀갈 수 있습니다.