개발 이야기3분 읽기

로그 한 줄에서 시작하는 조사

필드 여섯 개를 순서대로 열어 만료된 토큰을 찾아냅니다. 실제로 돌려 본 조사 흐름입니다.

경보가 왔고 로그에 이 줄이 있습니다.

2026-08-31T09:15:30.250Z WARN [api] req=01M1BHMD2A
user=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1LTQyIiwiZXhwIjoxNzg4MTAxODM0fQ.x
ip=203.0.113.42 dur=1834ms
payload=H4sIAAAAAAAAE6tWyi9KSS1SslJy1DVU0lEqLKlUsjKqBQBaiwz9FwAAAA==
hash=5d41402abc4b2a76b9719d911017c592

값 넷이 인코딩되어 있습니다. 순서대로 엽니다.

1단계 — 각 값이 무엇인지 판별

접두사와 길이로 갈립니다.

req      01M1BHMD2A…               26자, 하이픈 없음      → ULID
user     eyJhbGci…                 eyJ 로 시작            → JWT
payload  H4sIAAAA…                 H4sI 로 시작           → gzip 을 base64 로
hash     5d41402abc4b2a76…         32자 hex              → MD5

eyJ{"의 base64이고, H4sI는 gzip 매직 바이트 1f 8b 08의 base64입니다.

2단계 — ULID에서 시각을 꺼낸다

ULID 앞 10자가 유닉스 밀리초입니다. Crockford base32로 디코딩합니다.

01M1BHMD2A → 2026-08-31T09:15:30.250Z

로그 타임스탬프와 정확히 같습니다. 요청 ID가 서버에서 생성됐고 로그가 그 직후에 찍혔다는 뜻입니다.

만약 두 값이 크게 벌어져 있다면 그 사이에 지연이 있었다는 단서가 됩니다 — 큐에 머물렀거나, 재시도였거나.

3단계 — JWT를 연다

세 조각을 .으로 나눠 앞 두 조각을 base64url 디코딩합니다.

헤더     {"alg":"HS256"}
페이로드  {"sub":"u-42","exp":1788101834}

exp를 사람이 읽는 시각으로 바꿉니다.

exp        2026-08-30T14:57:14.000Z
요청 시각   2026-08-31T09:15:30.250Z

18시간 전에 만료된 토큰입니다.

여기서 조사 방향이 정해집니다. 클라이언트가 만료된 토큰을 계속 보내고 있거나, 갱신 로직이 동작하지 않았거나 — 어느 쪽이든 dur=1834ms라는 긴 응답 시간과 함께 볼 값이 생겼습니다.

서명은 확인하지 않았습니다. 디코딩은 키 없이 되지만 검증은 키가 필요합니다. 이 단계에서는 "무엇이 담겼나"만 봅니다.

4단계 — payload 압축을 푼다

base64로 디코딩한 뒤 gzip을 풉니다.

{"order":"A-1","qty":2}

23바이트짜리 JSON이 gzip과 base64를 거쳐 60자가 됐습니다. 원본보다 커졌습니다 — 짧은 값을 압축하면 헤더가 본문보다 큽니다.

이것 자체가 발견입니다. 이 경로에서 압축이 이득을 내지 못하고 있다는 뜻입니다.

5단계 — 해시를 대조한다

32자 hex는 MD5입니다. 값이 무엇의 해시인지는 후보를 넣어 봐야 압니다.

MD5("hello") = 5d41402abc4b2a76b9719d911017c592

일치합니다. 요청 본문의 체크섬으로 쓰이는 값이라면, 실제 본문과 대조해 전송 중 변형이 있었는지 볼 수 있습니다.

조사 순서가 도구 순서다

1  필드를 나눈다            줄 정렬·중복 제거
2  접두사로 무엇인지 판별      (표를 본다)
3  식별자에서 시각을 꺼낸다     Epoch 변환
4  토큰을 연다               JWT 디코더
5  압축·인코딩을 푼다         Base64 → Gzip
6  해시를 대조한다            MD5 / SHA-256

이 순서가 중요한 이유는 각 단계가 다음 단계의 입력을 만들기 때문입니다. ULID의 시각을 먼저 꺼내야 JWT의 exp와 비교할 대상이 생기고, base64를 먼저 풀어야 gzip인지 알 수 있습니다.

여러 줄을 다룰 때

로그 한 줄이 아니라 수천 줄이면 앞 단계가 달라집니다.

타임스탬프가 유닉스 초로 찍혀 있으면 일괄 변환이 필요합니다. IP나 URL만 뽑아 빈도를 보려면 추출이 먼저입니다. 중복 요청을 찾으려면 정렬과 중복 제거를 먼저 겁니다.

값 하나를 깊이 파는 것과 전체에서 패턴을 찾는 것은 도구 순서가 반대입니다.

확인 순서

  1. 인코딩된 값을 만나면 앞 4자로 종류를 먼저 정합니다
  2. 식별자에 시각이 들어 있으면 로그 타임스탬프와 대조합니다. 지연 구간이 보입니다
  3. JWT는 디코딩과 검증이 다릅니다. 조사 단계에서는 내용만 봅니다
  4. 압축된 값이 원본보다 큰지 봅니다. 짧은 페이로드에서 흔합니다
  5. 여러 줄을 다룰 때는 추출·정렬·중복 제거를 먼저 겁니다

tools.onuel.dev에서 이 순서를 그대로 밟을 수 있습니다. JWT 디코더가 세 조각을 나눠 주고, Base64 디코딩Gzip이 payload를 풉니다. 타임스탬프는 Epoch 변환, 여러 줄이면 Epoch 일괄 변환이고, 로그에서 주소만 뽑을 때는 이메일·URL 추출기, 중복 요청을 찾을 때는 줄 정렬·중복 제거를 씁니다.

  • #디버깅
  • #로그
  • #조사