From 64d94a1fdcca8034847c44369f8277b15d4379bd Mon Sep 17 00:00:00 2001 From: curry772 Date: Wed, 2 Sep 2026 17:25:16 +0900 Subject: [PATCH] =?UTF-8?q?=ED=91=9C=EC=A4=80=20=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=95=84=EC=9B=83=20DATA=20=EB=A5=BC=20GRID=20=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=95=EC=A0=95=20+=20MSG=EB=B6=80=20=ED=8C=8C=EC=8B=B1=20?= =?UTF-8?q?=ED=9A=8C=EA=B7=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit standard-layout-djb.csv 의 DATA 가 GROUP(3)으로 선언돼 있어, 매핑설정의 BIZ_DATA=DATA[0].BIZ_DATA 인덱스 접근이 실패했다. getBizDataBytes() 가 null 을 반환하고 makeupDataScopLen 이 이를 그대로 역참조해 NPE 가 나면서 coordinateBeforeRequest/Response 가 통째로 실패, 관련 테스트 9건이 깨져 있었다. 운영 레이아웃(DB)대로 GRID(4)로 정정한다. 이 CSV 는 layout.file.type=CSV 인 테스트 설정에서만 사용된다. - DATA 픽스처를 실제 전문과 같은 배열 형태로 변경 - GRID 는 인덱스 없는 경로가 직렬화에 쓰이지 않는 템플릿을 가리키므로, 조회/단정 경로를 운영과 같은 DATA[0] 형태로 변경 - 상대 헤더가 표준을 어긴 오류응답(dman_rspn_dvcd=S) 회귀 테스트 2건 추가. 같은 전문에 있던 선행 0 숫자(00001)와 이스케이프되지 않은 개행/탭도 픽스처에 포함 알려진 별건: DATA 가 GRID 이므로 StandardMessageCoordinatorDJB.DATA_SCOP_LEN ("DATA.data_scop_len") 이 행이 아닌 템플릿에 기록되어 data_scop_len 이 재계산되지 않고 수신값이 그대로 나간다. JSON 경로에서는 영향이 없어 별도 건으로 둔다. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UnybKuxcuPafGhVGh4wqkZ --- src/main/resources/standard-layout-djb.csv | 2 +- .../message/StandardMessageJsonFlowTest.java | 107 +++++++++++++++++- 2 files changed, 102 insertions(+), 7 deletions(-) diff --git a/src/main/resources/standard-layout-djb.csv b/src/main/resources/standard-layout-djb.csv index 3708b28..955efef 100644 --- a/src/main/resources/standard-layout-djb.csv +++ b/src/main/resources/standard-layout-djb.csv @@ -168,7 +168,7 @@ err_chrg_cnpl_no,3,2,0,1,20,1,,, # ============================================================ # DATA - 데이터부 # ============================================================ -DATA,1,3,1,1,0,1,,, +DATA,1,4,1,1,0,1,,, data_dvcd,2,2,1,1,2,1,,,IO data_scop_len,2,2,1,1,8,2,,, BIZ_DATA,2,9,1,1,0,1,,, diff --git a/src/test/java/com/eactive/eai/custom/message/StandardMessageJsonFlowTest.java b/src/test/java/com/eactive/eai/custom/message/StandardMessageJsonFlowTest.java index 63857c3..29d2c61 100644 --- a/src/test/java/com/eactive/eai/custom/message/StandardMessageJsonFlowTest.java +++ b/src/test/java/com/eactive/eai/custom/message/StandardMessageJsonFlowTest.java @@ -40,6 +40,7 @@ import static org.mockito.ArgumentMatchers.anyString; * 5. 비표준 수신 → 정상 응답 표준 생성 (coordinateAfterRecvNonStdSyncResponse → JSON) * 6. 오류 응답 표준 생성 (coordinateSetStandardMessageError → JSON) * 7. JSON 왕복(parse → serialize) 일관성 + * 8. 상대 헤더가 표준을 어긴 응답 파싱 (2026-09 사고 회귀 방지) */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class StandardMessageJsonFlowTest { @@ -80,11 +81,11 @@ class StandardMessageJsonFlowTest { + "\"frst_mesg_dman_dt\":\"20260420\"," + "\"frst_mesg_dman_time\":\"120000000\"" + "}," - + "\"DATA\":{" + + "\"DATA\":[{" + "\"data_dvcd\":\"IO\"," + "\"data_scop_len\":\"37\"," + "\"BIZ_DATA\":{\"acct_no\":\"123456789\",\"amt\":\"100000\"}" - + "}" + + "}]" + "}"; /** @@ -121,10 +122,58 @@ class StandardMessageJsonFlowTest { + "\"msg_list_rowcnt\":\"1\"" + "}" + "}," - + "\"DATA\":{" + + "\"DATA\":[{" + "\"data_dvcd\":\"IO\"," + "\"data_scop_len\":\"37\"," + "\"BIZ_DATA\":{\"acct_no\":\"123456789\",\"bal\":\"900000\"}" + + "}]" + + "}"; + + /** + * 상대 헤더가 표준을 어긴 오류응답 JSON (2026-09 실제 사고 전문 형태) + * + * 표준은 응답 시 dman_rspn_dvcd=R 이어야 하는데, 상대가 요청값 S 를 그대로 에코백하면서 + * procs_rslt_dvcd 만 F(오류)로 보냈다. MSG 블록의 레이아웃 조건이 !S 라 예전에는 + * MSG 이하가 통째로 버려졌고, MAIN_MSG 가 레이아웃 기본값으로 남아 오류가 + * "정상처리되었습니다." 로 보고되고 거래로그에서도 MSG 부가 누락됐다. + * + * 같은 전문에 들어있던 다른 비표준 요소도 함께 담아 둔다. + * - msg_list_rowcnt 가 따옴표 없는 선행 0 숫자(00001) + * - outp_msg_desc 안에 이스케이프되지 않은 개행/탭 + */ + private static final String RSP_JSON_BAD_HEADER = + "{" + + "\"HEAD\":{" + + "\"nxgn_stnd_idfr\":\"JERA\"," + + "\"guid\":\"" + TEST_GUID + "\"," + + "\"guid_prgs_no\":\"2\"," + + "\"stnd_mesg_ver\":\"R10\"," + + "\"ortr_guid\":\"" + TEST_GUID + "\"," + + "\"dman_rspn_dvcd\":\"S\"," // 표준 위반: 응답인데 R 이 아님 + + "\"if_id\":\"" + TEST_IF_ID + "\"," + + "\"procs_rslt_dvcd\":\"F\"," + + "\"tx_id\":\"" + TEST_TX_ID + "\"," + + "\"chnl_tycd\":\"EAI\"," + + "\"hmab_dvcd\":\"1\"," + + "\"trnm_sys_dvcd\":\"EAI\"," + + "\"sys_env_dvcd\":\"D\"," + + "\"frst_mesg_dman_dt\":\"20260420\"," + + "\"frst_mesg_dman_time\":\"120000000\"," + + "\"mesg_rspn_dt\":\"20260420\"," + + "\"mesg_rspn_time\":\"120100000\"," + + "\"rprs_msg_cd\":\"900400\"" + + "}," + + "\"MSG\":{" + + "\"msg_dvcd\":\"EM\"," + + "\"msg_scop_len\":1437," + + "\"MAIN_MSG\":{" + + "\"outp_atrb_cd\":\"0\"," + + "\"outp_msg_cd\":\"900400\"," + + "\"outp_msg_ctnt\":\"\"," + + "\"outp_msg_desc\":\"[ErrorCode:S9OWP0011].WTC.Adapter.\n\t호출 오류\"," + + "\"mngm_msg_cd\":\"\"," + + "\"msg_list_rowcnt\":00001" + + "}" + "}" + "}"; @@ -208,7 +257,11 @@ class StandardMessageJsonFlowTest { void 요청JSON_파싱_DATA_BIZ_DATA_JSON_오브젝트_저장() throws Exception { StandardMessage msg = parseReq(); - String bizData = msg.findItemValue("DATA.BIZ_DATA"); + // DATA는 GRID(반복부)라 값은 행(row 0)에 들어간다. 인덱스 없는 "DATA.BIZ_DATA"는 + // 직렬화에 쓰이지 않는 템플릿 항목을 가리키므로 빈 값이 나온다. + // 운영 경로도 인덱스 형태를 쓴다 — standard-message-mapping-config-djb.properties:27 + // BIZ_DATA=DATA[0].BIZ_DATA + String bizData = msg.findItemValue("DATA[0].BIZ_DATA"); assertNotNull(bizData, "BIZ_DATA가 null"); assertFalse(bizData.trim().isEmpty(), "BIZ_DATA가 공백"); // JSON 오브젝트로 파싱 가능한지 검증 @@ -344,9 +397,11 @@ class StandardMessageJsonFlowTest { String json = msg.getDataString(MessageType.JSON, "utf-8"); JsonNode root = jacksonMapper.readTree(json); + // DATA는 GRID(반복부)이므로 직렬화하면 JSON 배열이다. 표준 레이아웃상 1건 고정. // BIZ_DATA는 JSON 문자열 또는 JSON 오브젝트로 들어갈 수 있음 - assertNotNull(root.path("DATA").get("BIZ_DATA"), - "직렬화 JSON에 DATA.BIZ_DATA 없음"); + assertTrue(root.path("DATA").isArray(), "직렬화 JSON의 DATA가 배열이 아님"); + assertNotNull(root.path("DATA").path(0).get("BIZ_DATA"), + "직렬화 JSON에 DATA[0].BIZ_DATA 없음"); } // ================================================================ @@ -490,6 +545,40 @@ class StandardMessageJsonFlowTest { "응답 JSON 왕복 후 msg_dvcd 불일치"); } + // ================================================================ + // 8. 상대 헤더가 표준을 어긴 응답 파싱 (2026-09 사고 회귀 방지) + // ================================================================ + + @Test + void 상대헤더오류_dman_rspn_dvcd가_S여도_MSG부가_파싱됨() throws Exception { + StandardMessage msg = parseRspBadHeader(); + + assertEquals("F", msg.findItemValue("HEAD.procs_rslt_dvcd"), + "HEAD가 파싱되지 않음 — 전제 조건 실패"); + assertEquals("EM", msg.findItemValue("MSG.msg_dvcd"), + "MSG.msg_dvcd 미반영 — MSG 블록이 버려졌다"); + assertEquals("900400", msg.findItemValue("MSG.MAIN_MSG.outp_msg_cd"), + "outp_msg_cd가 레이아웃 기본값(NCMM00001) 그대로 — MSG부가 버려졌다"); + assertNotEquals("정상처리되었습니다.", msg.findItemValue("MSG.MAIN_MSG.outp_msg_ctnt"), + "오류응답인데 레이아웃 기본값 '정상처리되었습니다.'가 그대로 남음"); + assertNotNull(msg.findItemValue("MSG.MAIN_MSG.outp_msg_desc"), + "outp_msg_desc가 null — 개행/탭이 섞인 값이 파싱되지 않았다"); + } + + @Test + void 상대헤더오류_재직렬화시_MSG부_보존() throws Exception { + StandardMessage msg = parseRspBadHeader(); + + // 거래로그(EAILogDAO)는 수신 원문이 아니라 StandardMessage 재직렬화본을 저장한다. + // MSG가 hidden 이면 로그에서도 MSG부 이후가 통째로 누락된다. + String json = msg.getDataString(MessageType.JSON, "utf-8"); + JsonNode root = jacksonMapper.readTree(json); + + assertTrue(root.has("MSG"), "재직렬화 결과에 MSG 블록 없음 — 거래로그에 MSG부 누락"); + assertEquals("900400", root.path("MSG").path("MAIN_MSG").path("outp_msg_cd").asText(), + "재직렬화 결과의 outp_msg_cd 불일치"); + } + // ================================================================ // helper // ================================================================ @@ -505,4 +594,10 @@ class StandardMessageJsonFlowTest { jsonReader.parse(msg, RSP_JSON_OK); return msg; } + + private StandardMessage parseRspBadHeader() throws Exception { + StandardMessage msg = manager.getStandardMessage(); + jsonReader.parse(msg, RSP_JSON_BAD_HEADER); + return msg; + } }