개발 slecs

은행명 표기 불일치로 매일 누락되던 정산 30건 해결

목차

정산 로직에서 은행명 비교를 완전 일치(equals)로 처리하는 건 설계 초기엔 합리적인 판단이다. 데이터가 정형화된 시스템에서 들어온다고 전제하면 굳이 퍼지 매칭을 넣을 이유가 없다. 문제는 그 전제가 외부 시스템에 의존할 때 언제든 깨질 수 있다는 것. 결제대행사가 내부 코드를 변경하거나, PG사가 교체되거나, 은행이 브랜드명을 조정하는 순간 정형화된 표기가 흔들린다.

이번 이슈가 딱 그랬다. 지역 농축협·새마을 계열에서 동일한 금융기관이 세 가지 다른 표기로 들어오고 있었고, 핸들러는 그걸 서로 다른 은행으로 취급하거나 아예 미매칭으로 분류하고 있었다. 정산팀이 매일 아침 30건짜리 미매칭 큐를 수동으로 처리하면서도, 그게 "원래 있는 운영 업무"로 자리잡아 버린 상태였다는 게 더 문제였음.

완전 일치 매칭이 깨지는 전형적인 패턴

금융 데이터에서 은행명 불일치가 생기는 경로는 대개 비슷하다.

  • 브랜드 코드가 앞에 붙는 경우: MG새마을금고, NH농협, KB국민
  • 축약형과 풀네임 혼용: 새마을 vs 새마을금고
  • 지점명이 은행명에 붙어 들어오는 경우: OO새마을지점
  • 공백·특수문자 포함 여부: 농 협 vs 농협
  • PG사 업데이트로 표기 체계 자체가 바뀌는 경우

이 중 어느 하나만 발생해도 완전 일치 비교는 실패한다. 결제대행사 입장에서는 자기 시스템 내부 표기를 바꿀 이유가 없고, 우리 입장에서는 내부 표준 코드를 바꿀 이유가 없다. 그 사이 간극이 미매칭으로 조용히 쌓인다.

기존 코드는 이랬다.

// 기존: 한 글자만 달라도 미매칭
if (incoming.equals(STANDARD_NAME)) {
    // 매칭 성공
} else {
    // 미매칭 큐로 이동
}

한 줄이다. 이 한 줄이 매일 30건과 사람 30분을 잡아먹고 있었다.

세 단계 매칭으로 교체

방향은 단순하게 잡았다. 틀린 표기가 들어와도 최대한 맞춰보되, 진짜 판단이 어려운 것만 사람한테 넘긴다. 단계를 나눈 이유는 단계마다 신뢰도가 다르기 때문이다. 정규화 매칭은 거의 확실하고, fallback으로 갈수록 불확실해진다. 무분별하게 퍼지 매칭 하나로 퉁치면 나중에 오탐이 생겼을 때 어디서 잘못됐는지 추적이 안 된다.

public Optional<String> matchBankName(String incoming) {
    // 1단계: 정규화 후 비교 (공백·특수문자·브랜드코드 제거)
    String normalized = normalize(incoming);
    if (standardNames.contains(normalized)) {
        return Optional.of(normalized);
    }

    // 2단계: 별칭 테이블 조회
    String alias = aliasTable.get(normalized);
    if (alias != null) {
        return Optional.of(alias);
    }

    // 3단계: 핵심 키워드 부분 일치 (fallback)
    return standardNames.stream()
        .filter(s -> incoming.contains(s) || s.contains(incoming))
        .findFirst();
}

단계별로 어떤 케이스가 어디서 잡히는지 정리하면 이렇다.

단계 들어온 값 매칭 방식 결과
1 MG 새마을금고 정규화 매칭 새마을금고
2 새마을 별칭 테이블 새마을금고
3 OO새마을지점 키워드 fallback 새마을금고
- ??은행 모두 실패 미매칭 큐

fallback 단계에서 주의할 점이 하나 있다. 키워드 부분 일치는 광역 매칭이라 오탐 가능성이 있다. "농협" 키워드로 매칭하다가 "농협카드"와 "농협은행"을 잘못 연결할 수도 있음. 그래서 fallback 매칭은 결과를 별도 로그에 남기고, 초기 배포 후 일주일은 수동으로 검증했다. 자동화를 믿되, 그 자동화가 무슨 결정을 내리는지는 추적해야 한다.

별칭 테이블은 처음엔 코드 안에 넣으려고 했다가, 설정 파일로 분리했다. 이유는 단순하다. 코드에 박아두면 새 표기가 들어올 때마다 배포가 필요하고, 그 타이밍까지 누락이 계속 발생한다. 설정으로 빼두면 운영자가 즉시 추가할 수 있다.

# bank-alias.yml
aliases:
  새마을: 새마을금고
  MG새마을금고: 새마을금고
  NH농협: 농협은행
  농협: 농협은행

이 파일 하나 수정하면 배포 없이 다음 배치부터 반영된다. 같은 클래스를 다시 건드리지 않는 게 목표였고, 지금 구조에서는 그게 된다.

배포 결과와 남은 것

배포 다음 날 미매칭 큐를 확인했을 때 30건에서 2~3건으로 떨어져 있었다. 남은 케이스를 들여다보니 진짜 오타거나 신설 점포라 수동 처리가 맞는 건들이었다. 자동화가 잡아야 할 건 다 잡고, 사람 판단이 필요한 건 남긴 셈이다.

정산팀에서 업무 시간이 체감으로 30분 이상 줄었다는 피드백이 왔다. 숫자로는 작아 보이지만, 매일 아침 첫 업무가 수동 매핑이었다는 걸 생각하면 흐름이 달라진 거다.

처음엔 별칭 테이블만 추가하면 끝날 줄 알았다. 실제로 그것만 해도 당장의 30건 중 대부분은 해결됐을 거다. 근데 그렇게 하면 결제대행사가 표기를 다시 바꾸는 순간 또 같은 이슈가 터진다. 정규화와 fallback까지 같이 넣은 건 그 때문이었다. 한 번 해결하면 비슷한 변형에는 흔들리지 않아야 한다.

더 찜찜했던 건 타이밍이었다. 정산팀이 얼마나 오래 수동으로 처리해왔는지 물어봤더니 꽤 됐다고 했다. "아침마다 하는 일"로 완전히 정착해버린 거다. 핸들러 한 줄이 운영 비용을 조용히 만들고 있는 동안, 그게 코드 문제인지 아무도 연결짓지 않았던 것. 이번에 얻은 건 수치였다. 30건, 매일, 30분. 작아 보여도 쌓이면 꽤 되고, 그걸 숫자로 본 게 이번 수확이다.

외부 데이터 소스에서 값을 받아 내부 표준과 비교하는 로직은 처음부터 정규화 레이어를 끼워 넣는 게 맞다. 완전 일치로 시작하면 나중에 건드리기 어렵다. 특히 금융 데이터처럼 외부 시스템 의존도가 높은 영역에서는.

댓글 0

첫 댓글 달아줘.