고정길이와 JSON 사이의 변환 어댑터
목표
레이아웃 파일과 코드 매핑표를 읽어 고정길이 본문(EUC-KR)과 JSON 을 서로 바꾸는 변환기를 만들고, 바이트 길이·코드·인코딩·소수 규칙을 어기는 값을 거절하게 한다.
왜 중요한가
변환 계층의 실수는 조용하다. 글자 수로 센 길이는 뒤 필드를 모두 밀고, 모르는 코드를 흘려보내면 내부 시스템이 다른 뜻으로 읽고, float 을 거친 금리는 1 이 모자라고, 파이썬의 euc_kr 은 '똠' 을 8바이트로 바꿔 넘긴다. 그래서 규칙을 데이터(레이아웃·매핑표)로 두고, 고칠 수 없는 값은 고치지 않고 오류 코드로 거절한다.
단계
- 정의서
/opt/lab/fixtures/eaimw/transform/FXTR0001.md를/root/eaimw/xform/FXTR0001.layout으로 옮긴다. 머리글name,length,type,key,map,scale, 정의서 순서대로 12행. name 은 영문명, key 는 JSON 키, map 은 코드 매핑 도메인(없으면 빈칸), scale 은 이 전문에서는 빈칸. /opt/lab/fixtures/eaimw/transform/CODES.md를/root/eaimw/xform/codemap.csv로 옮긴다. 머리글domain,external,internal, 사용 중인 코드만, domain·external 순으로 정렬(LC_ALL=C sort)./root/eaimw/xform/f2j.py를 만든다.python3 f2j.py <레이아웃> <매핑표> <본문파일>이 JSON 한 줄을 출력하고 0 으로 끝난다. N 은 정수, AN·H 는 뒤 공백만 제거, 본문은 EUC-KR 로 엄격하게 읽고, map 이 있는 필드는 매핑표로 바꾼다. 거절할 때는 표준오류 첫 줄을 오류 코드로 시작하고 종료 코드 2: 매핑표에 없는 코드UNMAPPED, 본문 길이 불일치·숫자 아님·EUC-KR 로 읽을 수 없음BAD_VALUE./root/eaimw/xform/j2f.py를 만든다.python3 j2f.py <레이아웃> <매핑표> <JSON파일>이 본문 바이트를 표준출력으로 낸다. N 은 오른쪽 정렬·앞 0, AN·H 는 왼쪽 정렬·뒤 공백, 길이는 EUC-KR 바이트. 거절(종료 코드 2): 길이·자릿수 초과OVERFLOW(자르지 않는다), 매핑표에 없는 내부 코드UNMAPPED, 키 없음·N 이 0 이상의 정수가 아님BAD_VALUE./root/eaimw/xform/roundtrip.py를 만든다.python3 roundtrip.py <레이아웃> <매핑표> <본문파일>...이 파일마다<파일이름>,<결과>한 줄을 출력한다 — f2j 가 거절하면REJECT, f2j→j2f 결과가 원본 바이트와 같으면SAME, 다르면DIFF. 그리고 수신함의FXTR0001-*.body전부(이름순)에 대한 결과를 머리글file,result와 함께/root/eaimw/xform/roundtrip.csv로 저장한다.- 정의서
/opt/lab/fixtures/eaimw/transform/DPRT0001.md를/root/eaimw/xform/DPRT0001.layout으로 옮기고(암묵 소수점 필드는 scale 4), f2j·j2f 가 scale 을 지원하게 고친다. f2j 는"3.2500"처럼 scale 자리까지 쓴 문자열, j2f 는 문자열만 받아 decimal 로 계산한다. 소수 자리가 scale 보다 많으면OVERFLOW(반올림 금지), 문자열이 아니면(JSON 숫자 포함)BAD_VALUE. - j2f 가 KS X 1001 완성형 밖의 글자(예: '똠', 이모지)를
NOT_KSX1001로 거절하게 고친다(종료 코드 2). 한자·기호처럼 EUC-KR 2바이트인 글자는 그대로 받는다. - 수신함
/opt/lab/fixtures/eaimw/transform/inbox/전체를 f2j 로 변환한다. 파일 이름 앞부분(FXTR0001·DPRT0001)이 레이아웃 이름이다. 성공하면/root/eaimw/xform/out/<이름>.json(확장자.body를 뗀 이름), 거절되면 JSON 을 남기지 않는다. 결과를 머리글file,result, 파일 이름순, 결과는OK또는 오류 코드로/root/eaimw/xform/summary.csv에 적는다.
참고
- 바이트 다루기: 본문은
open(f, "rb").read(), 필드는body[pos:pos + 길이], 글자로 바꿀 때.decode("euc_kr"). 길이 검사는len(값.encode("euc_kr")). - 오류 출력:
print("UNMAPPED ...", file=sys.stderr); sys.exit(2). 바이트 출력:sys.stdout.buffer.write(본문). - j2f 는 f2j 의
load_layout·load_codemap을 가져다 쓰면 된다(from f2j import ...— 같은 디렉터리의 파일은 그대로 import 된다). - 흔한 실수: 글자 수로 길이를 세는 것, 넘친 값을 잘라서 넘기는 것, 모르는 코드를 원문 그대로 넘기는 것, 금리를 float 으로 곱하는 것.
- 채점기는 매번 새 레이아웃·매핑표·값을 만들어 인자로 넘긴다. 정의서의 필드 이름을 코드에 적어 두면 통과할 수 없다.
- 공용 라이브러리
/opt/lab/fixtures/eaimw/lib/lhconv.py는 4모듈부터 쓴다. 이 실습에서는 직접 만든다.
정의서를 레이아웃 파일로 옮긴다
/opt/lab/fixtures/eaimw/transform/FXTR0001.md 의 12개 필드를 /root/eaimw/xform/FXTR0001.layout(name,length,type,key,map,scale)으로 옮긴다.
정의서 1절의 표를 그대로 옮깁니다. name 은 영문명, key 는 JSON 키(대소문자 그대로), map 은 코드 매핑 열입니다. 길이를 다 더하면 정의서가 적은 업무부 합계와 같아야 합니다.
코드 정의서를 매핑표로 옮긴다
/opt/lab/fixtures/eaimw/transform/CODES.md 의 사용 중인 코드를 /root/eaimw/xform/codemap.csv(domain,external,internal)로 옮기고 domain·external 순으로 정렬한다.
도메인은 BANK·CHANNEL·KIND 셋입니다. 상태가 사용 중지인 행은 싣지 않습니다 — 그래야 그 코드가 든 전문이 변환에서 걸립니다. 대외 코드 01 의 앞 0 은 값의 일부입니다(엑셀로 열면 사라지는 그 0 입니다).
고정길이 → JSON
/root/eaimw/xform/f2j.py <레이아웃> <매핑표> <본문> 이 JSON 을 출력하고, 모르는 코드는 UNMAPPED, 형식·인코딩 오류는 BAD_VALUE 로 거절한다(종료 코드 2).
open(파일, 'rb') 로 읽어 레이아웃의 길이만큼 바이트로 자릅니다. 필드마다 decode('euc_kr') 을 하면 반쪽 한글과 CP949 전용 글자가 UnicodeDecodeError 로 드러납니다. N 은 strip 뒤 숫자인지 확인해 int 로, AN·H 는 rstrip(' ') 만 합니다. 매핑표에 없는 코드를 원문 그대로 넘기지 마세요.
JSON → 고정길이, 바이트로 센다
/root/eaimw/xform/j2f.py <레이아웃> <매핑표> 이 본문 바이트를 내고, 길이 초과는 OVERFLOW, 모르는 내부 코드는 UNMAPPED, 키 없음·정수 아님은 BAD_VALUE 로 거절한다.
H 필드의 길이는 len(값) 이 아니라 len(값.encode('euc_kr')) 입니다. 넘치면 잘라서 맞추지 말고 거절합니다 — 바이트로 자르면 한글이 반으로 갈립니다. 매핑은 매핑표를 뒤집은 사전(내부 → 대외)으로 찾습니다. N 은 rjust(길이, '0'), 나머지는 ljust(길이, b' ').
왕복 시험
/root/eaimw/xform/roundtrip.py 가 본문마다 SAME·DIFF·REJECT 를 가리고, 수신함 FXTR0001 표본 전부의 결과를 /root/eaimw/xform/roundtrip.csv 로 저장한다.
f2j 와 j2f 를 함수로 import 해서 씁니다. f2j 가 거절하면 REJECT, 되살린 바이트가 원본과 같으면 SAME 입니다. DIFF 가 나온 표본은 변환기 버그일 수도 있지만 상대가 표준을 어긴 것일 수도 있습니다 — 그 필드의 바이트를 od -c 로 보세요.
암묵 소수점은 decimal 로
/root/eaimw/xform/DPRT0001.layout 을 만들고(scale 4), f2j 는 "3.2500" 같은 문자열을, j2f 는 문자열 소수만 받아 decimal 로 계산하게 고친다.
0032500 을 scale 4 로 읽으면 Decimal(32500).scaleb(-4) = 3.2500 입니다. 되돌릴 때는 Decimal("3.25").scaleb(4) = 32500 입니다. float("0.29") * 100 은 28.999999999999996 이라 int 로 바꾸면 28 이 됩니다. 소수 자리가 scale 보다 많으면 반올림하지 말고 거절하세요.
완성형 밖의 글자를 거절한다
j2f 가 KS X 1001 완성형에 없는 글자('똠', 이모지)를 NOT_KSX1001 로 거절하고(종료 코드 2), 한자·기호 같은 2바이트 글자는 받게 고친다.
먼저 python3 -c "print('똠'.encode('euc_kr'))" 를 해 보세요 — 오류가 아니라 8바이트가 나옵니다. 글자마다 encode('euc_kr') 의 길이가 2 인지 확인하면 됩니다. 인코딩 자체가 실패하는 글자(이모지)도 같은 코드로 거절합니다.
수신함 전체를 변환한다
수신함의 모든 본문을 f2j 로 변환해 성공한 것은 /root/eaimw/xform/out/<이름>.json 으로, 결과 목록은 /root/eaimw/xform/summary.csv(file,result)로 남긴다.
파일 이름의 '-' 앞부분이 레이아웃 이름입니다. 종료 코드 2 면 표준오류 첫 줄의 첫 단어가 오류 코드입니다. 셸 리다이렉션은 실패해도 빈 파일을 만들어 두므로, 거절된 표본의 JSON 파일은 지우세요.