개발 slecs

정산 메시지 19% 누락을 일으킨 발신자명 스킵 로직 수정

목차

무의식적 스킵의 함정

메시지 파서에서 발신자명이 "No Name"이면 통째로 스킵하던 로직이 있었다. 처음 짤 때 의사결정은 거의 직관이었음. "이름 없는 메시지는 어차피 쓰레기"라고 단정하고 조기 return 하나 박아넣었다. 그 당시엔 맞는 것처럼 보였다. 유효하지 않은 메시지를 일찍 끊어내면 파이프라인이 깔끔해지고, 불필요한 파싱 시도도 줄어드니까.

문제는 그 가정이 조용히 틀려가고 있었다는 거다.

이번에 파트너 정산 데이터를 다시 훑다가 이상한 패턴을 발견했다. 발신자명은 "No Name"으로 비어있는데, raw_message 본문에는 실제 이름, 금액, 계좌번호가 멀쩡히 들어있는 케이스가 있었다. 처음엔 예외 케이스인 줄 알았는데 한 달치를 까보니 생각보다 훨씬 많았다.

원인은 단순했다. 발신처 시스템마다 헤더 구성 방식이 다르다. 어떤 채널은 발신자 필드를 명시적으로 채워주고, 어떤 채널은 본문에 모든 정보를 박아넣은 뒤 헤더는 아예 비워둔다. 이건 레거시 연동에서 자주 보이는 패턴인데, 통합하는 쪽에서 "발신자 필드는 옵셔널" 규격으로 설계했을 가능성이 높다. 어느 쪽이든 본문이 진짜 데이터 소스인 경우, 헤더만 보고 스킵하면 데이터가 사라진다.

무엇을 바꿨나

수정 자체는 크지 않다. 조기 return 제거하고, raw_message 파싱을 발신자 유무와 무관하게 항상 수행하도록 했다. 파싱 실패 케이스만 별도 분기로 빠지게 정리했음.

# 변경 전
def parse_message(msg):
    if msg.sender_name == "No Name":
        return None          # 여기서 19%가 사라지고 있었음

    return extract_payload(msg.raw_message)

# 변경 후
def parse_message(msg):
    payload = extract_payload(msg.raw_message)
    if payload is None:
        return None          # 본문 파싱 자체가 실패한 경우만 버림

    return payload

핵심은 "버리는 기준"을 헤더 필드에서 파싱 결과로 옮긴 것이다. 발신자명은 파싱 이후 보강 데이터로 활용하면 충분하고, 그게 없다고 본문 파싱을 건너뛸 이유가 없다.

분기 정리 방향을 표로 보면 이렇다.

케이스 기존 처리 변경 후 처리
발신자명 정상 + 본문 정상 정상 처리 정상 처리
"No Name" + 본문에 이름/금액 있음 스킵 (데이터 손실) raw_message 파싱 후 정상 처리
"No Name" + 본문도 비정형 스킵 파싱 실패로 분기, 버림

세 번째 케이스는 어차피 파싱이 실패하니 결과는 같다. 바뀐 건 두 번째 케이스만이다.

손실 규모와 백필 가능성

한 달치 샘플 기준으로 뽑아봤을 때 분포가 대략 이랬다.

  • 발신자명 정상: 약 78%
  • "No Name" + 본문에 실제 데이터 있음: 약 19%
  • "No Name" + 본문도 비정형: 약 3%

19%가 그냥 사라지고 있었다. 정산 측면에서 작지 않은 숫자다. 다행인 건 raw_message가 원본을 보존하고 있었다는 거다. 파서가 뱉어내는 processed 결과만 없을 뿐, 원본은 남아있으니 백필이 가능하다. 현재 이 구간 데이터를 재처리하는 작업을 별도로 잡아놓은 상태다.

이런 버그가 특히 나쁜 이유는 증상이 바로 안 보인다는 거다. 파서는 에러 없이 정상 종료되고, 로그에도 아무것도 안 찍힌다. 그냥 조용히 데이터가 줄어들 뿐이다. 정산 불일치가 누적되거나 누군가 직접 원본을 뒤지기 전까지는 발견이 안 된다.

회고

이번에 다시 학습한 것들을 정리해두면,

  • "이건 어차피 쓰레기"라는 가정은 거의 틀린다. 적어도 한 번은 raw 데이터를 직접 까봐야 한다. 이건 원칙인데 바쁘다는 이유로 건너뛰게 되는 원칙이기도 하다.
  • 헤더와 본문은 다른 시스템에서 채워지는 경우가 많다. 한쪽만 보고 유효성을 판단하면 반드시 어딘가에서 손해를 본다.
  • 파서 설계 원칙으로 정리하면: 버리는 기준은 보수적으로, 파싱 시도 기준은 공격적으로. 파싱 실패를 무서워해서 시도를 줄이면 안 된다. 실패하면 실패 분기로 빠지면 그만이고, 시도조차 안 하면 손실이 된다.
  • 조기 return이 성능 최적화처럼 느껴지지만, 데이터 파이프라인에서 "틀린 조기 return"은 가장 비싼 버그다. 처리 비용을 아끼려다 데이터를 버리는 셈이다.

같은 류의 패턴이 다른 유틸에도 있을 가능성이 높다. 다음 사이클에 비슷한 조건 분기를 grep으로 한번 훑어볼 생각이다. "No Name" 외에도 비슷한 방어 코드가 어딘가 조용히 데이터를 먹고 있을 수 있다.

조기 최적화로 데이터를 버리는 게 가장 비싼 버그라는 걸 또 확인했다.

댓글 0

첫 댓글 달아줘.