파일 연동의 사고는 경계에서 난다
한 줄 요약
파일 연동은 낡은 방식이 아니라 대량·정산·대사의 표준 방식이다. 안전한 파일 수신의 핵심은 세 가지다 — 다 쓴 파일만 가져간다(완료 플래그), 가져온 것이 보낸 것과 같은지 확인한다(바이트 수·체크섬·트레일러), 두 번 돌려도 결과가 같다(원격 표시·파일 단위 적재 기록). 그리고 마지막에 우리 원장과 맞춰 본다(대사).
왜 이게 필요했나
보험 청구 수만 건, 카드 매입 전표, 급여 이체 명세, 일마감 정산 — 이런 것들은 건마다 API 를 부르지 않는다. 하루치를 파일 하나로 만들어 정해진 시각에 넘기고, 받는 쪽은 한 번에 적재한다. 상대 기관이 API 를 열어 주지 않는 경우도 많고, 무엇보다 파일은 그날의 경계를 분명하게 만든다. "9월 23일 청구분" 이 파일 하나라서, 건수와 합계를 맞춰 보고 틀리면 파일째 다시 받으면 된다.
그런데 파일 연동의 사고는 거의 전부 경계에서 난다. 상대가 아직 쓰고 있는 파일을 가져가 절반만 적재한다. 전송 중에 깨진 파일을 모른 채 넣는다. 배치를 재실행했더니 어제 파일을 또 넣어 청구가 두 번 잡힌다. 적재는 됐는데 우리 접수 원장과 세 건이 다른 것을 한 달 뒤 결산에서야 안다. 이 모듈의 단계가 이 네 가지 사고와 1:1 로 대응한다.
어떻게 동작하나
SFTP. SFTP 는 SSH 연결 위에서 도는 파일 전송 프로토콜이다. 운영에서는 sftp user@host 로 상대 서버에 붙고, 이때 SSH 가 상대 서버의 호스트 키를 검증한다(처음 본 키를 묻지 않고 받아들이는 설정은 중간자 공격을 허용한다). 자동화 배치는 대화형으로 묻고 답할 수 없으므로 -b <배치파일> 로 명령 목록을 넘기고, 배치 모드에서는 명령 하나가 실패하면 거기서 멈춘다(sftp(1)).
이 실습은 파드 하나 안에서 돈다. 파트너의 SFTP 서버를 SSH 서버(sshd)째로 따로 세우는 대신 sftp 의 -D 옵션을 쓴다 — 이 실습에서 배울 것은 SSH 서버 운영이 아니라 파일을 주고받는 규칙이기 때문이다. 매뉴얼은 -D 를 "ssh 를 거치지 않고 로컬의 sftp 서버에 직접 연결한다" 고 설명한다. SFTP 프로토콜(목록·받기·이름 바꾸기)은 운영과 같고, 줄인 것은 SSH 연결·인증·호스트 키 검증이다. 운영 배치를 쓸 때는 그 세 가지가 반드시 더해진다는 것을 잊지 말자.
완료 플래그. 파일을 다 올린 뒤에 올리는 작은 표식 파일이다. 받는 쪽은 플래그가 있는 파일만 가져간다. 플래그에 바이트 수와 sha256 을 적어 두면 전송 중 손상까지 잡힌다. SHA-256 은 입력이 한 비트만 달라도 전혀 다른 값이 나오는 해시라, 크기는 같은데 내용이 깨진 경우를 가려낸다.
원자적 이름 바꾸기. 받는 파일을 최종 이름으로 바로 쓰면, 받는 도중에 다른 배치(적재)가 그 파일을 집어 갈 수 있다. 임시 이름(.part)으로 다 받고 검증한 뒤 최종 이름으로 바꾼다. POSIX 의 rename() 은 같은 파일 시스템 안에서 대상 이름을 원자적으로 바꾼다 — 다른 프로세스는 옛 상태나 새 상태를 볼 뿐 중간 상태를 보지 않는다(rename). 다른 파일 시스템으로 옮기는 mv 는 복사 후 삭제라 이 성질이 없다. 그래서 임시 파일은 최종 디렉터리와 같은 곳에 둔다.
다시 돌려도 같게. 배치는 반드시 재실행된다(실패 후, 장애 복구 후, 누군가 실수로). 가져온 파일은 원격에서 이름 끝에 .fetched 를 붙여 표시하고, 적재는 파일 이름 단위로 기록(load_log)해 같은 파일을 두 번 넣지 않는다. 적재는 파일 하나를 트랜잭션 하나로 — 전부 들어가거나 아무것도 안 들어가거나. 중간에 청구번호가 겹치는 행이 나오면 앞의 행까지 되돌린다. 절반만 들어간 파일은 재실행으로도 고칠 수 없다.
헤더·트레일러. 파일 안에도 경계가 있다. H(헤더)는 무슨 파일인지(일자·송수신 기관), T(트레일러)는 몇 건에 합계가 얼마인지를 적는다. 받는 쪽은 D 줄을 직접 세고 더해 트레일러와 맞춰 본다. 트레일러가 없으면 파일이 끝까지 오지 않은 것이다. 줄 길이는 바이트로 잰다 — 한글 이름이 든 줄이 UTF-8 로 다시 저장되면 글자는 같아 보여도 줄이 길어진다(1모듈과 같은 함정).
대사(對査). 적재가 끝이 아니다. 파트너 파일과 우리 접수 원장을 청구번호로 맞춰, 양쪽에 있고 금액이 같은 것(일치), 파트너에만 있는 것, 우리에만 있는 것, 금액이 다른 것으로 나눈다. 불일치는 대개 업무 사건이다 — 우리가 접수를 빠뜨렸거나, 파트너가 취소분을 안 뺐거나. 대사 결과는 사람이 보는 보고서이므로 청구번호순으로, 금액은 양쪽을 다 보여 준다.
현장에서 만나는 모습
가장 흔한 사고는 "20시 전에 올린다" 는 약속만 믿고 20시에 가져가는 배치다. 어느 날 파트너 서버가 느려 20시 01분에 쓰기가 끝나고, 우리는 절반짜리 파일을 적재한다. 완료 플래그 없이 시각으로 동기화하면 반드시 한 번은 이렇게 된다. 두 번째는 재실행 사고다. 장애 복구 담당자가 "혹시 몰라" 배치를 한 번 더 돌렸고, 적재 기록이 없어 같은 파일이 두 번 들어갔다. 세 번째는 대사를 안 하는 것이다. 연동은 "에러 없이 끝났다" 로 성공을 판단하지만, 업무는 "숫자가 맞는다" 로 판단한다. 마지막으로, 운영 SFTP 배치에서 호스트 키 검증을 끄는 설정(StrictHostKeyChecking=no)을 편의상 넣는 경우가 있는데, 그것은 상대 서버를 확인하지 않고 청구 데이터를 주고받겠다는 뜻이다.
다음 실습에서 할 것
파트너 픽스처로 원격 디렉터리를 만들고 SFTP 배치로 목록을 본다. 그다음 수신 스크립트 fetch.sh 를 키운다 — 플래그 있는 파일만, 체크섬 대조와 격리, 임시 이름과 원격 표시. 이어서 헤더·트레일러 검증기 check.py, 파일 단위 적재기 load.py, 원장 대사 recon.py 를 만들고, 오늘 파일로 대사 보고서를 낸다.