Compare commits

...

8 Commits

Author SHA1 Message Date
curry772 9eea585b52 HttpClientAccessTokenServiceByDB 구현체 내에 tls 연결에 대한 인증서 검증 옵션 적용 2026-09-09 17:09:52 +09:00
curry772 66e51d1755 보안성심의 오픈소스 조치 2026-09-07 16:08:30 +09:00
curry772 5e07100c3d EZDATA 없는데, Ref필드에는 'ERP'로 되어 있는 경우, flat 전문 파싱 오류 처리 2026-09-07 14:39:31 +09:00
curry772 1bf2c78f03 NON_DELIVERY_GROUP 기능 추가 2026-09-07 14:27:20 +09:00
curry772 1941292079 Merge branch 'feature/hsm-config-failover'
- HSM 설정 이중화(PRIMARY/SECONDARY) 및 상태 조회 API 추가
- isNotBlank -> StringUtils.isNotBlank 로 변경

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnybKuxcuPafGhVGh4wqkZ
2026-09-03 09:03:33 +09:00
curry772 abf0de7757 Merge branch 'feature/msg-block-parse-skip'
- JsonReader: 수신 전문에 존재하는 블록을 ref 조건으로 버리지 않도록 수정
- 표준 오류응답 메시지 형식을 PropManager 에서 조회하도록 변경

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnybKuxcuPafGhVGh4wqkZ
2026-09-03 08:58:24 +09:00
curry772 a7fde3f81e 표준 오류응답 메시지 형식을 PropManager 에서 조회하도록 변경
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnybKuxcuPafGhVGh4wqkZ
2026-09-03 08:57:17 +09:00
curry772 bfec1edea2 JsonReader: 수신 전문에 존재하는 블록을 ref 조건으로 버리지 않도록 수정
상대가 오류응답을 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UnybKuxcuPafGhVGh4wqkZ
2026-09-02 17:24:59 +09:00
12 changed files with 262 additions and 96 deletions
+27 -4
View File
@@ -75,7 +75,11 @@ dependencies {
api 'io.micrometer:micrometer-core:1.5.17' api 'io.micrometer:micrometer-core:1.5.17'
api 'io.micrometer:micrometer-registry-prometheus:1.5.17' api 'io.micrometer:micrometer-registry-prometheus:1.5.17'
implementation "com.google.code.gson:gson:2.3.1" // gson 2.3.1 -> 2.8.9 (CVE-2022-25647: 악의적 데이터 역직렬화 시 메모리 고갈/DoS)
// 사용처는 Jsons(new Gson), AlarmService(fromJson), TemplateAdapterErrorMsgHandler(JsonParser),
// alarm/ums/payload/*(@SerializedName) 4곳뿐이고 사용 API 는 그대로 유지된다.
// (new JsonParser().parse() 는 2.8.9 에서 deprecated 이지만 제거되지 않아 동작에 영향 없음)
implementation "com.google.code.gson:gson:2.8.9"
//api "com.eactive:mina-core-1.0.10-custom:1.0:custom@jar" //api "com.eactive:mina-core-1.0.10-custom:1.0:custom@jar"
api ("org.apache.mina:mina-filter-ssl:1.0.10") { api ("org.apache.mina:mina-filter-ssl:1.0.10") {
@@ -91,10 +95,23 @@ dependencies {
api 'com.nimbusds:nimbus-jose-jwt:9.24.3' api 'com.nimbusds:nimbus-jose-jwt:9.24.3'
api 'org.bouncycastle:bcprov-jdk15on:1.70' // BouncyCastle: bcprov-jdk15on 라인은 1.70에서 종료되어 후속 패치가 없다.
// jdk18on 라인으로 전환한다(패키지명·프로바이더명("BC") 동일 → 소스 변경 없음,
// 클래스파일 major 52 / Bundle-RequiredExecutionEnvironment: JavaSE-1.8 이라 JDK 8 유지 가능).
// 사용처: AESCryptoModuleExtension, ARIACryptoModuleExtension
// (BouncyCastleProvider 등록, ARIA 알고리즘, FPE 모드의 FPEParameterSpec)
// 1.70 대비 CVE-2023-33201 / CVE-2024-29857 / CVE-2024-30171 / CVE-2024-30172 해소.
// bcpkix 는 spring-security-jwt 가 전이로 끌고 오던 1.64 를 대체한다(아래 exclude 참조).
api 'org.bouncycastle:bcprov-jdk18on:1.78.1'
api 'org.bouncycastle:bcpkix-jdk18on:1.78.1'
api "io.undertow:undertow-servlet:${undertowVersion}" api "io.undertow:undertow-servlet:${undertowVersion}"
api 'org.java-websocket:Java-WebSocket:1.3.9' // Java-WebSocket 1.3.9 -> 1.5.7 (CVE-2020-11050: 구형 암호화 통신/인증서 검증 결함에 의한 MitM)
// 사용 API 는 WebSocketServer 상속(onOpen/onClose/onMessage/onError/onStart)과
// WebSocket 의 send/close/getRemoteSocketAddress/isOpen, start()/stop(timeout) 뿐이라
// 1.4.0 의 파괴적 변경(WebSocketImpl.DEBUG 제거, Draft_10/17 제거, connections() -> getConnections())
// 에 해당하는 사용처가 없다. 1.5.x 는 로깅을 SLF4J 로 하는데 이미 클래스패스에 있다.
api 'org.java-websocket:Java-WebSocket:1.5.7'
api 'javax.cache:cache-api:1.1.1' api 'javax.cache:cache-api:1.1.1'
api 'org.apache.ignite:ignite-slf4j:2.14.0' api 'org.apache.ignite:ignite-slf4j:2.14.0'
@@ -146,7 +163,13 @@ dependencies {
// XmlMapper/JacksonXml* 사용처가 전 소스에 0건이고, 선언 버전(2.13.1)이 // XmlMapper/JacksonXml* 사용처가 전 소스에 0건이고, 선언 버전(2.13.1)이
// 실제 해석되는 jackson-core/databind(2.12.7)와 마이너 불일치라 승격 시 위험했다. // 실제 해석되는 jackson-core/databind(2.12.7)와 마이너 불일치라 승격 시 위험했다.
api "org.springframework.security:spring-security-jwt:1.1.1.RELEASE" // bcpkix-jdk15on:1.64 -> bcprov-jdk15on:1.64 를 전이로 끌고 온다.
// 위에서 jdk18on 으로 전환했으므로 함께 두면 org.bouncycastle.* 클래스가 중복되고
// 로딩 순서에 따라 구버전이 선택될 수 있다. 전이를 끊고 jdk18on 만 사용한다.
// (이 exclude 는 발행 POM 에도 기록되어 이 모듈을 참조하는 타 사이트에도 동일 적용된다)
api ("org.springframework.security:spring-security-jwt:1.1.1.RELEASE") {
exclude group: 'org.bouncycastle'
}
testRuntimeOnly 'com.h2database:h2:2.1.214' testRuntimeOnly 'com.h2database:h2:2.1.214'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
@@ -108,6 +108,8 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
public static final String AUTH_TOKEN = "AUTH_TOKEN"; public static final String AUTH_TOKEN = "AUTH_TOKEN";
public static final String HTTP_HEADER_SETTING = "HTTP_HEADER_SETTING"; public static final String HTTP_HEADER_SETTING = "HTTP_HEADER_SETTING";
public static final String NON_DELIVERY_GROUP = "NON_DELIVERY_GROUP";
public static final String PAYLOAD_PARAM_NAME_CLIENT_ID = "client_id"; // jwhong public static final String PAYLOAD_PARAM_NAME_CLIENT_ID = "client_id"; // jwhong
static final String DJB_ROOTLESS_ARRAY = "{ \"DJB_ROOTLESS_ARRAY\" : "; static final String DJB_ROOTLESS_ARRAY = "{ \"DJB_ROOTLESS_ARRAY\" : ";
@@ -135,6 +137,12 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
String bizCode = ""; String bizCode = "";
String headerGroupName = prop.getProperty(HEADER_GROUP); String headerGroupName = prop.getProperty(HEADER_GROUP);
String relayResponseHeaderKeys = prop.getProperty(HEADER_KEYS, ""); String relayResponseHeaderKeys = prop.getProperty(HEADER_KEYS, "");
// 전송 대상에서 제외할 그룹명(콤마로 다건 지정 가능)
String[] nonDeliveryGroups = StringUtils.split(
StringUtils.trimToEmpty(prop.getProperty(NON_DELIVERY_GROUP)), ",");
if (nonDeliveryGroups == null) nonDeliveryGroups = new String[0];
useAdapterToken = StringUtils.equalsIgnoreCase(prop.getProperty("ADAPTER_TOKEN_USE_YN", "N"), "Y"); useAdapterToken = StringUtils.equalsIgnoreCase(prop.getProperty("ADAPTER_TOKEN_USE_YN", "N"), "Y");
String authorizationHeaderName = prop.getProperty("ADAPTER_TOKEN_HEADER_NAME", "Authorization"); String authorizationHeaderName = prop.getProperty("ADAPTER_TOKEN_HEADER_NAME", "Authorization");
@@ -253,23 +261,38 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
} }
} }
if(dataObject instanceof ObjectNode) { if (dataObject instanceof ObjectNode) {
if (dataObject != null && StringUtils.isNotBlank(headerGroupName)) { ObjectNode node = (ObjectNode) dataObject;
httpHeader = ((ObjectNode)dataObject).get(headerGroupName); if (StringUtils.isNotBlank(headerGroupName)) {
((ObjectNode)dataObject).remove(headerGroupName); httpHeader = node.get(headerGroupName);
node.remove(headerGroupName);
} }
} else if(dataObject instanceof JSONObject) { for (String nonDeliveryGroup : nonDeliveryGroups) {
if (dataObject != null && StringUtils.isNotBlank(headerGroupName)) { if (StringUtils.isNotBlank(nonDeliveryGroup)) node.remove(nonDeliveryGroup.trim());
httpHeader = ((JSONObject)dataObject).get(headerGroupName); }
((JSONObject)dataObject).remove(headerGroupName); } else if (dataObject instanceof JSONObject) {
JSONObject json = (JSONObject) dataObject;
if (StringUtils.isNotBlank(headerGroupName)) {
httpHeader = json.get(headerGroupName);
json.remove(headerGroupName);
}
for (String nonDeliveryGroup : nonDeliveryGroups) {
if (StringUtils.isNotBlank(nonDeliveryGroup)) json.remove(nonDeliveryGroup.trim());
} }
} else if (dataObject instanceof Document) { } else if (dataObject instanceof Document) {
Document doc = (Document)dataObject; Element root = ((Document) dataObject).getRootElement();
if (doc != null && StringUtils.isNotBlank(headerGroupName)) { if (root != null) {
Element root = doc.getRootElement(); if (StringUtils.isNotBlank(headerGroupName)) {
Element httpHeaderElement = root.element(headerGroupName); Element httpHeaderElement = root.element(headerGroupName);
root.remove(httpHeaderElement); if (httpHeaderElement != null) root.remove(httpHeaderElement);
httpHeader = httpHeaderElement; httpHeader = httpHeaderElement;
}
// element(null) 은 dom4j 내부에서 NPE 가 발생하므로 반드시 blank 검사 후 조회한다
for (String nonDeliveryGroup : nonDeliveryGroups) {
if (StringUtils.isBlank(nonDeliveryGroup)) continue;
Element nonDeliveryGroupElement = root.element(nonDeliveryGroup.trim());
if (nonDeliveryGroupElement != null) root.remove(nonDeliveryGroupElement);
}
} }
} }
@@ -173,7 +173,12 @@ public class HttpClientAccessTokenServiceWithBase64Header implements HttpClientA
cmBuilder.setSSLSocketFactory(sslSocketFactory); cmBuilder.setSSLSocketFactory(sslSocketFactory);
} else { } else {
sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build(); sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build();
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext); SSLConnectionSocketFactory csf = null;
if (testMode) {
csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
} else {
csf = new SSLConnectionSocketFactory(sslContext);
}
cmBuilder.setSSLSocketFactory(csf); cmBuilder.setSSLSocketFactory(csf);
} }
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException
@@ -181,7 +181,12 @@ public class HttpClientAccessTokenServiceWithBase64NiceOn implements HttpClientA
cmBuilder.setSSLSocketFactory(sslSocketFactory); cmBuilder.setSSLSocketFactory(sslSocketFactory);
} else { } else {
sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build(); sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build();
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext); SSLConnectionSocketFactory csf = null;
if (testMode) {
csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
} else {
csf = new SSLConnectionSocketFactory(sslContext);
}
cmBuilder.setSSLSocketFactory(csf); cmBuilder.setSSLSocketFactory(csf);
} }
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException
@@ -180,7 +180,12 @@ public class HttpClientAccessTokenServiceWithDefault implements HttpClientAccess
cmBuilder.setSSLSocketFactory(sslSocketFactory); cmBuilder.setSSLSocketFactory(sslSocketFactory);
} else { } else {
sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build(); sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build();
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext); SSLConnectionSocketFactory csf = null;
if (testMode) {
csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
} else {
csf = new SSLConnectionSocketFactory(sslContext);
}
cmBuilder.setSSLSocketFactory(csf); cmBuilder.setSSLSocketFactory(csf);
} }
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException
@@ -179,7 +179,12 @@ public class HttpClientAccessTokenServiceWithParam implements HttpClientAccessTo
cmBuilder.setSSLSocketFactory(sslSocketFactory); cmBuilder.setSSLSocketFactory(sslSocketFactory);
} else { } else {
sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build(); sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build();
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext); SSLConnectionSocketFactory csf = null;
if (testMode) {
csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
} else {
csf = new SSLConnectionSocketFactory(sslContext);
}
cmBuilder.setSSLSocketFactory(csf); cmBuilder.setSSLSocketFactory(csf);
} }
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException
@@ -196,7 +196,12 @@ public class HttpClientAccessTokenServiceWithParamAddBody implements HttpClientA
cmBuilder.setSSLSocketFactory(sslSocketFactory); cmBuilder.setSSLSocketFactory(sslSocketFactory);
} else { } else {
sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build(); sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build();
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext); SSLConnectionSocketFactory csf = null;
if (testMode) {
csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
} else {
csf = new SSLConnectionSocketFactory(sslContext);
}
cmBuilder.setSSLSocketFactory(csf); cmBuilder.setSSLSocketFactory(csf);
} }
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException
@@ -295,6 +295,58 @@ public class StandardItem implements Serializable, Cloneable {
this.isHidden = isHidden; this.isHidden = isHidden;
} }
/**
* 조건부 블록의 refValue 와 실제 필드값을 비교한다.
* FlatReader / StandardMessageCoordinator 와 동일한 기준을 쓰기 위한 공용 판정 로직이다.
* - '!' 접두사 : NOT 조건 (예: !S → S가 아닌 경우)
* - '|' 구분자 : OR 조건 (예: NM|EM → NM 또는 EM인 경우)
* - '!' 와 '|' 조합 가능 (예: !NM|EM → NM도 EM도 아닌 경우)
*/
public static boolean matchRefCondition(String refValue, String actualValue) {
if(refValue == null) return false;
String expect = refValue;
boolean negate = expect.startsWith("!");
if(negate) {
expect = expect.substring(1);
}
String actual = (actualValue == null) ? null : actualValue.trim();
boolean matched = false;
String[] expectValues = expect.split("\\|");
for(int i=0; i<expectValues.length; i++) {
if(expectValues[i].equals(actual)) {
matched = true;
break;
}
}
return negate ? !matched : matched;
}
/**
* FLAT 직렬화 시 이 GROUP 블록의 바이트를 출력할지 판단한다.
* FlatReader.traverse() 의 GROUP 처리와 같은 기준을 쓰기 위한 것으로,
* ref 조건이 성립하면 size==0(전문에 블록이 없어 미활성) 이어도 레이아웃 기본값으로 출력한다.
* 조건이 성립하는데 출력하지 않으면, 파서는 블록이 있다고 보고 읽어 오프셋이 어긋난다.
*
* @param root 조건 평가 기준이 되는 최상위 메시지. null 이면 조건 평가 없이 기존 규칙만 적용
* @param parentActivated 조건 성립으로 활성화된 상위 GROUP 하위인지 여부
*/
protected boolean isFlatGroupActive(StandardMessage root, boolean parentActivated) {
if(isHidden) return false;
if(getSize() > 0) return true;
if(root != null
&& StringUtils.isNotBlank(getRefPath())
&& StringUtils.isNotBlank(getRefValue())
&& matchRefCondition(getRefValue(), root.findItemValue(getRefPath())) ) {
if(logger.isDebugEnabled()) {
logger.debug("@GROUP name={} ACTIVATED by ref condition({}=[{}]). size=0 이지만 FLAT 출력 대상"
, getName(), getRefPath(), getRefValue());
}
return true;
}
// 조건 성립으로 활성화된 상위 GROUP 하위의 무조건 블록은 함께 출력한다.
return parentActivated;
}
protected String toTypeValue(String svalue) { protected String toTypeValue(String svalue) {
if(svalue == null) return svalue; if(svalue == null) return svalue;
@@ -1007,6 +1059,15 @@ public class StandardItem implements Serializable, Cloneable {
} }
public int getBytesDataLength(String charset) { public int getBytesDataLength(String charset) {
return getBytesDataLength(charset, null, false);
}
/**
* @param root 조건부 블록(refPath/refValue) 평가 기준이 되는 최상위 메시지
* @param parentActivated 조건 성립으로 활성화된 상위 GROUP 하위인지 여부
* @see #isFlatGroupActive(StandardMessage, boolean)
*/
public int getBytesDataLength(String charset, StandardMessage root, boolean parentActivated) {
int totalSize = 0; int totalSize = 0;
Iterator<String> keyIter = null; Iterator<String> keyIter = null;
try { try {
@@ -1016,23 +1077,25 @@ public class StandardItem implements Serializable, Cloneable {
while(keyIter.hasNext()) { while(keyIter.hasNext()) {
String key = keyIter.next(); String key = keyIter.next();
StandardItem item = childs.get(key); StandardItem item = childs.get(key);
totalSize +=item.getBytesDataLength(charset); totalSize +=item.getBytesDataLength(charset, root, false);
} }
break; break;
case StandardType.FIELD : case StandardType.FIELD :
totalSize += getLength(); totalSize += getLength();
break; break;
case StandardType.GROUP : case StandardType.GROUP :
// skip inactive/conditional group (size==0 means not activated) // ref 조건이 성립하면 size==0 이어도 출력한다(FlatReader 와 동일 기준)
if( getSize() == 0 ) { if( !isFlatGroupActive(root, parentActivated) ) {
break; break;
} }
if (isHidden) break; // 출력하는 GROUP 의 하위 무조건 블록은 함께 출력한다.
// FlatReader 는 이 블록을 조건 없이 읽으므로 빼면 오프셋이 어긋난다.
boolean childActivated = true;
keyIter = childs.keySet().iterator(); keyIter = childs.keySet().iterator();
while(keyIter.hasNext()) { while(keyIter.hasNext()) {
String key = keyIter.next(); String key = keyIter.next();
StandardItem item = childs.get(key); StandardItem item = childs.get(key);
totalSize +=item.getBytesDataLength(charset); totalSize +=item.getBytesDataLength(charset, root, childActivated);
} }
break; break;
case StandardType.GRID : case StandardType.GRID :
@@ -1050,7 +1113,7 @@ public class StandardItem implements Serializable, Cloneable {
while(keyIter.hasNext()) { while(keyIter.hasNext()) {
String key = keyIter.next(); String key = keyIter.next();
StandardItem item = group.get(key); StandardItem item = group.get(key);
totalSize +=item.getBytesDataLength(charset); totalSize +=item.getBytesDataLength(charset, root, false);
} }
} }
break; break;
@@ -1086,6 +1149,15 @@ public class StandardItem implements Serializable, Cloneable {
return toByteArray(true, charset); return toByteArray(true, charset);
} }
public byte[] toByteArray(boolean withBizData, String charset) { public byte[] toByteArray(boolean withBizData, String charset) {
return toByteArray(withBizData, charset, null, false);
}
/**
* @param root 조건부 블록(refPath/refValue) 평가 기준이 되는 최상위 메시지
* @param parentActivated 조건 성립으로 활성화된 상위 GROUP 하위인지 여부
* @see #isFlatGroupActive(StandardMessage, boolean)
*/
public byte[] toByteArray(boolean withBizData, String charset, StandardMessage root, boolean parentActivated) {
ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteArrayOutputStream bos = new ByteArrayOutputStream();
Iterator<String> keyIter = null; Iterator<String> keyIter = null;
// logger.debug("Encoding : {}", charset); // logger.debug("Encoding : {}", charset);
@@ -1096,7 +1168,7 @@ public class StandardItem implements Serializable, Cloneable {
while(keyIter.hasNext()) { while(keyIter.hasNext()) {
String key = keyIter.next(); String key = keyIter.next();
StandardItem item = childs.get(key); StandardItem item = childs.get(key);
bos.write(item.toByteArray(withBizData,charset)); //하위로 내려갈때 withBizDat 값이 없어져서 추가 jun's 20231101 bos.write(item.toByteArray(withBizData, charset, root, false)); //하위로 내려갈때 withBizDat 값이 없어져서 추가 jun's 20231101
} }
break; break;
case StandardType.FIELD : case StandardType.FIELD :
@@ -1118,16 +1190,18 @@ public class StandardItem implements Serializable, Cloneable {
} }
break; break;
case StandardType.GROUP : case StandardType.GROUP :
// skip inactive/conditional group (size==0 means not activated) // ref 조건이 성립하면 size==0 이어도 출력한다(FlatReader 와 동일 기준)
if( getSize() == 0 ) { if( !isFlatGroupActive(root, parentActivated) ) {
break; break;
} }
if (isHidden) break; // 출력하는 GROUP 의 하위 무조건 블록은 함께 출력한다.
// FlatReader 는 이 블록을 조건 없이 읽으므로 빼면 오프셋이 어긋난다.
boolean childActivated = true;
keyIter = childs.keySet().iterator(); keyIter = childs.keySet().iterator();
while(keyIter.hasNext()) { while(keyIter.hasNext()) {
String key = keyIter.next(); String key = keyIter.next();
StandardItem item = childs.get(key); StandardItem item = childs.get(key);
bos.write(item.toByteArray(withBizData,charset)); //하위로 내려갈때 withBizDat 값이 없어져서 추가 jun's 20231101 bos.write(item.toByteArray(withBizData, charset, root, childActivated)); //하위로 내려갈때 withBizDat 값이 없어져서 추가 jun's 20231101
} }
break; break;
case StandardType.GRID : case StandardType.GRID :
@@ -1145,7 +1219,7 @@ public class StandardItem implements Serializable, Cloneable {
while(keyIter.hasNext()) { while(keyIter.hasNext()) {
String key = keyIter.next(); String key = keyIter.next();
StandardItem item = group.get(key); StandardItem item = group.get(key);
bos.write(item.toByteArray(withBizData, charset)); //하위로 내려갈때 withBizDat 값이 없어져서 추가 jun's 20231101 bos.write(item.toByteArray(withBizData, charset, root, false)); //하위로 내려갈때 withBizDat 값이 없어져서 추가 jun's 20231101
} }
} }
break; break;
@@ -461,7 +461,8 @@ public class StandardMessage extends StandardItem {
while(keyIter.hasNext()) { while(keyIter.hasNext()) {
String key = keyIter.next(); String key = keyIter.next();
StandardItem item = childs.get(key); StandardItem item = childs.get(key);
bos.write(item.toByteArray(withBizData, charset)); // this 를 root 로 넘겨 조건부 블록(refPath/refValue)을 평가하게 한다
bos.write(item.toByteArray(withBizData, charset, this, false));
} }
return bos.toByteArray(); return bos.toByteArray();
} catch(Exception e) { } catch(Exception e) {
@@ -490,7 +491,7 @@ public class StandardMessage extends StandardItem {
while(keyIter.hasNext()) { while(keyIter.hasNext()) {
String key = keyIter.next(); String key = keyIter.next();
StandardItem item = childs.get(key); StandardItem item = childs.get(key);
totalSize +=item.getBytesDataLength(charset); totalSize +=item.getBytesDataLength(charset, this, false);
} }
return totalSize; return totalSize;
} catch(Exception e) { } catch(Exception e) {
@@ -516,7 +517,7 @@ public class StandardMessage extends StandardItem {
while (keyIter.hasNext()) { while (keyIter.hasNext()) {
String key = keyIter.next(); String key = keyIter.next();
StandardItem item = find.childs.get(key); StandardItem item = find.childs.get(key);
totalSize += item.getBytesDataLength(this.bizDataCharset); totalSize += item.getBytesDataLength(this.bizDataCharset, this, false);
} }
return totalSize; return totalSize;
} catch (Exception e) { } catch (Exception e) {
@@ -121,24 +121,15 @@ public class FlatReader implements StandardReader {
logger.debug("@GROUP name={}, getRefPath={}, refItemValue=[{}], getRefValue=[{}]" logger.debug("@GROUP name={}, getRefPath={}, refItemValue=[{}], getRefValue=[{}]"
, currentItem.getName(), currentItem.getRefPath(), refItemValue, currentItem.getRefValue()); , currentItem.getName(), currentItem.getRefPath(), refItemValue, currentItem.getRefValue());
if(refItemValue != null) refItemValue = refItemValue.trim();
/* /*
* 값의 경우의수 NM,EM,""(blank) * 값의 경우의수 NM,EM,""(blank)
* ! : NOT 조건 (예: !EM → EM이 아닌 경우) * ! : NOT 조건 (예: !EM → EM이 아닌 경우)
* | : OR 조건 (예: NM|EM → NM 또는 EM인 경우) * | : OR 조건 (예: NM|EM → NM 또는 EM인 경우)
* !와 | 조합 가능 (예: !NM|EM → NM도 아니고 EM도 아닌 경우) * !와 | 조합 가능 (예: !NM|EM → NM도 아니고 EM도 아닌 경우)
* 판정 기준은 StandardItem.matchRefCondition 하나로 통일한다.
* (FLAT 직렬화 StandardItem.isFlatGroupActive 와 같은 기준이어야 오프셋이 어긋나지 않음)
*/ */
String refValue = currentItem.getRefValue(); boolean matched = StandardItem.matchRefCondition(currentItem.getRefValue(), refItemValue);
boolean isNot = refValue.startsWith("!");
if (isNot) {
refValue = refValue.substring(1);
}
boolean matched = matchRefValue(refValue, refItemValue);
if (isNot) {
matched = !matched;
}
if (!matched) { if (!matched) {
logger.warn("@GROUP name={} SKIPPED.", currentItem.getName()); logger.warn("@GROUP name={} SKIPPED.", currentItem.getName());
@@ -307,21 +298,4 @@ public class FlatReader implements StandardReader {
} }
return refItemValue; return refItemValue;
} }
/**
* refValue와 refItemValue를 비교하여 일치 여부를 반환
* | 구분자로 OR 조건 지원 (예: "NM|EM" → "NM" 또는 "EM"과 일치하면 true)
*/
private boolean matchRefValue(String refValue, String refItemValue) {
if (refValue.contains("|")) {
String[] values = refValue.split("\\|");
for (String val : values) {
if (val.equals(refItemValue)) {
return true;
}
}
return false;
}
return refValue.equals(refItemValue);
}
} }
@@ -119,6 +119,19 @@ public class JsonReader implements StandardReader {
switch(currentNode.getNodeType()) { switch(currentNode.getNodeType()) {
case OBJECT: case OBJECT:
// 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 if( currentItem != null && currentItem.getSize() == 0
&& StringUtils.isNotBlank(currentItem.getRefPath()) && StringUtils.isNotBlank(currentItem.getRefPath())
&& StringUtils.isNotBlank(currentItem.getRefValue()) ) { && StringUtils.isNotBlank(currentItem.getRefValue()) ) {
@@ -131,25 +144,20 @@ public class JsonReader implements StandardReader {
if (refItemValue != null) refItemValue = refItemValue.trim(); if (refItemValue != null) refItemValue = refItemValue.trim();
String refValue = currentItem.getRefValue(); String refValue = currentItem.getRefValue();
if (refValue.startsWith("!")) { // 느낌표가 들어가면 다음에 나오는게 아닌경우 boolean negate = refValue.startsWith("!"); // 느낌표가 들어가면 다음에 나오는게 아닌경우
if (negate) {
refValue = refValue.substring(1); 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<String> fieldNames = currentNode.fieldNames(); Iterator<String> fieldNames = currentNode.fieldNames();
while(fieldNames.hasNext()) { while(fieldNames.hasNext()) {
@@ -20,6 +20,7 @@ import com.eactive.eai.common.header.HeaderActionKeys;
import com.eactive.eai.common.message.EAIMessage; import com.eactive.eai.common.message.EAIMessage;
import com.eactive.eai.common.message.EAIMessageKeys; import com.eactive.eai.common.message.EAIMessageKeys;
import com.eactive.eai.common.message.MessageType; 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.routing.Process;
import com.eactive.eai.common.server.EAIServerManager; import com.eactive.eai.common.server.EAIServerManager;
import com.eactive.eai.common.submessage.SubMessageManager; 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 logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
protected static Logger esbLogger = Logger.getLogger(Logger.LOGGER_ESBFW); 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 String guidLogPrefix = this.getClass().getSimpleName() + "] ";
protected static ThreadLocal local = new ThreadLocal(); // 경과시간 동기화를 위한 ThreadLocal protected static ThreadLocal local = new ThreadLocal(); // 경과시간 동기화를 위한 ThreadLocal
@@ -689,7 +696,7 @@ public abstract class DefaultProcess extends Process {
String errorCode = mapper.getErrorCode(resStandardMessage); String errorCode = mapper.getErrorCode(resStandardMessage);
String errorMsg = StringUtils.trim(mapper.getErrorMsg(resStandardMessage)); String errorMsg = StringUtils.trim(mapper.getErrorMsg(resStandardMessage));
String errorDesc = StringUtils.trim(resStandardMessage.findItemValue("MSG.MAIN_MSG.outp_msg_desc")); 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; return;
} else { } else {
this.resEaiMsg.setRspErr("RECEAIINA001", "비표준 오류 응답 수신"); this.resEaiMsg.setRspErr("RECEAIINA001", "비표준 오류 응답 수신");
@@ -707,7 +714,7 @@ public abstract class DefaultProcess extends Process {
String errorCode = mapper.getErrorCode(resStandardMessage); String errorCode = mapper.getErrorCode(resStandardMessage);
String errorMsg = StringUtils.trim(mapper.getErrorMsg(resStandardMessage)); String errorMsg = StringUtils.trim(mapper.getErrorMsg(resStandardMessage));
String errorDesc = StringUtils.trim(resStandardMessage.findItemValue("MSG.MAIN_MSG.outp_msg_desc")); 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 { } else {
this.resEaiMsg.setRspErr("RECEAIINA001", "비표준 오류 응답 수신"); 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() { public boolean isTgtTranCall() {
return this.isTgtTran; return this.isTgtTran;
} }