은행 현장의 언어 · 수신 파일의 형식 계약 · 이론
적재하고 나서 틀린 줄을 찾으면 이미 늦다
한 줄 요약
상대 기관에서 받은 정산·이체 지시 파일은 적재하기 전에 형식 계약으로 검사하고, 한 줄이라도 어긋나면 파일 전체를 돌려보낸다. 문 앞에서 거절하는 일은 적재한 뒤에 대사하는 일보다 언제나 싸다.
왜 이게 필요했나
은행 전산의 하루는 파일로 시작해서 파일로 끝난다. 상대 기관이 새벽에 FTP 로 올려놓은 하루치 지시 파일을 받아 적재하고, 저녁에 우리 쪽 결과를 같은 방식으로 돌려준다. 이 길목에서 사고가 나는 방식은 늘 비슷하다. 파일은 도착했고, 적재 배치는 "성공" 으로 끝났고, 그런데 상대가 보낸 명세(trailer)의 건수와 우리가 넣은 건수가 다르다.
그다음이 진짜 문제다. 이미 들어간 줄은 다른 배치가 물고 갔다. 잔액이 바뀌었고 통지가 나갔다. 되돌리려면 정정 전표를 쓰고, 어느 줄이 들어갔고 어느 줄이 빠졌는지 다시 세야 한다. 파일 하나를 받아들이지 않았으면 5분이면 끝났을 일이, 하루치 대사와 사과 전화가 된다.
그래서 수신 경로에는 반드시 문지기가 선다. 문지기가 하는 일은 단순하다. 이 파일이 우리가 합의한 형식 계약을 지켰는가만 본다. 지키지 않았으면 한 줄도 적재하지 않고 사유를 붙여 돌려보낸다. 절반만 넣는 선택지는 없다.
어떻게 동작하나
형식 계약은 대개 네 겹이다.
첫째, 파일 겉모양. 이름 규칙, 인코딩, 줄 끝. 국내 금융권 대외 파일에는 아직 고정폭(fixed width) EUC-KR 계열이 많이 남아 있다. 파이썬에서는 cp949 코덱을 쓰는데, [표준 인코딩 표](https://docs.python.org/3/library/codecs.html)는 이 코덱의 별칭을 949, ms949, uhc 로 적고 언어를 Korean 으로 분류한다. 한글 한 글자가 CP949 에서 2바이트, UTF-8 에서 3바이트라는 차이가 다음 겹으로 바로 이어진다.
둘째, 레코드 구조. 고정폭이면 한 줄이 정확히 몇 바이트인가를 본다. 여기서 가장 자주 나는 실수가 글자 수로 채우는 것이다. 수취인명 자리가 20이라면 그것은 스무 글자가 아니라 스무 바이트다. "김서준" 을 세 글자로 보고 열일곱 칸을 공백으로 채우면 그 줄은 규격 폭에서 어긋나고, 그 뒤 필드가 통째로 밀린다. [유니코드 다루기 안내](https://docs.python.org/3/howto/unicode.html)가 설명하듯 파이썬 문자열의 길이는 코드 포인트 수이고, 파일에 적히는 것은 인코딩된 바이트다. 자르는 자리는 str 이 아니라 bytes 위에 있어야 한다.
CSV 라면 [RFC 4180](https://www.rfc-editor.org/rfc/rfc4180.html)이 기준이다. 이 문서는 Category 가 Informational 이고 스스로 "어떤 인터넷 표준도 규정하지 않는다" 고 밝히지만, 사실상의 합의로 널리 쓰인다. 핵심은 세 줄이다. 줄바꿈·큰따옴표·쉼표가 든 필드는 큰따옴표로 감싼다. 감싼 필드 안의 큰따옴표는 큰따옴표 둘로 적는다. 레코드는 CRLF 로 끝난다. 그래서 line.split(",") 로 자르는 순간, 수취인명에 쉼표가 든 줄은 필드가 하나 늘어나고 계좌 자리에 이름 조각이 들어온다. [csv 모듈](https://docs.python.org/3/library/csv.html)에 맡기면 인용 안의 쉼표와 줄바꿈을 구분자로 세지 않는다.
셋째, 헤더와 명세(trailer). 파일 끝의 T 레코드에 적힌 건수와 합계를 실제로 센 값과 맞춘다. 이것이 전송 중 잘림을 잡는 가장 싼 장치다. 건수가 맞는데 합계가 다르면 잘린 것이 아니라 값이 바뀐 것이고, 둘이 다른 사고라서 오류 코드도 따로 둔다.
넷째, 필드 단위 규격과 중복. 거래번호 모양, 계좌 형식, 금액 범위를 본다. 그리고 같은 파일이 두 번 오는 일은 예외가 아니라 일상이다. 상대가 우리 응답을 못 받으면 같은 파일을 다시 올린다. 파일의 sha256 이 같으면 단순 재전송이고, 일련번호는 같은데 내용이 다르면 그것은 사고다. [hashlib](https://docs.python.org/3/library/hashlib.html)로 파일 해시를 남겨 두면 이 둘을 갈라낼 수 있다.
수신함 ──▶ 이름·인코딩 ──▶ 레코드 폭·인용 ──▶ 트레일러 ──▶ 필드 ──▶ 중복 │ │ │ │ │ └──────────── 하나라도 걸리면 파일 전체를 거절 ──────┘현장에서 만나는 모습
첫 번째로 만나는 모습은 조용히 깨지는 인코딩이다. 규격은 CP949 인데 상대 기관이 시스템을 바꾸면서 UTF-8 로 보내기 시작한다. 운이 좋으면 디코딩이 예외로 끝나 바로 걸린다. 운이 나쁘면 일부 바이트가 다른 글자로 해석되어 이름만 깨진 채 그대로 들어간다. 그래서 인코딩만 보지 않고 바이트 폭까지 함께 본다. 한글 한 글자가 2바이트에서 3바이트로 바뀌면 줄 길이가 달라지므로, 폭 검사가 인코딩 사고를 한 번 더 잡아 준다.
두 번째는 부분 적재의 유혹이다. "998건은 멀쩡한데 2건 때문에 전부 돌려보내야 하나" 라는 말이 반드시 나온다. 돌려보내야 한다. 998건만 넣으면 상대의 트레일러 1000건과 우리 원장 998건이 어긋난 채로 남고, 다음 날 상대가 보낸 정정 파일에 그 2건이 다시 들어와 이번에는 중복이 된다. 전부 아니면 전무로 두면 사고는 하루에 끝난다.
세 번째는 사유 없는 거절이다. "형식 오류" 라고만 적어 돌려보내면 상대 담당자는 무엇을 고쳐야 할지 모른다. 어느 줄의 어느 필드가 무엇이어서 걸렸는지를 기계가 읽을 수 있는 거절 파일로 함께 돌려주면, 상대는 그 파일을 자기 배치에 그대로 물릴 수 있다.
실무에서 진짜 중요한 것
- 거절은 파일 단위다. 줄 단위 거절을 허용하는 순간 대사가 영원히 안 끝난다.
- 판정에는 관측값을 붙인다. "트레일러 합계 000000000050000 · 실제 46000" 처럼 양쪽을 다 적는다.
- 오류 코드는 계약이다. 배치 자동화가 코드로 다음 행동을 고른다. 트레일러 불일치는 재전송 요청, 중복은 무시, 인코딩은 상대 시스템 담당자 호출이다.
- 재전송은 정상 동작이다. 막지 말고 구분한다. 해시가 같으면 재전송, 일련번호만 같으면 충돌이다.
- 문지기는 파일을 고치지 않는다. 공백을 잘라 주고 인코딩을 바꿔 주기 시작하면, 상대 기관은 자기 파일이 규격을 어기고 있다는 사실을 영영 모른다.
다음 실습에서 할 것
다음 실습이 쓰는 80바이트 레코드 배치와 오류 코드 이름, 종료 코드는 이 실습의 가정이다. 기관마다 규격이 다르고, 현장에서 원본이 되는 것은 상대 기관과 주고받은 합의 문서다. 바뀌지 않는 것은 검사하는 겹의 순서와 전부 아니면 전무라는 원칙이다.
상대 기관 네 곳이 보내온 하루치 수신함을 직접 만들고, 그 앞에 설 문지기를 단계별로 쌓는다. 트레일러 대조에서 시작해 고정폭의 바이트 폭, 규격과 다른 인코딩, RFC 4180 인용, 필드 검증과 거절 파일, 재전송 판정까지 붙인다. 채점기는 매번 다른 기관 코드와 건수로 자기 수신 파일을 만들어 여러분의 문지기를 실행하고 판정을 대조한다. 마지막에는 자기 수신함을 처리해 수신 보고서를 남긴다.