Merge branch 'feature/std-msg-explicit-success-value'

정상 응답 MSG 코드/메시지를 레이아웃 기본값 대신 coordinateBeforeResponse 에서
명시적으로 설정한다. 운영 반영에는 DB 표준전문 정의의 기본값 제거가 함께 필요하다.

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 10:20:23 +09:00
4 changed files with 94 additions and 13 deletions
@@ -77,7 +77,15 @@ public class StandardMessageCoordinatorDJB extends DefaultStandardMessageCoordin
// DATA 영역 경로
public static final String DATA_SCOP_LEN = "DATA.data_scop_len";
// 정상 응답 메시지.
// 예전에는 표준전문 레이아웃의 기본값으로 붙였으나, 그렇게 두면 "아무도 채우지 않은 상태" 와
// "정상이라고 판정한 상태" 가 구분되지 않는다. 실제로 2026-09 사고에서 MSG 부 파싱이 통째로
// 스킵됐는데도 오류가 "정상처리되었습니다." 로 보고됐다.
// 이제 레이아웃 기본값을 없애고, 정상이라고 판정한 지점에서만 명시적으로 설정한다.
public static final String NORMAL_OUTP_MSG_CD = "NCMM00001";
public static final String NORMAL_OUTP_MSG_CTNT = "정상처리되었습니다.";
private String[] netInfo;
@Override
@@ -156,7 +164,21 @@ public class StandardMessageCoordinatorDJB extends DefaultStandardMessageCoordin
standardMessage.setData(HEAD_MESG_RSPN_DT, now.format(FMT_DATE));
standardMessage.setData(HEAD_MESG_RSPN_TIME, now.format(FMT_TIME_MILLIS));
// 정상 응답의 MSG 코드/메시지를 여기서 채운다. 레이아웃 기본값을 없앴으므로
// 이 지점이 정상 응답 메시지의 유일한 출처다.
// coordinateBeforeResponse 는 모든 동기 응답이 거치는 마지막 관문이라
// (RequestProcessor 에서 호출) 경로를 빠짐없이 덮는다.
//
// 반드시 두 조건을 함께 본다.
// 1) 정상 응답일 때만. 오류 응답에 "정상처리되었습니다." 를 채우면 2026-09 사고
// (MSG 부가 버려졌는데 오류가 정상으로 보고됨)를 코드로 재현하는 셈이다.
// 2) 값이 비어 있을 때만. 대외계가 표준응답으로 MSG 를 채워 보낸 경우 그 값이
// 우선이며 덮어쓰면 안 된다.
if (STDMessageKeys.RESPONSE_TYPE_CODE_N.equals(mapper.getResponseType(standardMessage))) {
setIfBlank(standardMessage, MSG_OUTP_MSG_CD, NORMAL_OUTP_MSG_CD);
setIfBlank(standardMessage, MSG_OUTP_MSG_CTNT, NORMAL_OUTP_MSG_CTNT);
}
StandardItem msgListRowCnt = standardMessage.findItem(StandardMessageCoordinatorDJB.MSG_LIST_ROWCNT);
if (msgListRowCnt == null || "0".equals(msgListRowCnt.getValue()) || "".equals(msgListRowCnt.getValue())) {
StandardItem msgPart = standardMessage.findItem("MSG");
@@ -249,8 +271,9 @@ public class StandardMessageCoordinatorDJB extends DefaultStandardMessageCoordin
mainMsgItem.setSize(1);
}
// msg_dvcd=NM, outp_atrb_cd=0, outp_msg_cd, outp_msg_ctnt, msg_list_rowcnt=0
// → 모두 CSV 기본값 사용
// msg_dvcd=NM, outp_atrb_cd=0, msg_list_rowcnt=0 → 레이아웃 기본값 사용
// outp_msg_cd, outp_msg_ctnt → 레이아웃 기본값을 없앴으므로 여기서는 비워 두고,
// 모든 동기 응답이 거치는 coordinateBeforeResponse 에서 정상 응답일 때만 채운다.
makeMsgScopLen(responseMessage);
}
@@ -270,6 +293,17 @@ public class StandardMessageCoordinatorDJB extends DefaultStandardMessageCoordin
}
}
/**
* 항목이 존재하고 값이 비어 있을 때만 설정한다.
* 이미 채워진 값(대외계가 보낸 MSG, 오류 경로가 설정한 값 등)은 그대로 보존한다.
*/
private void setIfBlank(StandardMessage standardMessage, String path, String value) {
StandardItem item = standardMessage.findItem(path);
if (item != null && StringUtils.isBlank(item.getValue())) {
item.setValue(value);
}
}
private void resolveCallPropRefValues(StandardMessage standardMessage, Properties prop) {
if (prop == null) return;
resolveCallPropInChilds(standardMessage.getChilds(), prop);
+2 -2
View File
@@ -151,8 +151,8 @@ msg_dvcd,2,2,0,1,2,1,,,NM
msg_scop_len,2,2,0,1,8,2,,,
MAIN_MSG,2,3,0,1,0,1,,,
outp_atrb_cd,3,2,1,1,1,1,,,0
outp_msg_cd,3,2,1,1,12,1,,,NCMM00001
outp_msg_ctnt,3,2,1,1,200,1,,,정상처리되었습니다.
outp_msg_cd,3,2,1,1,12,1,,,
outp_msg_ctnt,3,2,1,1,200,1,,,
outp_msg_desc,3,2,1,1,300,1,,,
mngm_msg_cd,3,2,1,1,12,1,,,
msg_list_rowcnt,3,2,0,1,5,2,,,0
1 #-----------------------------------------------------------------------------
151 msg_scop_len,2,2,0,1,8,2,,,
152 MAIN_MSG,2,3,0,1,0,1,,,
153 outp_atrb_cd,3,2,1,1,1,1,,,0
154 outp_msg_cd,3,2,1,1,12,1,,,NCMM00001 outp_msg_cd,3,2,1,1,12,1,,,
155 outp_msg_ctnt,3,2,1,1,200,1,,,정상처리되었습니다. outp_msg_ctnt,3,2,1,1,200,1,,,
156 outp_msg_desc,3,2,1,1,300,1,,,
157 mngm_msg_cd,3,2,1,1,12,1,,,
158 msg_list_rowcnt,3,2,0,1,5,2,,,0
@@ -324,20 +324,59 @@ class StandardMessageCoordinatorDJBTest {
"정상 수신 시 msg_dvcd는 NM이어야 함");
}
// outp_msg_cd / outp_msg_ctnt 는 레이아웃 기본값을 없앴으므로 이 시점에는 비어 있고,
// 실제 흐름대로 coordinateBeforeResponse 까지 거쳐야 채워진다.
@Test
void 정상수신시_outp_msg_cd_설정됨() {
void 정상수신시_outp_msg_cd_는_응답조정후_설정됨() {
coordinator.coordinateAfterRecvNonStdSyncResponse(message);
assertTrue(isBlankValue(message.findItemValue("MSG.MAIN_MSG.outp_msg_cd")),
"수신 조정 단계에서는 아직 비어 있어야 함 (레이아웃 기본값이 없어야 한다)");
String msgCd = message.findItemValue("MSG.MAIN_MSG.outp_msg_cd");
assertFalse(msgCd == null || msgCd.trim().isEmpty(), "outp_msg_cd가 설정되어야 함");
coordinator.coordinateBeforeResponse(message, null, null, "utf-8");
assertEquals(StandardMessageCoordinatorDJB.NORMAL_OUTP_MSG_CD,
message.findItemValue("MSG.MAIN_MSG.outp_msg_cd"),
"정상 응답의 outp_msg_cd 가 설정되어야 함");
}
@Test
void 정상수신시_outp_msg_ctnt_설정됨() {
void 정상수신시_outp_msg_ctnt_는_응답조정후_설정됨() {
coordinator.coordinateAfterRecvNonStdSyncResponse(message);
assertTrue(isBlankValue(message.findItemValue("MSG.MAIN_MSG.outp_msg_ctnt")),
"수신 조정 단계에서는 아직 비어 있어야 함 (레이아웃 기본값이 없어야 한다)");
String ctnt = message.findItemValue("MSG.MAIN_MSG.outp_msg_ctnt");
assertFalse(ctnt == null || ctnt.trim().isEmpty(), "outp_msg_ctnt가 설정되어야 함");
coordinator.coordinateBeforeResponse(message, null, null, "utf-8");
assertEquals(StandardMessageCoordinatorDJB.NORMAL_OUTP_MSG_CTNT,
message.findItemValue("MSG.MAIN_MSG.outp_msg_ctnt"),
"정상 응답의 outp_msg_ctnt 가 설정되어야 함");
}
@Test
void 오류응답에는_정상메시지가_채워지지_않는다() {
coordinator.coordinateSetStandardMessageError(message, mapper, "RECEAIINA001", "테스트오류");
coordinator.coordinateBeforeResponse(message, null, null, "utf-8");
assertNotEquals(StandardMessageCoordinatorDJB.NORMAL_OUTP_MSG_CD,
message.findItemValue("MSG.MAIN_MSG.outp_msg_cd"),
"오류 응답에 정상코드가 채워지면 안 됨");
assertNotEquals(StandardMessageCoordinatorDJB.NORMAL_OUTP_MSG_CTNT,
message.findItemValue("MSG.MAIN_MSG.outp_msg_ctnt"),
"오류 응답에 '정상처리되었습니다.' 가 채워지면 안 됨 — 2026-09 사고의 재현");
}
@Test
void 대외계가_보낸_MSG_값은_덮어쓰지_않는다() {
coordinator.coordinateAfterRecvNonStdSyncResponse(message);
message.setData("MSG.MAIN_MSG.outp_msg_cd", "ABC00001");
message.setData("MSG.MAIN_MSG.outp_msg_ctnt", "상대가 보낸 메시지");
coordinator.coordinateBeforeResponse(message, null, null, "utf-8");
assertEquals("ABC00001", message.findItemValue("MSG.MAIN_MSG.outp_msg_cd"),
"이미 채워진 outp_msg_cd 를 덮어쓰면 안 됨");
assertEquals("상대가 보낸 메시지", message.findItemValue("MSG.MAIN_MSG.outp_msg_ctnt"),
"이미 채워진 outp_msg_ctnt 를 덮어쓰면 안 됨");
}
@Test
@@ -496,4 +535,8 @@ class StandardMessageCoordinatorDJBTest {
f.setAccessible(true);
f.set(item, refValue);
}
private static boolean isBlankValue(String value) {
return value == null || value.trim().isEmpty();
}
}
@@ -443,11 +443,15 @@ class StandardMessageJsonFlowTest {
void 비표준수신_정상응답_JSON_직렬화_outp_msg_ctnt_설정됨() throws Exception {
StandardMessage msg = manager.getStandardMessage();
coordinator.coordinateAfterRecvNonStdSyncResponse(msg);
// outp_msg_cd/ctnt 는 레이아웃 기본값이 없으므로, 실제 흐름대로 응답 직전 조정까지
// 거쳐야 채워진다 (StandardMessageCoordinatorDJB.coordinateBeforeResponse)
coordinator.coordinateBeforeResponse(msg, null, null, "utf-8");
String json = msg.getDataString(MessageType.JSON, "utf-8");
JsonNode root = jacksonMapper.readTree(json);
String ctnt = root.path("MSG").path("MAIN_MSG").path("outp_msg_ctnt").asText();
assertFalse(ctnt.isEmpty(), "정상응답 JSON의 outp_msg_ctnt가 설정되어야 함");
assertEquals(StandardMessageCoordinatorDJB.NORMAL_OUTP_MSG_CTNT, ctnt,
"정상응답 JSON의 outp_msg_ctnt가 설정되어야 함");
}
// ================================================================