Merge branch 'feature/msg-block-parse-skip'

- 표준 레이아웃 DATA 를 GRID 로 정정
- MSG부 파싱 회귀 테스트 추가

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnybKuxcuPafGhVGh4wqkZ
This commit is contained in:
curry772
2026-09-03 08:58:31 +09:00
2 changed files with 102 additions and 7 deletions
+1 -1
View File
@@ -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,,,
1 #-----------------------------------------------------------------------------
168 # ============================================================
169 # DATA - 데이터부
170 # ============================================================
171 DATA,1,3,1,1,0,1,,, DATA,1,4,1,1,0,1,,,
172 data_dvcd,2,2,1,1,2,1,,,IO
173 data_scop_len,2,2,1,1,8,2,,,
174 BIZ_DATA,2,9,1,1,0,1,,,
@@ -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;
}
}