📕 전자책 전문 읽기 무언가 말해주는 시스템 · 32쪽
브라우저가 PDF를 바로 표시하지 못합니다. 새 탭에서 열기
이 글은 요약해 다시 쓴 것이고, PDF가 전문입니다.

혼자서 프로덕션을 돌리는 개발자를 위한 글입니다. 읽고 나면 지금의 로그가 증거인지 소문인지 판별하는 기준, 그리고 그 기준을 채우는 순서를 갖게 됩니다.

결론부터 말합니다. 로그는 나중에 시스템에게 물릴 질문의 답을 미리 써두는 행위입니다. 시스템은 이미 지난 일을 겪었고 지난 일은 로그에 남겨진 만큼만 남습니다. 남겨지지 않은 일은 일어난 일일지라도 증명이 안 되며, 증명이 안 되는 일은 새벽 2시에는 없는 일과 다르지 않습니다.

새벽 2시에 결제 에러율이 5퍼센트를 넘겼다는 알림이 오면, 그 뒤의 30분은 같은 네 질문으로 채워집니다. 에러율이 언제부터 올라왔나, 어떤 요청이 실패하고 있나, 실패는 내 코드에서 일어나는 건지 외부에서 오는 건지, 어제 내가 건드린 것과 관련이 있는가. 네 답이 로그에 남아 있으면 새벽은 견딜 만하고, 남아 있지 않으면 새벽은 추측 게임이 됩니다. 이 글은 한 가지 주장을 끝까지 따집니다. 로그 줄의 가치는 쓰는 순간에 정해진다.

새벽 2시에는 기억이 가장 위험한 출처다

팀에서 일하면 고장이 났을 때 첫 번째 수단은 질문입니다. 어젯밤 배포한 사람이 있는가, 설정을 건드린 사람이 있는가, 두세 번 물으면 원인이 좁혀집니다. 동료는 살아 있는 문서지만 혼자 시스템이면 살아 있는 문서는 자신뿐이고 새벽 2시의 자신은 위험한 출처입니다. 어젯밤 배포했다는 기억은 있고 워커를 재시작했는지는 기억이 없습니다.

더 위험한 것은 기억이 사실처럼 느껴진다는 점입니다. 확신과 사실은 별개의 것인데, 피곤한 새벽에는 둘을 구분할 에너지가 없습니다. ‘어젯밤 배포했을 것 같다’는 확신은 감정입니다. 그 감정이 다음 조치를 결정하게 되면 추측이 조치의 근거가 됩니다. 추측이 근거가 되는 순간 사고는 한 걸음 더 길어집니다.

그래서 잊지 않는 증인, 즉 로그가 필요합니다. 로그는 설계입니다. print를 지우는 순간 로그가 만들어지는 것이 아니라, 어떤 줄이 어디에, 어떤 형식으로, 어떤 레벨로 남을지를 코드를 쓸 때 정하는 것입니다. 정하지 않으면 시스템은 고장 났을 때 침묵으로 응답하고, 침묵은 최악의 증언입니다.

같은 상황을 두 버전으로 보겠습니다. 결제 요청이 15초 뒤 성공했고 사용자가 지연을 불평했습니다. 빈약한 시스템의 기록은 두 줄입니다.

[14일 09:12] request ok
[14일 09:12] request ok

두 줄은 성공했다는 정도만 알려 줍니다. 15초가 어디에 갔는지는 기록에 없고 불평을 조사하려면 재시도 로직을 다시 읽고 벤더 대시보드를 보고 기억을 더듬어야 하며, 결론까지 40분이 걸립니다. 그 40분은 전부 나 자신에서 나온 것입니다. 규율대로 쓰인 시스템의 기록은 네 줄입니다.

09:12:31 INFO payment call start provider=stripe request_id=req_9f2k attempt=1
09:12:33 ERROR payment call failed provider=stripe request_id=req_9f2k status=502 latency_ms=3120 attempt=1
09:12:34 INFO retry scheduled request_id=req_9f2k next_in_ms=15000
09:12:47 INFO payment call succeeded provider=stripe request_id=req_9f2k latency_ms=210 attempt=2

네 줄이 15초를 설명합니다. 벤더가 한 번 502를 던졌고 재시도 대기 15초, 두 번째 시도가 210밀리초에 성공했다는 사실입니다. 줄의 수가 문제가 아닙니다. 관련이 있는 줄이, 관련을 알 수 있는 형태로, 나중에 찾을 수 있는 자리에 남아 있는가가 문제입니다.

레벨은 독자에게 하는 약속이다

처음에는 모든 것이 중요합니다. 그래서 모든 줄이 INFO가 됩니다. 서버를 시작했습니다, 요청을 받았습니다, 배치 작업을 마쳤습니다, 다 같은 눈높이에 놓입니다. 결과는 하루 10만 줄의 INFO이고 그 가운데 새벽에 필요한 줄이 있습니다. 그런데 찾을 수 없습니다. 찾지 못한 이유는 그 줄과 똑같은 줄이 10만 번 더 있었기 때문입니다.

레벨은 독자에게 하는 약속입니다. 이 레벨의 줄을 보면 얼마나 급하게 행동해야 하는지를 말해 주는 것입니다. ERROR를 보면 일어나야 하고 WARN을 보면 오늘 중에 확인하고 INFO는 흐름을 파악할 때 읽습니다. 약속이 깨지면 반대 현상이 옵니다. ERROR를 봐도 다시 확인해야 하나 하는 마음이 들고 확인하면 또 ERROR입니다. 그 순간 독자는 레벨을 믿지 않게 되고, 레벨을 믿지 않는 시스템은 로그가 없는 것과 다름없습니다.

독자가 한 명뿐이면 약속은 표 하나로 정의됩니다.

레벨 의미 빈도 기대
DEBUG 개발용 상세 요청당 수십 개
INFO 정상 이벤트 요청당 몇 개
WARN 이상인데 자회복 가끔
ERROR 행동이 필요한 실패 목표는 제로

표의 실질적 가치는 마지막 줄입니다. ERROR의 의미가 희석된 시스템에서 ERROR가 매일 여러 번 나면, ‘이 줄이 또 나왔군, 이 정도면 되는 것이지’라는 마음이 듭니다. 그 순간 시스템의 ERROR는 약속을 지키지 못하고 있습니다. 그래서 규칙을 하나만 남깁니다. ERROR는 일어나고 싶은 줄이다. 그 기준에 못 미치면 WARN이고 WARN도 아니라면 INFO입니다.

각 줄을 쓸 때 한 번만 묻습니다. 새벽 2시에 이 줄만 보면, 행동할 수 있는가. 결제 벤더 호출이 실패했다고 하면, 두 줄의 차이는 이렇습니다.

ERROR 결제 실패
ERROR payment call failed provider=stripe status=502 latency_ms=3120 request_id=req_9f2k attempt=2

첫 줄은 뭔가 실패했다는 것만 알립니다. 두 번째 줄은 뭘, 어디에서, 어떻게, 어느 요청이 실패했는지를 알립니다. stripe가 502를 던졌고 두 번째 시도였다는 사실까지. 시작과 판단 사이의 거리가 이 두 줄 사이의 거리입니다. 테스트를 통과하지 못하는 줄은 독자에게 빈칸을 요구하는 줄이고, 빈칸은 컨텍스트로 채워집니다. 그런데 컨텍스트가 최악인 시점이 바로 새벽 2시입니다.

기계가 찾을 수 있는 줄을 쓴다

‘결제 API 2026-03-14 09:12:33 ERROR request req_9f2k failed’ 같은 텍스트 줄은 사람이 읽기는 합니다. 다만 사람만이 읽습니다. 14일에 실패한 요청을 전부 찾으려면, request ID가 줄의 어디쯤 있는지 기억하고 패턴을 맞춰야 합니다. 필드의 순서가 한 칸만 어긋나면 grep이 끊깁니다.

자유 텍스트 로그는 두 가지 병으로 고꾸라집니다. 포맷 추측은 필드의 위치가 줄마다 달라, 검색할 때마다 포맷을 눈으로 확인해야 하는 상태입니다. 어휘 추측은 같은 이벤트가 failed, error occurred, could not complete 처럼 줄마다 다른 단어로 쓰이는 상태입니다. 검색은 패턴이 되고 패턴은 변형에 취약하며 한 단어만 빠져도 그 단어의 줄은 증거에서 사라집니다. 두 병의 치료제는 같습니다. 값에 이름을 붙이는 것, 구조화 로그를 쓰는 것입니다.

값에 이름을 붙인 줄이 구조화 로그입니다.

{"ts":"2026-03-14T09:12:33Z","service":"payment-api","level":"ERROR","msg":"payment call failed","provider":"stripe","status":502,"latency_ms":3120,"request_id":"req_9f2k"}

이 줄에는 두 명의 독자가 있습니다. msg 필드는 사람이 읽는 부분이고 payment call failed, 결제 호출이 실패했다는 구문입니다. 나머지는 기계가 찾는 부분이고 status가 502인 줄, provider가 stripe인 줄, request_id가 req_9f2k인 줄을 각각 전부 골라냅니다. 모든 줄이 공유하는 필드 세트를 다섯 개로 고정하면 검색은 더 이상 추측이 아닙니다. ts는 UTC로 저장된 시간, service는 컴포넌트, level은 약속, msg는 사람이 읽는 구문, request_id는 상관관계입니다. 이벤트마다 provider, status, latency_ms 같은 필드가 그 위에 얹힙니다.

형식은 한 줄에 한 JSON입니다. JSON이 여러 줄에 걸리면 그 줄은 세지도, 카운트되지도, rotation되지도 않습니다. 필드 이름에도 약속이 필요합니다. 소문자 스네이크 케이스, 흔한 필드에는 흔한 이름, 그리고 이름 변경은 브레이킹 체인지입니다. request_id를 한 서비스에서는 req, 다른 서비스에서는 correlation_id로 쓰면 값은 같아도 검색은 다시 추측이 됩니다. 그래서 이름은 한 번 정하면 바꾸지 않고 고쳐야 할 때는 이름을 유지한 채 값을 고칩니다.

request_id가 상관관계 ID의 역할입니다. 하나의 요청은 웹 API를 거쳐 벤더를 부르고 워커가 결과를 다루면, 세 서비스의 세 로그 흐름에 흩어집니다. 열쇠가 없으면 셋은 서로 모르는 세 개의 섬입니다. 진입점에서 고유 ID를 만들고 서비스 경계에서 ID를 요청과 함께 넘기면, req_9f2k를 하나로 검색하는 것만으로 흩어진 줄이 시간 순서로 한 줄이 됩니다. 네 줄의 순서를 읽으면, 사용자가 15초를 기다린 이유는 재시도 대기였고 그 15초는 벤더의 첫 502에서 비롯됐다는 이야기가 됩니다.

payment-api 09:12:31 INFO request received
payment-api 09:12:33 ERROR provider call failed status=502 attempt=1
worker 09:12:34 INFO retry scheduled next_in_ms=15000
payment-api 09:12:47 INFO payment succeeded attempt=2

로그는 다섯 가지 얼굴로 거짓말한다

‘로그가 사실이다’를 구호로 삼으면 안 됩니다. 로그가 거짓말하는 방식은 구조이고 흔한 거짓말은 다섯 가지이며 각자의 해독제가 이미 알려져 있습니다. 얼굴을 알면 의심할 곳이 줄어듭니다. 순서가 뒤집혀 보이면 시계를, 마지막 줄이 없으면 버퍼를, 같은 시각이 두 개면 형식을 의심합니다.

첫 번째는 시계 오차입니다. 두 서버의 시계가 30초씩 틀리면, 뭐가 먼저였는지가 뒤집힙니다. A가 B를 불렀는데 로그에는 B가 먼저 보일 수 있습니다. 원인 분석은 순서에서 시작하므로, 순서가 틀리면 원인도 틀립니다. 해독제는 NTP입니다. 모든 기계가 시간 동기화를 쓰고 가끔 공공 시간 소스와 비교합니다. 서버 두 대가 서로 10초 이상 차이나는 것은 사고의 전조입니다.

두 번째는 유실된 꼬리입니다. 프로세스가 강제 종료되면 버퍼에 남은 줄은 디스크에 도착하지 않고 마지막 몇 줄이 사라집니다. 그런데 죽기 직전의 줄은 가장 중요한 줄인 경우가 많습니다. 메모리에만 있는 줄은 아직 쓰이지 않은 줄입니다. 해독제는 플러시, ERROR 레벨의 줄은 쓰자마자 디스크에 내리고, 버퍼 플러시 간격을 짧게 합니다.

세 번째는 시간대 혼합입니다. 어떤 줄은 UTC이고 어떤 줄은 로컬이면, 09:12가 두 개의 순간을 뜻합니다. 저장할 때는 UTC, 볼 때는 로컬, 이 한 줄의 규칙이 해독제입니다. 네 번째는 에러를 먹는 샘플링입니다. 스토리지를 아끼려고 줄을 골라 내리면, 골라진 줄이 필요한 줄일 수 있습니다. 드문 줄이 값진 줄인데, 드문 줄을 내리면 로그는 흔한 일의 기록이 되고 필요한 순간만 잃습니다. ERROR와 WARN은 절대 샘플링하지 않고 내릴 때는 INFO와 DEBUG만 내립니다.

다섯 번째는 없는 로그입니다. 내린 줄도, 죽은 줄도 아닙니다. 코드가 그 줄을 쓰지 않은 것입니다. 요청이 걸리면 마지막 줄이 없고 답은 아무것도 없습니다. 해독제는 경계, 각 단계의 진입과 출구를 쓰는 것입니다. 그러면 없는 줄이 어디에 있는지가 보이며 죽은 자리는 두 줄 사이의 간격으로 읽힙니다.

존재하는 줄만 읽는 분석은 가장 중요한 증거를 놓칩니다. 증거는 없는 줄에도 있기 때문입니다. 문제 서비스의 시간 창 안에서 마지막 줄을 찾고 다음으로 기대되는 줄이 있는지 봅니다. 마지막 줄이 09:12:31의 request received이고, 다음으로 기대되는 provider call start가 없다면, 프로세스는 그 둘 사이에 죽은 것입니다. 경계에 로그를 쓰면 마지막 로그가 위치를 알려줍니다.

쓰지 않는 것도 규율이다

로그 규율은 무엇을 쓸까만 다루지 않습니다. 무엇을 쓰지 않을까도 다룹니다. 첫 번째 종류는 안전입니다. 시크릿, API 키, 토큰, 비밀번호는 영원히 로그에 들어가면 안 됩니다. 시크릿을 한 번 실어 보낸 로그는 영원히 오염됩니다. 로그 저장 서비스로 복제되고 백업으로 복제되고 아카이브로 복제되며 오염이 퍼진 경로는 역추적할 수 없습니다. 실수는 한 번이면 충분합니다.

개인정보의 전체도 없습니다. 진단에 필요한 것은 어느 요청이었느냐이지, 어느 카드였느냐가 아닙니다. 식별자와 길이면 됩니다. 길은 16자리 카드임을, 식별자는 어느 사용자인지를 말하고 번호 자체는 기록하지 않습니다. 페이로드 전체도 마찬가지입니다. 요청 바디가 500KB면 전부 쓰면 스토리지가 부풀고 줄은 노이즈로 가득 차는데, 크기, 식별자, 그리고 중요한 필드면 충분합니다.

두 번째 종류는 신호입니다. 가장 큰 노이즈인 재시도와 폴링을 봅시다. 폴링 워커가 1분마다 ‘새 작업이 없다’고 쓰면 하루 1,440줄, 한 달이면 4만 3천 줄[추정]이 됩니다. 이 줄들은 사실이지만 검색할 가치가 없는 반복입니다. 정책은 하나, 첫 발생만 쓰고 이후는 집계하는 것입니다.

재시도는 번마다 줄을 쓰지 않습니다. 처음 실패 한 줄, 재시도 요약 한 줄, 최종 결과 한 줄이면 됩니다. 중간에 재시도가 몇 번 더 있었는지 일일이 쓰지 않아도 attempts 필드가 대신합니다. 반복을 줄마다 쓰면, 반복 횟수만큼 신호가 희석됩니다. 횟수는 필드로, 사건은 줄로.

줄의 개수에도 예산이 필요합니다. 하나의 요청이 정상적으로 끝나면 INFO는 3줄 안팎, 시작, 중간 경계, 성공입니다. 한 요청이 INFO 20줄을 만든다면, 그 20줄 가운데 새벽에 필요한 줄은 몇 줄인가, 대부분 3줄보다 적습니다. 나머지는 DEBUG로 내려 보냅니다. 예산을 정하면 줄을 줄이는 일이 허용되고 줄이는 것은 신호를 끌어올리는 일입니다.

신뢰는 훈련에서 자란다

찾은 것을 믿어도 되는가는 가장 무거운 질문입니다. 찾을 수 있지만 거짓인 로그는 없는 로그보다 위험합니다. 잘못된 확신을 주기 때문에, 확신이 잘못되면 행위가 잘못되고 행위가 잘못되면 인시던트는 길어집니다. 그런데 신뢰는 훈련으로 자랍니다.

훈련은 하나입니다. 매주 15분, 지난 달의 인시던트 하나, 또는 최근의 알림 하나를 가져와서 로그만으로 답해 보는 것입니다. 뭐가, 언제, 어떤 요청, 왜. 답이 나오면 로그는 사실입니다. 답이 나오지 않으면, 원했는데 없는 줄을 적습니다. 그 줄이 다음 주일의 일이 됩니다. 이 훈련은 회의보다 싸고 다른 누구도 찾을 수 없는 빈틈을 찾습니다. 이 로그를 쓰는 사람은 나뿐이니까요.

로그를 믿지 못할 때는 매번 로그 탓으로 돌리지 않는 것도 규율입니다. 불완전한 이유는 세 가지가 서로 다르기 때문입니다. 첫 번째, 로그가 없어서 몰랐던 사고. 이것은 다음 주에 줄을 추가하는 일입니다. 두 번째, 로그는 있는데 사고가 시스템 밖이었다, 벤더의 장애, 네트워크, DNS. 이 경우 로그는 자신의 역할을 했고 밖의 증인은 벤더 대시보드와 상태 페이지입니다. 세 번째, 로그가 있어서 속은 사고 시계 오차, 샘플링, 유실. 이것은 앞의 다섯 해독제의 일입니다.

인시던트가 끝나면 메모를 세 줄 씁니다. 뭐가, 어떤 증거, 뭐를 바꿨다. 증거는 느낌으로 쓰지 않고 쿼리로 씁니다. ‘지난 15분 ERROR 검색, provider 502 확인’이 메모이고 ‘결제 벤더 같았어’는 메모가 아닙니다. 형식은 고정, 인시던트 이름, 시간 창, 원인 한 줄, 쿼리 한 줄, 조치 한 줄. 형식이 같으면 메모들은 나중에 서로 검색됩니다.

끝은 점검입니다. 지금 시스템의 로그를 열어서 ERROR 줄이 새벽 2시에 일어나게 만드는 줄인지, 한 줄만 읽어도 행동할 수 있는지, 모든 줄에 ts와 service, level, msg, request_id가 있는지, 각 단계에 진입과 출구가 있는지 봅니다. 전부 답할 필요는 없습니다. 하나를 답할 때마다 새벽은 조금 부드러워집니다. 이 글의 논증을 장으로 늘리고 각 장 끝에 점검을 둔 전자책은 PDF로 함께 제공됩니다.

태그: incident-response, logging, 관찰성, production-operations, solo-developer, sre-practices, structured-logging

카테고리:

업데이트: