Building an EAI Middleware Layer
One Number Joins Four Teams' Logs
한국어 원문으로 표시합니다.
한 줄 요약
장애 회의의 첫 질문은 늘 같다 — "그 거래, 어디까지 갔나요?" 이 질문에 몇 분 안에 답하려면 모든 구간이 같은 번호(GUID)를 로그에 남기고, 그 로그를 한 형식·한 시간대로 이어 붙일 수 있어야 한다. 번호가 구간마다 다르거나 시각이 제각각이면, 답은 몇 시간짜리 추측이 된다.
왜 이게 필요했나
이체 하나가 채널(MCI) → 허브(EAI) → 계정계(CORE) → 필요하면 대외계(FEP)를 지난다. 고객이 "돈이 빠졌는데 이체 실패라고 떴다" 고 전화하면, 네 팀이 각자의 로그를 뒤진다. MCI 는 key=value, 허브는 파이프 구분, 계정계는 JSON, 대외계는 고정 칼럼이다. 계정계는 UTC 로 적고 나머지는 한국 시각이다. 그리고 최악의 경우 — 허브가 계정계를 부를 때 번호를 새로 땄다. 채널 로그의 번호로 계정계 로그를 검색하면 아무것도 안 나온다. "우리 쪽에는 그 거래가 안 보인다" 가 네 번 반복되고, 결국 시각과 금액으로 비슷한 줄을 찾아 손으로 잇는다.
1모듈에서 GUID 를 "처음 만든 시스템이 한 번 발급하고 모든 구간이 그대로 싣는다" 고 정한 이유가 여기 있다. 이 모듈은 그 약속이 지켜진 로그로 무엇을 할 수 있는지, 그리고 약속을 어기는 중계를 어떻게 고치는지를 다룬다.
어떻게 동작하나
정규화. 형식이 다른 로그를 같은 열(guid, hop, 시각, 이벤트, 응답코드)로 바꾼다. 원본 로그의 형식을 바꾸라고 네 팀에 요구하는 것보다, 읽는 쪽에서 한 번 맞추는 것이 빠르다. 다만 새로 만드는 시스템은 처음부터 구조화 로그(JSON 한 줄, 정해진 열쇠)로 쓴다 — 정규식 없이 읽힌다.
시간대. 로그 시각은 반드시 시간대를 달고 있어야 한다. 2026-09-23T00:02:08.268Z 의 Z 는 UTC 라는 뜻이고, 한국 시각(UTC+9)으로는 09:02:08.268 이다. 시간대 없는 시각(2026-09-23 09:02:08)은 "어느 시간대인지 아는 사람만 읽을 수 있는" 시각이다. 한 시스템만 UTC 인 줄 모르고 이으면, 계정계가 허브보다 9시간 먼저 처리한 것처럼 보인다.
시간순 잇기. 한 GUID 의 행을 시간순으로 세우면 거래의 경로가 나온다. 첫 이벤트부터의 경과 시간을 붙이면 어느 구간에서 시간을 썼는지 보인다. 다만 서로 다른 서버의 시계는 조금씩 어긋나므로(각자 NTP 로 맞춰도 수 밀리초), 밀리초 단위의 구간 간 순서는 참고로만 본다. 같은 서버 안의 두 이벤트 차이(계정계 RECV → APPLY)는 믿을 만하다.
사라진 거래. 허브가 "보냈다(OUT)" 고 적었는데 계정계에 "받았다(RECV)" 가 없으면, 그 거래는 둘 사이에서 사라졌다. 네트워크 단절일 수도, 계정계 앞단에서 버려졌을 수도 있다. 이런 거래는 허브가 E901 로 답했을 것이고(4모듈), 8모듈의 조회로 확정해야 한다. GUID 로 이어야만 이 목록을 기계적으로 뽑을 수 있다.
느린 거래 분해. 전체 시간이 3초를 넘긴 거래를 구간별로 나눈다 — 계정계 안에서 걸린 시간(RECV→APPLY), 대외 기관에서 걸린 시간(REQ→RSP). 평균이 아니라 개별 거래를 분해해야 "계정계가 느린 날" 과 "특정 기관이 느린 날" 이 갈린다.
전파 규칙. 중계는 받은 GUID 를 그대로 다음 구간에 싣는다. 전문 구간에서는 헤더의 GUID 자리, HTTP 구간에서는 헤더(이 코스는 X-GUID)와 본문이다. 그리고 표준도 있다. W3C Trace Context 는 HTTP 로 추적 문맥을 넘기는 traceparent 헤더를 정한다. 모양은 버전-trace-id-parent-id-flags 이고, 버전 00 에서 trace-id 는 16바이트(소문자 16진수 32자), parent-id 는 8바이트(16자)이며 둘 다 전부 0 이면 무효다. trace-id 는 거래 전체에 하나, parent-id 는 호출마다 새로 만든다 — 그래서 한 거래 안의 여러 호출을 부모·자식으로 그릴 수 있다. LH-STD GUID 를 trace-id 와 같은 모양으로 정해 둔 덕분에(1모듈), GUID 를 그대로 trace-id 에 실으면 전문 구간과 HTTP 구간의 추적이 끊기지 않고 이어진다.
로그 필수 열. 구간 이벤트 한 줄에 최소한 시각(시간대 포함), GUID, 구간 이름, 이벤트, 응답코드가 있어야 한다. 금액·계좌번호 같은 개인정보는 넣지 않거나 가린다 — 추적에는 번호 하나면 충분하다.
현장에서 만나는 모습
가장 흔한 것은 중계가 GUID 를 새로 따는 사고다. 누군가 "우리 시스템의 거래번호 규칙" 을 지키려고 받은 번호를 버리고 자기 번호를 붙였다. 좋은 의도였지만 추적은 그 구간에서 끊긴다. 자기 번호가 꼭 필요하면 추가로 적고, 받은 GUID 는 그대로 넘긴다. 두 번째는 시간대 없는 로그다. 서버 한 대의 시간대 설정이 바뀐 날부터 로그가 9시간씩 밀리고, 누구도 눈치채지 못한다. 세 번째는 로그에 GUID 를 안 남기는 오류 경로다. 정상 흐름에는 GUID 를 찍는데, 예외 처리 블록의 로그 한 줄에는 빠져 있다 — 정작 필요한 건 그 줄이다.
다음 실습에서 할 것
한 영업일의 네 구간 로그(MCI·EAI·CORE·FEP, 형식 넷, 시간대 둘)를 한 형식으로 정규화하고 한국 시각으로 맞춘다. GUID 하나의 경로를 그리는 trace.py, 계정계에 닿지 않은 거래 목록, 느린 거래의 구간 분해를 만든다. 마지막으로 GUID 를 새로 따는 중계(relay_buggy.py)를 고쳐 받은 GUID 를 그대로 싣고 구조화 로그를 남기게 하고, HTTP 구간에 traceparent 를 싣는다.