원화 입금 대조에서 매칭 안 된 건이 수천 건 나왔다. 은행 입금 내역 파일과 우리 입금 기록을 대조하는 작업이었다.
Table of contents
Open Table of contents
증상 — 미매칭 수천 건
첫 가설은 단순했다.
입금 코드 컬럼이 비어 있어서 매칭이 안 된다
파일을 열면 빈 칸이 바로 눈에 보이니 NULL 쪽으로 먼저 갔다.
그런데 NULL 인 행을 COUNT 해 보니 그렇게 많지 않았다. 그것을 전부 채워도 대부분이 그대로 남는다는 뜻이라 다른 원인을 찾아야 했다.
원인 — 다섯 겹으로 쌓인 규칙
매칭 코드를 읽으니 JOIN 하나로 끝나는 구조가 아니었다.
일부 은행은 입금 코드로 매칭하는 것을 아예 건너뛰고 있었다.
if (in_array($bank, self::SKIP_DEPOSIT_CODE_MATCH)) {
// 이 은행은 입금자명에 코드가 안 실린다 — 금액·시각으로 맞춘다
}
SKIP_DEPOSIT_CODE_MATCH 에 든 은행은 입금 코드가 아닌 다른 키로 맞춰야 한다.
일부 은행 파일은 문자셋이 달랐다.
입금자명 EUC-KR, 20바이트에서 절단
EUC-KR 파일을 LOAD DATA INFILE 로 그냥 올리면 입금자명이 깨진다. MySQL 5.7 참고 매뉴얼의 그 절은 서버가 파일을 character_set_database 로 해석하고 SET NAMES 는 입력 해석에 영향이 없다고 적으므로, 파일 문자셋이 다르면 CHARACTER SET euckr 절을 줘야 우리 값과 맞는다.
분할 입금은 원 신청이 아니라 부분 입금 항목에 붙어야 했고 후보가 여럿이면 실패로 보는 분기도 있었다.
if (count($candidates) !== 1) {
// 매칭 실패
}
$candidates 가 정확히 하나로 특정될 때에만 매칭으로 통과시키고 있었다.
마지막으로 리뉴얼 때 이관된 회원은 두 표의 식별자 체계가 달랐다.
입금 신청 테이블 신 구조 member_no
가상계좌 테이블 옛 구조 회원 번호
입금 신청 테이블 과 가상계좌 테이블 을 직접 JOIN 할 수 없어 member_map 한 단계를 거쳐야 한다.
처음 본 것은 파일 컬럼과 DB 컬럼의 대응 하나였는데 실제로는 은행 예외와 문자셋과 분할 입금 분기와 유일성과 이관 불일치가 그 위에 얹혀 있었다. 컬럼 매핑만 보고 있었으니 나머지 네 겹은 보일 수가 없었다.
조치 — 순서대로 좁혔다
다섯 규칙을 한꺼번에 걸지 않고 순서대로 적용했다.
1. 은행별로 나눈다
2. 문자셋을 맞춰 다시 올린다
3. 분할 입금은 부분 입금 항목으로 대상을 바꾼다
4. 이관 회원은 member_map 을 거쳐 조인한다
5. 양방향 1:1 확인
각 단계에서 매칭되는 건수가 얼마나 느는지를 COUNT(*) 로 그때마다 셌다.
한꺼번에 거는 것 대신 하나씩 벗기는 쪽을 골랐다. 이유는 다섯을 한꺼번에 걸어 버리면 어느 규칙이 몇 건을 설명했는지 알 수 없어서다. 하나씩 벗겨 내니 규칙마다의 기여가 숫자로 남았다.
검증 — 양방향 1:1 확인
매칭 스크립트에 규칙을 하나 두었다.
-- 방향 A: 은행 파일 → 우리 것
SELECT COUNT(*) FROM krw_deposits WHERE match_key = <은행 행 키>;
-- 정확히 1이어야 함
-- 방향 B: 우리 것 → 은행 파일
SELECT COUNT(*) FROM bank_rows WHERE match_key = <우리 키>;
-- 정확히 1이어야 함
krw_deposits 쪽과 bank_rows 쪽 COUNT(*) 가 둘 다 1일 때에만 매칭으로 인정한다.
krw_deposits 쪽만 재고 넘어가면 통과와 오염을 가르지 못하고 두 방향이 같은 답을 낼 때에만 짝이 하나로 정해진다.
주의 — 한 방향만 보면 다대일이 통과한다
왜 한쪽으로 부족한지는 이런 모양에서 그대로 드러난다.
은행 행 A → 우리 신청 X (1건, 통과)
은행 행 B → 우리 신청 X (1건, 통과)
A 도 B 도 각각은 1건이라 은행 쪽에서 보면 둘 다 통과한다.
반대 방향으로 보면 그 자리에서 잡힌다.
우리 신청 X → 은행 행 A, B (2건, 실패)
X 하나에 A 와 B 가 둘 다 붙어 있으니 실패로 걸린다.
한 방향만 봤으면 이것이 통과해 데이터가 오염된다. 입금 대조에서 그것은 한 입금이 두 신청에 잡혀 잔액이 두 번 올라간다는 뜻이었다.
결과 — 남은 몇 건
돌린 결과는 이랬다.
전체 수천 건
해결 거의 전부
남음 몇 건
남은 몇 건은 각자 개별 사정이 있는 것들이라 손으로 하나씩 봤다.
단계별로 세어 두었기 때문에 그 몇 건이 어느 단계에서 막혔는지도 함께 알 수 있었다. 처음에 세운 NULL 가설은 눈에 보이는 것이었고 실제 원인은 코드를 읽어야 나오는 쪽에 있었다.
정리
- 첫 가설이 단순하면 표면일 가능성이 높다
- 매칭 규칙이 여러 겹일 수 있다
- 컬럼 매핑만 보면 나머지 겹이 안 보인다
- 은행 예외와 문자셋과 분할 입금 분기와 유일성과 이관 불일치를 다 본다
- 한 방향만 보면 다대일이 그대로 통과한다
- 양쪽에서
COUNT(*)가 1 인지 확인한다 - 매칭이 오염되면 한 입금이 두 번 반영된다
- 규칙을 하나씩 벗겨 내며 기여를 센다
- 단계별로 세면 안 풀린 것이 어디서 막혔는지 안다