← Posts

Observability · Logging · Structured Logging

로그 - 무슨 일이 있었나

로그는 사건의 기록이다. 문자열로 찍으면 사람만 읽고 기계는 못 읽는다. 구조화 로깅으로 필드를 남기고, 상관ID로 흩어진 줄을 한 요청으로 꿰는 법까지.

목차
  1. 로그는 사건의 기록이다
  2. 로그 레벨: 얼마나 중요한가
  3. 문자열 로그는 기계가 못 읽는다
  4. 구조화 로깅: 필드로 남긴다
  5. 상관ID로 한 요청을 꿴다
  6. 무엇을 남기고 무엇은 안 남기나
  7. 로그만으로는 전체가 안 보인다
  8. 실무에서: 중앙으로 모은다
  9. 정리

세 기둥 중 가장 오래되고 손에 익은 게 로그다. 그런데 익숙한 방식 그대로 찍으면 정작 급할 때 안 쓰인다.

로그는 사건의 기록이다

로그는 한 줄이 한 사건이다. 언제, 무슨 일이 있었는지를 시간 순으로 남긴다.

plaintext
2025-07-29 14:03:11  로그인 성공  user=42
2025-07-29 14:03:12  주문 생성   order=1007 user=42
2025-07-29 14:03:15  결제 실패   order=1007 reason=timeout

가장 직관적인 신호다. 코드에 log(...) 한 줄이면 되고, 사람이 눈으로 바로 읽는다. 문제가 나면 제일 먼저 여는 것도 로그다. 그래서 잘 남기는 법이 중요하다.

로그 레벨: 얼마나 중요한가

모든 로그가 같은 무게는 아니다. 그래서 레벨을 매긴다.

  • DEBUG - 개발·디버깅용 상세. 평소엔 끈다.
  • INFO - 정상 흐름의 이정표(로그인·주문 생성).
  • WARN - 이상하지만 아직 고장은 아님(재시도, 느린 응답).
  • ERROR - 실패. 사람이 봐야 한다.

레벨을 나누는 이유는 켜고 끄기 위해서다. 평소엔 INFO 위로만 남겨 양을 줄이고, 문제를 팔 땐 DEBUG를 켜서 자세히 본다. 레벨 없이 전부 같은 급으로 찍으면 중요한 ERROR가 잡소리에 묻힌다.

문자열 로그는 기계가 못 읽는다

여기까진 익숙하다. 그런데 이렇게 찍으면 곧 벽에 부딪힌다.

plaintext
로그인 실패: 사용자 42, IP 1.2.3.4, 3번째 시도

사람은 읽는다. 하지만 기계는 못 읽는다. “IP 1.2.3.4에서 온 실패를 다 세라”, “3번 이상 실패한 사용자를 뽑아라” 같은 걸 하려면 문장을 문자열로 뒤져야 한다(grep 지옥). 형식이 조금만 달라도 새고, 집계는 사실상 불가능하다.

구조화 로깅: 필드로 남긴다

그래서 문장이 아니라 필드로 남긴다. 흔히 JSON을 쓴다.

json
{ "level": "WARN", "msg": "로그인 실패", "user_id": 42, "ip": "1.2.3.4", "attempt": 3 }

이게 구조화 로깅이다. 같은 사건이지만 이제 기계가 읽는다 - user_id=42로 거르고, ip별로 세고, attempt >= 3을 뽑는다. 사람이 읽는 문장(msg)은 그대로 두고, 검색·필터·집계에 필요한 것들을 필드로 뽑아둔 것이다. 로그가 데이터가 되는 지점이다.

상관ID로 한 요청을 꿴다

로그의 진짜 골칫거리는 한 요청이 여러 줄에 흩어진다는 것이다. 위의 로그인·주문·결제 세 줄이 사실 한 사용자의 한 흐름인데, 수천 명이 동시에 들어오면 로그에서 서로 섞여 어느 줄이 한 묶음인지 안 보인다.

그래서 요청이 들어오는 입구에서 고유한 ID 하나를 만들어, 그 요청이 남기는 모든 로그에 함께 찍는다. 이걸 상관ID(correlation ID, request ID)라 한다.

json
{ "trace_id": "a1b2", "msg": "주문 생성", "order": 1007 }
{ "trace_id": "a1b2", "msg": "결제 실패", "order": 1007, "reason": "timeout" }

이제 trace_id=a1b2 하나로 그 요청의 전 과정을 뽑아 읽는다. 이 ID를 어디서 심느냐 - 대개 요청이 처음 닿는 미들웨어에서 만들어 흘려보낸다. 그리고 이 ID를 서비스 경계 너머로 넘기고, 구간마다 걸린 시간까지 붙이면 분산 트레이싱이 된다(트레이싱 편에서).

무엇을 남기고 무엇은 안 남기나

로그는 남기는 만큼 위험도 는다.

  • 민감정보는 절대 안 남긴다 - 비밀번호·토큰·카드번호. 로그는 여러 곳에 복제되고 오래 보관돼서, 한 번 찍히면 유출 표면이 넓어진다. (비밀번호를 되읽지 않는 이유와 같은 결이다.)
  • 과하면 비용이자 소음 - 모든 함수 입출력을 INFO로 찍으면 정작 ERROR를 못 찾고, 저장·검색 비용만 커진다.

로그의 원칙은 “다 찍기”가 아니라 **“급할 때 답이 되는 것만 골라 찍기”**다.

로그만으로는 전체가 안 보인다

로그를 아무리 잘 남겨도 못 하는 게 있다. “지금 전체가 어떤가.” 초당 수천 줄이 쌓이는데, “지금 에러율이 몇 %인지”, “응답이 평소보다 느린지”를 로그 줄을 세어 알 수는 없다.

로그는 개별 사건에 강하고 전체 집계에 약하다. 그 약한 자리를 메우는 게 다음 신호, 메트릭이다.

실무에서: 중앙으로 모은다

서버가 여러 대면 로그도 여러 곳에 흩어진다. 그래서 실무는 한곳으로 모은다(중앙 로그 수집).

  • 구조화 + 상관ID를 갖춰두면, 모아둔 로그에서 검색·집계가 산다. 이 둘이 없으면 모아둬도 grep 지옥이 커질 뿐이다.
  • 레벨은 환경별로 - 운영은 INFO 위, 문제를 팔 땐 일시적으로 DEBUG.
  • 양과 보관 기간을 정한다 - 다 영원히 두면 비용이 감당 안 된다.

정리

  • 로그는 한 줄이 한 사건인 기록이다. 레벨(DEBUG~ERROR)로 무게를 나눠 켜고 끈다.
  • 문자열로 찍으면 사람만 읽고 기계는 못 읽는다 - 필드로 남기는 구조화 로깅이 검색·집계를 살린다.
  • 상관ID로 흩어진 줄을 한 요청으로 꿴다. 이 ID가 서비스를 넘으면 트레이싱이 된다.
  • 민감정보는 안 남기고, 로그만으론 전체 집계가 약하다 - 그건 메트릭의 몫이다.

다음 글은 개별 사건을 수로 집계하는 신호, 메트릭으로 간다.

관련 글