変換は境界でだけ行い、変換できないものは拒否する
一言でいうと
変換アダプターは、標準電文(固定長・EUC-KR・外部コード)と内部JSON(UTF-8・内部コード)の間の境界1か所でだけ形式を変えます。ルールはレイアウトとコードマッピング表というデータに置き、知らないコード・あふれる長さ・表現できない文字は直して渡さず、拒否します。
なぜ必要なのか
銀行の中では、2つの世界が共存しています。勘定系と外部機関は数十年前の固定長電文を使い、新しいチャネルと内部APIはJSONを使います。2つの世界を直接つなぐと、新しいチャネルを作るチームごとにEUC-KRとバイトパディングを学び直し、外部コードと内部コードの対応表をそれぞれ1セットずつ抱えることになります。表が複数セットあれば、いつか必ず1セットだけが古くなります。
『Enterprise Integration Patterns』は、この位置にMessage Translatorを置きます。異なるデータ形式を使うアプリケーションの間に、形式を変えるフィルターを挟むもので、オブジェクト指向のAdapterパターンをメッセージングに移したものだと説明しています。ハブに変換を集めれば、内側のシステムはJSONと内部コードだけを知っていればよく、外側の標準が変わっても直す場所はアダプター1つです。
どう動くのか
レイアウトはデータです。フィールドごとに名前・長さ・形式・JSONキー・マッピングドメイン・小数桁数(scale)を1行ずつ書いた表を置き、変換器はその表を解釈するだけです。インターフェース仕様書が変わったら表を直します。取引ごとに変換コードを別々に書くと、取引が100個あればパディング規則も100セットになります。
長さはバイトです。形式は3つです。N(数字)は右寄せで前を0で埋め、AN(英数字)とH(ハングル混在)は左寄せで後ろを空白で埋めます。Hフィールドの長さはEUC-KRのバイト数なので、ハングル10文字で20バイトのフィールドがいっぱいになります。文字数で数えると、ハングル11文字の名前が「20文字以下」の検査を通って22バイトになり、後ろのすべてのフィールドが2バイトずつずれます。
あふれたら切りません。長さを合わせるためにバイトで切ると、ハングル1文字が半分に割れます。受信側はそのフィールドをデコードできないか、残った最初のバイトを次のフィールドの最初のバイトとくっつけて、まったく別の文字として読みます。文字単位で切っても問題は残ります。受取人の名前が途切れた振込は、業務上の事故です。変換器は拒否し、短くするかどうかはチャネルと業務が決めます。
コードは表で変換し、知らないコードは拒否します。電文の銀行コード201は、内部ではGRMです。対応はマッピング表1か所に置き、双方向で使います。表にないコードを元のまま流すと、内部システムはその値を自分のコード体系で解釈します。たまたま同じ値が別の意味で存在すると、見当違いの場所へお金が行きます。合併で使用停止になったコードも表から外し、入ってきた瞬間に引っかかるようにします。
往復が変換器の仕様です。標準どおりに作った本文をJSONに変え、また本文に戻したとき、元のバイトと1バイトも違ってはいけません。この性質から、ルールがいくつか自然に導かれます。ANとHは後ろの空白だけを取り除きます。前の空白まで取ると、戻すときに消えてしまいます。Nを整数に変えてよいのは、前の0が埋め草にすぎず値ではないからです。変換器を直すたびにサンプル全体で往復を回せば、何かを失う修正はその場で見つかります。
金額と金利は浮動小数点を通しません。電文の金利0032500は暗黙の小数点が小数第4位なので、3.2500です。Python decimalドキュメントは、2進浮動小数点では1.1 + 2.2が3.3000000000000003に見え、0.1 + 0.1 + 0.1 - 0.3が0にならないため等式の検査を信用できないと説明し、そのため厳密な等式が必要な会計にはdecimalが適すると記しています。同じドキュメントは、Decimal(0.1)とDecimal('0.1')が違うことも示しています。一度floatになった値は、decimalに戻してももう手遅れです。そこでJSONでも数値ではなく文字列"3.2500"でやり取りします。後ろの0の2つは桁数を知らせる情報で、decimalは1.30 + 1.20を2.50のままにして、この表記を守ります。実際にfloat("0.29") * 100は28.999999999999996なので、整数に変えると1足りなくなります。
EUC-KRという名前を信用しません。EUC-KRが収めるハングルは、KS X 1001完成形の2,350字です。Windowsで一般的なCP949(UHC)は、これに残りのハングルを加え、Pythonで数えるとUnicodeのハングル音節11,172字をすべて2バイトで収めます。2,350字は2つのエンコーディングでバイトが同じなので、普段はまったく問題がなく、ttomと読む字のような文字でだけ問題が表に出ます。さらに厄介なのは、実装ごとに違うという点です。Python標準エンコーディング表のeuc_krコーデックは、ttomと読む字をエラーとして拒否しません。CPythonソースのコメントのとおり、KS X 1001:1998の結合シーケンスに変えて8バイトを出します。同じ文字をglibcのiconv -t EUC-KRは拒否します。2バイトの完成形しか知らない相手は、その8バイトを子音字・母音字の4文字として読みます。そのため変換器が、文字ごとに「完成形の2バイトか」を自分で判定し、違えば拒否します。
現場での姿
半分のハングル。摘要を30バイトに合わせるためにバイトで切ったチャネルがあると、外部機関は電文全体を形式エラーとして返してきます。エラーは外部側で出ますが、原因は自分たちのチャネルの切り詰めです。CP949が混ざった本文。あるチャネルがファイルをCP949で保存して送ると、数か月は問題なく動き、名前に完成形にない文字がある顧客が初めて来た日に壊れます。変換器がEUC-KRで厳密に読んで拒否すれば、その日のうちに原因が見えます。空白で埋めた数字。金額フィールドを0ではなく空白で埋めて送ってくるシステムがあります。読むことはできますが、往復するとバイトが変わります。往復テストは、変換器のバグだけでなく、こうした標準に違反した相手も見つけ出します。
次のラボですること
インターフェース仕様書をレイアウトファイルに、コード定義書をマッピング表に書き写したあと、変換器2つ(f2j.py・j2f.py)を自分で作ります。サンプルすべてで往復テストを回し、暗黙の小数点をdecimalで扱い、完成形にない文字を拒否するように直してから、受信箱全体を変換します。共通ライブラリlhconv.pyが同じ処理を行いますが、このラボでは使いません。モジュール4から使います。採点ツールは毎回、新しいレイアウトと新しい値(ハングルを含む)で変換器を動かします。