
장애가 났을 때 로그를 열었는데 원인을 못 찾은 경험이 있다면, 로그를 남기는 방식을 바꿀 때가 된 것이다. 로그는 많이 남긴다고 좋은 것이 아니라 찾을 수 있어야 쓸모가 있다. 이 글은 구조화 로깅으로 바꾸는 방법과, 무엇을 남기고 무엇을 남기지 말아야 하는지를 정리한다.
문자열 로그는 검색할 수 없다
흔한 로그는 이렇게 생겼다.
2026-09-11 13:02:11 INFO 주문 처리 완료 - 주문번호: 10023, 사용자: 42, 금액: 35000원
사람이 읽기에는 괜찮다. 문제는 기계가 읽을 수 없다는 것이다. "금액이 10만 원 넘는 주문 중 실패한 것"을 찾으려면 정규식을 짜야 하고, 로그 형식이 조금만 달라도 깨진다.
구조화 로깅은 같은 내용을 이렇게 남긴다.
{"ts":"2026-09-11T13:02:11Z","level":"info","msg":"order.completed",
"order_id":10023,"user_id":42,"amount":35000,"trace_id":"a3f9c1"}
이제 amount > 100000 AND level = "error" 같은 조건으로 바로 검색된다. 대시보드에 금액 분포를 그리는 것도 가능하다. 로그가 데이터가 된다.
메시지를 "주문 처리 완료" 대신 "order.completed"처럼 고정된 식별자로 쓰는 것도 요령이다. 문구가 바뀌어도 검색 조건이 안 깨지고, 같은 이벤트를 세기 쉽다.
요청 하나를 꿰는 trace_id
마이크로서비스가 아니더라도 이건 필요하다. 요청 하나가 처리되는 동안 남은 로그를 한 줄로 묶어 볼 수 있어야 한다.

X-Request-Id나 W3C traceparent)로 전달한다.
이게 있으면 장애 대응이 달라진다. 사용자가 "3시 5분쯤 결제가 안 됐어요"라고 하면, 그 시각의 에러 로그에서 trace_id를 찾고 그것으로 전체 흐름을 복원한다. 없으면 여러 서비스의 로그를 시각으로 대조해야 하는데, 초당 수백 건이 찍히는 환경에서는 사실상 불가능하다.
레벨을 실제로 구분해서 쓴다
로그 레벨이 형식적으로만 존재하는 경우가 많다. 기준을 정해두면 유용해진다.
| 레벨 | 기준 | 예 |
|---|---|---|
| ERROR | 사람이 대응해야 함 | 결제 승인 실패, DB 연결 끊김 |
| WARN | 지금은 괜찮지만 추세를 봐야 함 | 재시도 성공, 폴백 동작, 응답 지연 |
| INFO | 정상 흐름의 주요 사건 | 주문 생성, 로그인, 배치 시작·종료 |
| DEBUG | 개발·조사용 상세 | 쿼리 파라미터, 중간 계산값 |
ERROR는 알림과 연결된다는 전제로 써야 한다. 사용자 입력이 잘못된 것(400)까지 ERROR로 찍으면 알림이 계속 울리고, 사람들은 곧 알림을 무시하게 된다. 그러면 진짜 장애도 묻힌다.
반대로 빠지기 쉬운 실수는 예외를 잡고 로그만 남긴 뒤 그냥 넘어가는 것이다.
try:
charge(order)
except Exception as e:
log.error("결제 실패", error=str(e)) # 그리고 끝? 주문은 어떻게 되나
로그는 남았지만 주문 상태는 애매하게 남는다. 로그를 남기는 것과 처리하는 것은 다른 일이다.
예외는 스택 트레이스와 함께
에러 로그에서 가장 중요한 것은 어디서 났는지다. 메시지만 남기면 같은 메시지가 여러 곳에서 나올 때 구분할 수 없다.
# 나쁨 — 어디서 났는지 모른다
log.error("결제 실패: " + str(e))
# 좋음 — 스택 트레이스가 함께 남는다
log.exception("payment.failed", order_id=order.id)
파이썬은 logger.exception(), 자바는 예외 객체를 마지막 인자로 넘기면 스택이 남는다. 구조화 로깅 라이브러리들은 이걸 stack 필드로 분리해 준다.
원인 예외(cause chain)도 중요하다. 라이브러리 예외를 잡아 도메인 예외로 바꿔 던질 때, 원래 예외를 버리면 진짜 원인이 사라진다. 파이썬은 raise MyError() from e, 자바는 생성자에 cause를 넘긴다.
절대 로그에 남기면 안 되는 것
이건 보안 문제라 예외가 없다.
비밀번호, 토큰, API 키, 세션 ID. 로그는 여러 시스템에 복제되고 오래 보관된다. 한 번 들어가면 회수할 수 없다.
주민등록번호, 카드번호, 계좌번호 같은 개인정보. 마스킹해서 남기거나 아예 남기지 않는다. card_last4 정도면 조사에 충분하다.
요청 본문 전체. 편해 보이지만 위 항목이 그대로 들어간다. 필요한 필드만 골라 남긴다.
실수를 막으려면 로거 단에서 거르는 것이 확실하다. 필드 이름에 password, token, secret, authorization이 들어가면 값을 ***로 바꾸는 필터를 넣어둔다. 사람이 매번 조심하는 방식은 언젠가 뚫린다.
로그를 많이 남기는 것이 답은 아니다
마지막으로 균형이다. 로그는 비용이 든다. 저장 비용, 전송 대역, 그리고 디스크 I/O다. 초당 수천 건을 찍는 서비스에서 로그가 성능 병목이 되는 경우가 실제로 있다.
그래서 나는 "나중에 볼 로그"와 "지금 찍는 로그"를 구분하라고 권한다. 장애 조사에 쓸 로그는 INFO 이상으로 남기고, 개발 중 확인용은 DEBUG로 두고 프로덕션에서는 끈다. 반복문 안에서 매 항목마다 로그를 찍는 코드는 대개 DEBUG여야 한다.
그리고 로그만으로 모든 걸 해결하려 하지 않는 편이 낫다. 지표(metric)로 볼 것을 로그로 세지 않는다. "초당 요청 수"나 "평균 응답 시간"은 카운터와 히스토그램으로 수집하는 편이 훨씬 싸다. 로그는 개별 사건의 맥락을 보는 도구이고, 지표는 전체 추세를 보는 도구다. 둘을 섞으면 비용은 로그만큼 들고 효용은 지표만 못하다.
참고 자료
- OpenTelemetry: Logs — 구조화 로그와 트레이스의 연결
- W3C Trace Context — 서비스 간 trace_id 전달 표준
- OWASP Logging Cheat Sheet — 로그에 남기면 안 되는 데이터와 보호 방법
'IT' 카테고리의 다른 글
| 캐시 전략, 무효화와 스탬피드 대응 (0) | 2026.10.14 |
|---|---|
| N+1 쿼리 문제, 발견하고 고치는 법 (0) | 2026.10.13 |
| 인덱스를 만들었는데 쿼리가 느린 이유 (0) | 2026.10.12 |
| AI 코딩 에이전트 시대의 코드 리뷰 (0) | 2026.10.11 |
| Git 브랜치 전략, rebase와 merge 중 무엇을 쓸까 (0) | 2026.10.10 |
