From bfec1edea28b92b0ba1ee445b7bebf93d809eec8 Mon Sep 17 00:00:00 2001 From: curry772 Date: Wed, 2 Sep 2026 17:24:59 +0900 Subject: [PATCH 1/2] =?UTF-8?q?JsonReader:=20=EC=88=98=EC=8B=A0=20?= =?UTF-8?q?=EC=A0=84=EB=AC=B8=EC=97=90=20=EC=A1=B4=EC=9E=AC=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EB=B8=94=EB=A1=9D=EC=9D=84=20ref=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=EC=9C=BC=EB=A1=9C=20=EB=B2=84=EB=A6=AC=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 상대가 오류응답을 procs_rslt_dvcd=F 로 보내면서 요청응답구분 dman_rspn_dvcd 는 요청값 S 를 그대로 에코백했다(표준은 응답 시 R). 레이아웃의 MSG 블록 조건이 !S 라 MSG 이하가 통째로 버려졌고, 그 결과 - MAIN_MSG 가 레이아웃 기본값으로 남아 오류가 "정상처리되었습니다." 로 보고되고 - 거래로그는 StandardMessage 재직렬화본(EAILogDAO)이라 MSG 부 이후가 통째로 누락됐다 refPath/refValue 는 "이 블록이 전문에 들어있는가" 를 판단하는 조건인데, 고정길이와 달리 JSON 은 키의 존재 자체가 답이다. OBJECT 분기에 도달했다는 것은 수신 전문에 해당 블록이 실제로 들어있다는 뜻이므로 조건으로 다시 거르지 않고, 불일치는 상대 헤더의 오류로 보고 경고만 남긴 뒤 파싱을 계속한다. 기존에 한쪽 분기의 스킵 경고가 isDebugEnabled 가드에 가려 운영 로그에 남지 않던 문제도 함께 해소된다. FlatReader 는 조건이 블록 존재 판단에 반드시 필요하므로 그대로 둔다. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UnybKuxcuPafGhVGh4wqkZ --- .../eai/message/parser/JsonReader.java | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/eactive/eai/message/parser/JsonReader.java b/src/main/java/com/eactive/eai/message/parser/JsonReader.java index a75e2a6..a8dfc47 100644 --- a/src/main/java/com/eactive/eai/message/parser/JsonReader.java +++ b/src/main/java/com/eactive/eai/message/parser/JsonReader.java @@ -119,8 +119,21 @@ public class JsonReader implements StandardReader { switch(currentNode.getNodeType()) { case OBJECT: - if( currentItem != null && currentItem.getSize() == 0 - && StringUtils.isNotBlank(currentItem.getRefPath()) + // refPath/refValue 는 "이 블록이 전문에 들어있는가" 를 판단하기 위한 조건이다. + // 고정길이(FLAT)는 블록의 존재를 알려주는 키가 없어 이 조건이 유일한 근거지만, + // JSON 은 키의 존재 자체가 답이다. 여기에 도달했다는 것은 수신 전문에 이 블록이 + // 실제로 들어있다는 뜻이므로, 조건으로 다시 걸러내면 받은 데이터를 잃는다. + // + // 실제 사고(2026-09): 상대가 오류응답을 procs_rslt_dvcd=F 로 보내면서 요청응답구분 + // dman_rspn_dvcd 는 요청값 S 를 그대로 에코백했다(표준은 응답 시 R). MSG 블록의 + // 조건이 !S 라 MSG 이하가 통째로 버려졌고, 그 결과 + // - MAIN_MSG 가 레이아웃 기본값으로 남아 오류가 "정상처리되었습니다." 로 보고되고 + // - 거래로그는 StandardMessage 재직렬화본이라 MSG 부 이후가 통째로 누락됐다. + // + // 조건 불일치는 상대 헤더의 오류로 보고, 경고만 남기고 파싱은 계속한다. + // (FlatReader 는 조건이 존재 판단에 반드시 필요하므로 그대로 둔다.) + if( currentItem != null && currentItem.getSize() == 0 + && StringUtils.isNotBlank(currentItem.getRefPath()) && StringUtils.isNotBlank(currentItem.getRefValue()) ) { String refItemValue = itemMap.get(currentItem.getRefPath()); @@ -131,25 +144,20 @@ public class JsonReader implements StandardReader { if (refItemValue != null) refItemValue = refItemValue.trim(); String refValue = currentItem.getRefValue(); - if (refValue.startsWith("!")) { // 느낌표가 들어가면 다음에 나오는게 아닌경우 + boolean negate = refValue.startsWith("!"); // 느낌표가 들어가면 다음에 나오는게 아닌경우 + if (negate) { refValue = refValue.substring(1); - if (refValue.equals(refItemValue)) { - logger.warn("@GROUP name={} SKIPPED.", currentItem.getName()); - currentItem.setHidden(true); - break; - } else { - currentItem.setHidden(false); - } - } else { - if (!currentItem.getRefValue().equals(refItemValue)) { - if (logger.isDebugEnabled()) - logger.warn("@GROUP name={} SKIPPED.", currentItem.getName()); - currentItem.setHidden(true); - break; - } else { - currentItem.setHidden(false); - } } + boolean matched = refValue.equals(refItemValue); + if (negate) { + matched = !matched; + } + if (!matched) { + logger.warn("@GROUP name={} 조건 불일치({}=[{}], 기대=[{}]). 수신 전문에 존재하므로 파싱은 계속한다.", + currentItem.getName(), currentItem.getRefPath(), refItemValue, + currentItem.getRefValue()); + } + currentItem.setHidden(false); } Iterator fieldNames = currentNode.fieldNames(); while(fieldNames.hasNext()) { From a7fde3f81e263e68f0622d28b18c1a61c6ec487a Mon Sep 17 00:00:00 2001 From: curry772 Date: Thu, 3 Sep 2026 08:57:17 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=ED=91=9C=EC=A4=80=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EC=9D=84=20PropManager=20=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DefaultProcess.setResMsg() 두 곳(표준/준표준 오류응답)에 하드코딩돼 있던 String.format("[%s] %s (%s)", ...) 를 프로퍼티 기반으로 바꾼다. - 그룹/키: DefaultProcess / std.error.msg.format - 인자 순서: 1=오류코드, 2=오류메시지, 3=오류상세 - 기본형식: "[%s] %s" 프로퍼티는 에이전트의 ReloadPropertyCommand 로 무중단 갱신될 수 있으므로 캐시하지 않고 호출할 때마다 조회한다. 방어 두 가지를 함께 넣는다. - 그룹/키가 없거나 값이 공백이면 기본형식을 쓴다. 설정을 넣지 않아도 동작한다. - 형식이 잘못되면(%s 개수 불일치 등) String.format 이 IllegalFormatException 을 던지는데, 하필 오류응답 경로라 여기서 예외가 나면 오류 자체를 못 내려보낸다. 조립 실패 시 경고만 남기고 기본형식으로 되돌린다. 특정 사이트 전용이 아닌 공통 기능이다. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UnybKuxcuPafGhVGh4wqkZ --- .../eactive/eai/outbound/DefaultProcess.java | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/eactive/eai/outbound/DefaultProcess.java b/src/main/java/com/eactive/eai/outbound/DefaultProcess.java index fae5bdd..ce2ce49 100644 --- a/src/main/java/com/eactive/eai/outbound/DefaultProcess.java +++ b/src/main/java/com/eactive/eai/outbound/DefaultProcess.java @@ -20,6 +20,7 @@ import com.eactive.eai.common.header.HeaderActionKeys; import com.eactive.eai.common.message.EAIMessage; import com.eactive.eai.common.message.EAIMessageKeys; import com.eactive.eai.common.message.MessageType; +import com.eactive.eai.common.property.PropManager; import com.eactive.eai.common.routing.Process; import com.eactive.eai.common.server.EAIServerManager; import com.eactive.eai.common.submessage.SubMessageManager; @@ -43,6 +44,12 @@ public abstract class DefaultProcess extends Process { protected static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); protected static Logger esbLogger = Logger.getLogger(Logger.LOGGER_ESBFW); + /** 표준 오류응답 메시지 형식을 조회할 프로퍼티 그룹/키. 없으면 기본형식을 쓴다. */ + public static final String PROP_GROUP = "DefaultProcess"; + public static final String PROP_STD_ERR_MSG_FORMAT = "std.error.msg.format"; + /** 인자 순서: 1=오류코드, 2=오류메시지, 3=오류상세 */ + public static final String DEFAULT_STD_ERR_MSG_FORMAT = "[%s] %s"; + protected String guidLogPrefix = this.getClass().getSimpleName() + "] "; protected static ThreadLocal local = new ThreadLocal(); // 경과시간 동기화를 위한 ThreadLocal @@ -689,7 +696,7 @@ public abstract class DefaultProcess extends Process { String errorCode = mapper.getErrorCode(resStandardMessage); String errorMsg = StringUtils.trim(mapper.getErrorMsg(resStandardMessage)); String errorDesc = StringUtils.trim(resStandardMessage.findItemValue("MSG.MAIN_MSG.outp_msg_desc")); - this.resEaiMsg.setRspErr("RECEAIINA001", String.format("[%s] %s (%s)", errorCode, errorMsg, errorDesc)); + this.resEaiMsg.setRspErr("RECEAIINA001", formatStdErrorMessage(errorCode, errorMsg, errorDesc)); return; } else { this.resEaiMsg.setRspErr("RECEAIINA001", "비표준 오류 응답 수신"); @@ -707,7 +714,7 @@ public abstract class DefaultProcess extends Process { String errorCode = mapper.getErrorCode(resStandardMessage); String errorMsg = StringUtils.trim(mapper.getErrorMsg(resStandardMessage)); String errorDesc = StringUtils.trim(resStandardMessage.findItemValue("MSG.MAIN_MSG.outp_msg_desc")); - this.resEaiMsg.setRspErr("RECEAIINA001", String.format("[%s] %s (%s)", errorCode, errorMsg, errorDesc)); + this.resEaiMsg.setRspErr("RECEAIINA001", formatStdErrorMessage(errorCode, errorMsg, errorDesc)); } else { this.resEaiMsg.setRspErr("RECEAIINA001", "비표준 오류 응답 수신"); } @@ -720,6 +727,37 @@ public abstract class DefaultProcess extends Process { } } + /** + * 표준 오류응답 메시지를 조립한다. + * + * 형식은 PropManager 의 {@code DefaultProcess / std.error.msg.format} 에서 가져온다. + * 프로퍼티는 에이전트의 ReloadPropertyCommand 로 무중단 갱신될 수 있으므로, + * 캐시하지 않고 호출할 때마다 조회한다. 그룹이나 키가 없으면 + * {@link #DEFAULT_STD_ERR_MSG_FORMAT} 를 쓴다. + * + * 형식 문자열은 운영에서 편집 가능하므로 잘못된 형식(예: %s 개수 불일치)이 들어올 수 있다. + * 이 메서드는 오류응답 경로에서 호출되므로 여기서 예외가 나면 오류 자체를 못 내려보낸다. + * 따라서 조립에 실패하면 경고만 남기고 기본형식으로 되돌린다. + * + * @param errorCode 오류코드 (형식 인자 1) + * @param errorMsg 오류메시지 (형식 인자 2) + * @param errorDesc 오류상세 (형식 인자 3) + */ + protected String formatStdErrorMessage(String errorCode, String errorMsg, String errorDesc) { + String format = PropManager.getInstance().getProperty( + PROP_GROUP, PROP_STD_ERR_MSG_FORMAT, DEFAULT_STD_ERR_MSG_FORMAT); + if (StringUtils.isBlank(format)) { + format = DEFAULT_STD_ERR_MSG_FORMAT; + } + try { + return String.format(format, errorCode, errorMsg, errorDesc); + } catch (Exception e) { + logger.warn(guidLogPrefix + " 표준 오류응답 형식이 잘못되어 기본형식으로 대체한다. [" + + PROP_GROUP + "/" + PROP_STD_ERR_MSG_FORMAT + "=" + format + "] - " + e.getMessage()); + return String.format(DEFAULT_STD_ERR_MSG_FORMAT, errorCode, errorMsg, errorDesc); + } + } + public boolean isTgtTranCall() { return this.isTgtTran; }