로그 레벨 기준: ERROR·WARN·INFO·DEBUG·TRACE에 무엇을 남길까

반응형

로그 레벨을 정할 때 가장 흔한 기준은 문장의 심각해 보이는 정도다. 그러면 같은 실패가 어떤 곳에서는 ERROR, 다른 곳에서는 WARN으로 남고, 정상 요청이 너무 많은 INFO를 만들어 정작 필요한 신호를 가린다.

레벨은 감정의 강도가 아니라 운영자가 취해야 할 행동과 system의 상태를 표현해야 한다.

이 event가 발생했을 때 service가 약속한 일을 수행하지 못했는가, 자동으로 회복했는가, 운영 개입이 필요한가?

이 질문을 기준으로 삼으면 ERROR·WARN·INFO·DEBUG·TRACE의 경계가 비교적 안정된다.

로그 레벨을 한 문장씩 정의한다

레벨 사용할 때 운영 관점
ERROR 요청·job·핵심 기능이 실패했고 현재 계층에서 정상 처리하지 못함 즉시 조사 또는 alert 후보
WARN 예상 밖 상황이지만 fallback·retry 등으로 현재 기능은 계속됨 반복·증가 시 조사
INFO 정상적인 lifecycle이나 중요한 business·operation event 운영 흐름과 audit 단서
DEBUG 원인 분석에 유용한 내부 decision·state 평소에는 대개 비활성 또는 제한
TRACE 호출·loop·세부 값처럼 가장 미세한 실행 흐름 짧은 진단 구간에만 사용

Log4j 2에는 FATAL 등 더 많은 표준 레벨이 있고 custom level도 만들 수 있다. 반면 SLF4J API에서 흔히 사용하는 기본 method는 ERROR, WARN, INFO, DEBUG, TRACE다. 팀에서 도구별 레벨 차이를 그대로 노출하기보다 service 전체의 의미 계약을 먼저 정하는 편이 좋다.

ERROR: 현재 계층에서 성공으로 바꿀 수 없는 실패

다음은 ERROR 후보에 가깝다.

  • 요청을 정상 응답으로 완료하지 못했다.
  • queue message 처리를 최종 실패로 종료했다.
  • 필요한 database·storage write가 실패했다.
  • process가 시작 조건을 만족하지 못해 종료한다.
  • 데이터 정합성이나 보안 경계가 깨졌을 가능성이 있다.
try {
    paymentService.capture(paymentId);
} catch (PaymentException e) {
    log.error(
        "payment capture failed paymentId={} orderId={}",
        paymentId,
        orderId,
        e
    );
    throw e;
}

예외 객체를 함께 전달하면 stack trace가 보존된다. 문자열 보간으로 e.getMessage()만 남기면 발생 위치와 cause chain을 잃을 수 있다.

그러나 같은 예외를 여러 계층에서 반복해 ERROR로 기록하면 event 하나가 여러 alert처럼 보인다.

repository ERROR
service ERROR
controller ERROR
global exception handler ERROR

예외를 처리하거나 최종 경계를 소유한 곳에서 한 번 충분한 context와 함께 남기고, 다른 계층은 필요할 때만 보강하는 원칙이 필요하다.

WARN: 회복했지만 계속 무시해서는 안 되는 상태

WARN은 실패를 숨기는 낮은 ERROR가 아니다. system이 현재 요청을 계속 수행했지만 정상 경로에서 벗어났음을 뜻한다.

Profile profile;

try {
    profile = profileCache.get(userId);
} catch (CacheTimeoutException e) {
    log.warn("profile cache timeout; fallback to database userId={}", userId);
    profile = profileRepository.findByUserId(userId);
}

database fallback으로 요청은 성공할 수 있다. 하지만 cache timeout이 반복되면 latency와 database 부하가 커질 수 있으므로 관찰할 가치가 있다.

다음은 WARN이 될 수 있다.

  • 첫 시도는 실패했지만 retry 후 성공했다.
  • 기본값이나 fallback 경로로 처리했다.
  • 곧 제거될 deprecated 설정이 사용됐다.
  • resource 사용량이 위험 threshold에 가까워졌다.
  • 외부 입력이 유효하지 않아 거부했으며 service 자체는 정상이다.

모든 4xx를 WARN으로 남길 필요는 없다. internet-facing service의 잘못된 입력은 흔할 수 있다. 양이 많은 정상 거부를 WARN으로 만들면 운영 신호가 약해진다. 빈도와 security 의미를 metric으로 분리해 보는 편이 나을 수 있다.

INFO: 정상 흐름 가운데 운영 가치가 있는 사건

INFO는 “code가 여기까지 왔다”를 찍는 레벨이 아니다. 평상시 production에서 읽어도 가치가 있는 정상 event를 남긴다.

좋은 후보는 다음과 같다.

  • application 시작·종료와 version
  • 주요 configuration의 비민감 요약
  • batch job 시작·완료·처리 건수·소요 시간
  • deployment나 leader 전환 같은 lifecycle event
  • 반드시 추적해야 하는 business 상태 변화
log.info(
    "settlement job completed jobId={} processed={} failed={} durationMs={}",
    jobId,
    processedCount,
    failedCount,
    duration.toMillis()
);

모든 method 진입과 종료를 INFO로 남기면 request 수에 비례해 비용이 늘고 signal-to-noise ratio가 낮아진다. 처리량·오류율·latency는 log뿐 아니라 metric과 trace로 관찰하는 것이 적합하다. SRE 관점의 signal 구분은 SLI·SLO와 운영 지표에서 이어서 볼 수 있다.

DEBUG와 TRACE: 원인 분석을 위한 임시 해상도

DEBUG는 branch 선택, 계산 결과, dependency 호출 전후처럼 문제를 재현하거나 추적할 때 유용한 내부 상태를 남긴다.

log.debug(
    "shipping policy selected region={} memberGrade={} policy={}",
    region,
    memberGrade,
    policyName
);

TRACE는 더 세밀한 흐름이다.

log.trace("candidate evaluated index={} score={}", index, score);

loop 안의 TRACE는 작은 입력에는 괜찮지만 production traffic에서는 엄청난 양이 될 수 있다. 동적으로 level을 높일 수 있다면 다음 안전장치를 함께 둔다.

  • 특정 instance·logger·request 범위로 제한
  • 자동으로 원래 level로 돌아가는 만료 시간
  • 개인·인증·결제 정보가 출력되지 않는지 검토
  • storage와 전송량 증가에 대한 quota·sampling

level을 낮춘다고 민감정보를 기록해도 되는 것은 아니다. DEBUG와 TRACE도 사고 때 외부로 반출되거나 장기 보관될 수 있다.

무엇을 함께 기록해야 검색 가능한가

좋은 log는 문장만 자연스러운 log가 아니라 event를 식별하고 연결할 수 있는 log다. OpenTelemetry Logs Data Model은 timestamp, severity, body뿐 아니라 resource, attributes, trace ID와 span ID 같은 필드를 정의한다.

예를 들어 결제 실패 event라면 다음 context가 유용하다.

{
  "timestamp": "2026-08-02T10:15:32.184Z",
  "severity": "ERROR",
  "event.name": "payment.capture.failed",
  "service.name": "payment-api",
  "service.version": "2026.08.02.1",
  "trace_id": "...",
  "span_id": "...",
  "order_id": "ORD-20260802-001",
  "provider": "example-pay",
  "error.type": "PaymentTimeoutException",
  "retryable": true
}

운영 환경에서는 다음 항목을 일관되게 정하면 좋다.

  • event.name: 검색과 dashboard에 쓸 안정적인 이름
  • service.name, service.version, environment
  • request·trace·span correlation ID
  • domain 식별자: order ID, job ID 등
  • 결과와 duration
  • error type, retry 가능 여부, 최종 시도 횟수

문장 형식은 바뀔 수 있지만 field name이 안정적이면 query와 alert가 덜 깨진다. 반대로 동적인 값을 message에만 붙이면 parsing rule에 의존하게 된다.

절대로 그대로 남기지 말아야 할 값

OWASP Logging Cheat Sheet는 인증 정보와 민감한 개인정보를 직접 기록하지 않도록 권고한다. 대표적인 금지·주의 대상은 다음과 같다.

  • password와 password reset token
  • access token, session ID, API key, signing key
  • 결제 카드 정보와 민감한 금융 정보
  • 주민등록번호 등 법적으로 보호되는 식별자
  • 필요 이상으로 상세한 request·response body
  • database connection string에 포함된 credential
// 나쁜 예
log.debug("login request email={} password={}", email, password);

// 더 나은 방향
log.info("login rejected accountIdHash={} reason={}", accountIdHash, reasonCode);

마스킹도 만능은 아니다. 원본을 수집한 뒤 화면에서만 가리는 것보다 logger에 전달되기 전에 제거하거나 allowlist field만 기록하는 편이 안전하다.

예외 상황별 레벨 결정 순서

레벨이 애매하면 다음 순서로 판단할 수 있다.

  1. 요청·job의 최종 결과는 성공인가? 실패면 ERROR 후보를 검토한다.
  2. 현재 계층이 fallback·retry로 회복했는가? 회복했지만 비정상이면 WARN 후보가 된다.
  3. 정상 운영에서 계속 보아야 하는 state transition인가? 그렇다면 INFO다.
  4. 문제 분석 시에만 필요한 내부 정보인가? DEBUG 또는 TRACE다.
  5. 이미 상위 경계에서 같은 실패를 기록하는가? 중복 log를 줄인다.

예시를 적용하면 다음과 같다.

상황 권장 시작점 이유
사용자가 존재하지 않는 URL 요청 INFO·DEBUG 또는 접근 log 정책 service 고장이 아님
외부 API 첫 호출 실패, retry 성공 WARN 현재 요청은 회복했지만 이상 signal
retry를 모두 소진해 요청 실패 ERROR 약속한 결과를 제공하지 못함
예정된 scheduler job 시작 INFO 운영 lifecycle
query parameter와 내부 branch DEBUG 진단용 내부 state
각 record의 변환 과정 TRACE 매우 세밀하고 양이 많음

이는 절대 규칙이 아니다. service의 error budget, traffic 규모, alert 정책에 맞게 팀의 logging contract로 문서화해야 한다.

로그만으로 해결하려 하지 않는다

로그, metric, trace는 서로 대체재가 아니다.

  • metric: 오류율과 latency가 언제 얼마나 변했는지
  • trace: 한 요청이 여러 service를 어디로 거쳤는지
  • log: 특정 event의 context와 error detail이 무엇인지

같은 trace_id로 세 신호를 연결할 수 있으면 “오류율 상승 → 느린 span 확인 → 해당 event log와 stack trace 확인”의 흐름을 만들 수 있다. 로그 레벨 하나를 잘 정하는 것보다 observability 전체에서 어떤 질문에 답할지 설계하는 편이 중요하다.

운영 체크리스트

새 log를 추가하거나 review할 때 다음을 확인한다.

  1. 이 event의 최종 결과와 운영 행동에 맞는 level인가?
  2. 같은 failure를 여러 계층에서 중복 기록하지 않는가?
  3. message가 아니라 field로 검색할 stable key가 있는가?
  4. trace ID와 service·version context가 연결되는가?
  5. token·password·개인정보가 들어가지 않는가?
  6. 정상 traffic에서 발생량과 storage 비용을 감당할 수 있는가?
  7. log를 기반으로 alert한다면 alert가 실제 조치로 이어지는가?

좋은 logging 기준은 모든 것을 기록하는 것이 아니다. 문제를 발견하고 원인을 좁히는 데 필요한 신호를, 해석 가능한 level과 context로, 감당 가능한 양만 남기는 것이다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글