문제가 발생한 기기에서 로그 파일을 받아도 원인을 찾기 어려운 경우가 많았다. request start, success, refresh 같은 문장은 수백 줄 있었지만 어느 계좌와 화면의 요청인지 연결되지 않았다. 로그가 없는 게 아니라 문맥이 없었다.
반대로 응답 전체를 그대로 찍으면 파일이 너무 커지고 계정 정보 같은 값이 섞일 위험도 있었다. 무엇을 남기고 무엇을 숨길지 정하는 일이 필요했다.
한 번의 흐름을 묶을 식별자가 필요했다
동시에 여러 요청이 나가면 시간순 로그만으로 시작과 종료를 짝짓기 어렵다. 요청마다 짧은 식별자를 만들고 관련 로그에 함께 넣었다.
string requestId = Guid.NewGuid().ToString("N")[..8];
Log.Info($"orders.request start id={requestId} account={Mask(accountId)}");
var result = await LoadOrders();
Log.Info($"orders.request end id={requestId} count={result.Count}");
실제 식별값은 사용자를 알아볼 수 없도록 가리거나 내부에서 생성한 요청 ID를 사용했다.
결과보다 결정 이유를 남겼다
refresh skipped만으로는 왜 건너뛰었는지 알 수 없다. 현재 화면이 닫혔는지, 요청이 이미 진행 중인지, 선택 계좌가 바뀌었는지를 구분했다.
orders.apply skipped id=a1b2c3d4 reason=screen-inactive
orders.apply skipped id=e5f6g7h8 reason=account-changed
이런 로그는 코드의 분기와 맞아야 했다. 메시지를 예쁘게 쓰는 것보다 운영 중 의사결정을 재구성할 수 있는지가 중요했다.
민감한 값과 큰 본문은 남기지 않았다
로그인 정보, 토큰, 원본 응답 전문은 디버깅에 편해 보여도 로그에 오래 남는다. 성공 여부, 응답 개수, 상태 코드, 걸린 시간처럼 원인을 좁히는 메타데이터를 우선했다. 특정 필드가 필요하면 허용 목록을 두고 값 자체보다 존재 여부나 길이를 남겼다.
오류 로그에도 예외 메시지만 쓰지 않고 현재 작업 단계와 안전한 문맥을 함께 기록했다. 스택 트레이스는 개발 빌드와 필요한 구간에서만 보존했다.
좋은 로그는 나중의 질문을 예상했다
로그를 넣을 때 당장 콘솔에 값이 보이는지만 생각했었다. 이번에는 며칠 뒤 다른 기기에서 파일 하나만 받았을 때 무엇을 물어볼지를 기준으로 정했다. 요청이 도착했는지, 어느 단계까지 갔는지, 왜 적용되지 않았는지를 답할 수 있어야 했다.
로그의 양을 늘리는 것보다 흐름을 연결할 작은 문맥을 넣는 편이 훨씬 효과적이었다. 이후 문제를 수정할 때는 코드와 함께 “다음에 같은 문제가 나면 어떤 증거가 남을까”도 생각하게 됐다.