diff --git a/build.gradle b/build.gradle index 9cfa9cc..7dfd3df 100644 --- a/build.gradle +++ b/build.gradle @@ -141,8 +141,10 @@ dependencies { compileOnly group: 'javax.servlet.jsp', name: 'javax.servlet.jsp-api', version: '2.3.3' compileOnly 'javax.resource:javax.resource-api:1.7' compileOnly 'javax.jms:javax.jms-api:2.0.1' - - compileOnly group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1' + + // jackson-dataformat-xml 제거 (2026-08-27) + // XmlMapper/JacksonXml* 사용처가 전 소스에 0건이고, 선언 버전(2.13.1)이 + // 실제 해석되는 jackson-core/databind(2.12.7)와 마이너 불일치라 승격 시 위험했다. api "org.springframework.security:spring-security-jwt:1.1.1.RELEASE" diff --git a/src/main/java/com/eactive/eai/adapter/handler/TemplateCodeConvertAdapterErrorMsgHandler.java b/src/main/java/com/eactive/eai/adapter/handler/TemplateCodeConvertAdapterErrorMsgHandler.java index fb33f04..43a32c5 100644 --- a/src/main/java/com/eactive/eai/adapter/handler/TemplateCodeConvertAdapterErrorMsgHandler.java +++ b/src/main/java/com/eactive/eai/adapter/handler/TemplateCodeConvertAdapterErrorMsgHandler.java @@ -6,6 +6,7 @@ import org.apache.commons.lang3.StringUtils; import com.eactive.eai.common.message.EAIMessage; import com.eactive.eai.common.property.PropManager; +import com.eactive.eai.common.util.JacksonUtil; import com.eactive.eai.common.util.Logger; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -19,7 +20,8 @@ public class TemplateCodeConvertAdapterErrorMsgHandler extends TemplateAdapterEr /** PropManager 에서 코드 변환 설정을 조회할 프로퍼티 그룹 이름 */ static final String PROP_GROUP = "AdapterErrorMessageHandler{CODE_CONVERT}"; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + // 기본 ObjectMapper 는 응답 JSON 왕복에서 100000000.00 을 1.0E8 로 바꿔버린다. + private static final ObjectMapper OBJECT_MAPPER = JacksonUtil.newNumberSafeMapper(); @Override public Object generateNonStandardErrorResponseMessage( diff --git a/src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceRestAddFilter.java b/src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceRestAddFilter.java index d9979c6..93c616a 100644 --- a/src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceRestAddFilter.java +++ b/src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceRestAddFilter.java @@ -9,6 +9,7 @@ import org.json.simple.JSONObject; import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey; import com.eactive.eai.adapter.http.client.impl.filter.HttpClient5AdapterFilterFactory; import com.eactive.eai.adapter.http.client.impl.filter.HttpClientAdapterFilter; +import com.eactive.eai.common.util.JacksonUtil; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -37,7 +38,8 @@ public class HttpClient5AdapterServiceRestAddFilter extends HttpClient5AdapterSe // // Adapter에서 Exception이 발생한 경우, 처리할 필터 // static final String EXCEPTION_FILTER = "EXCEPTION_FILTER"; - private ObjectMapper mapper = new ObjectMapper(); + // 기본 ObjectMapper 는 JsonNode 직렬화 시 작은 소수를 1.2E-7 로 바꿔버린다. + private ObjectMapper mapper = JacksonUtil.newNumberSafeMapper(); /** * 1. 기능 : HttpClient 호출 전후 Filter 적용용 2. 처리 개요 :
diff --git a/src/main/java/com/eactive/eai/adapter/http/dynamic/HttpAdapterServiceSupport.java b/src/main/java/com/eactive/eai/adapter/http/dynamic/HttpAdapterServiceSupport.java index e8cebdb..c4c13b9 100644 --- a/src/main/java/com/eactive/eai/adapter/http/dynamic/HttpAdapterServiceSupport.java +++ b/src/main/java/com/eactive/eai/adapter/http/dynamic/HttpAdapterServiceSupport.java @@ -19,6 +19,7 @@ import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilterType; import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException; import com.eactive.eai.common.TransactionContextKeys; import com.eactive.eai.common.server.EAIServerManager; +import com.eactive.eai.common.util.JacksonUtil; import com.eactive.eai.common.util.Logger; import com.eactive.eai.common.util.TxSiftContext; import com.eactive.eai.common.util.UUIDGenerator; @@ -31,7 +32,8 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H public static final String HEADER_NAME_CLIENT_ID = "x-elink-client-id"; public static final String PROPERTIES_NAME_CLIENT_ID = "clientId"; static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER); - private ObjectMapper mapper = new ObjectMapper(); + // 기본 ObjectMapper 는 JsonNode 직렬화 시 작은 소수를 1.2E-7 로 바꿔버린다. + private ObjectMapper mapper = JacksonUtil.newNumberSafeMapper(); EAIServerManager eaiServerManager; String instid = null; diff --git a/src/main/java/com/eactive/eai/common/util/JacksonUtil.java b/src/main/java/com/eactive/eai/common/util/JacksonUtil.java index 4bdce14..143e03b 100644 --- a/src/main/java/com/eactive/eai/common/util/JacksonUtil.java +++ b/src/main/java/com/eactive/eai/common/util/JacksonUtil.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonMappingException; @@ -13,6 +14,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; import org.json.simple.JSONObject; @@ -42,16 +44,26 @@ public final class JacksonUtil { /** * JSON 숫자를 double 로 좁히지 않고 BigDecimal 로, 수신한 자릿수 그대로 유지하는 ObjectMapper. * - * readTree() 로 파싱한 뒤 writeValueAsString() 으로 다시 문자열을 만드는 왕복에서, - * 기본 설정이면 100000000.00 이 1.0E8 로 변형된다. - * USE_BIG_DECIMAL_FOR_FLOATS 만 켜고 withExactBigDecimals(true) 를 빼면 기본 - * JsonNodeFactory 가 stripTrailingZeros() 를 적용해 scale 이 음수가 되어 1E+8 이 된다. - * 두 옵션을 함께 켜야 수신한 값이 그대로 보존된다. + * 입력측(파싱) - 2개 옵션이 함께 필요하다. + * readTree() 로 파싱한 뒤 writeValueAsString() 으로 다시 문자열을 만드는 왕복에서, + * 기본 설정이면 100000000.00 이 1.0E8 로 변형된다. + * USE_BIG_DECIMAL_FOR_FLOATS 만 켜고 withExactBigDecimals(true) 를 빼면 기본 + * JsonNodeFactory 가 stripTrailingZeros() 를 적용해 scale 이 음수가 되어 1E+8 이 된다. + * 두 옵션을 함께 켜야 수신한 값이 그대로 보존된다. + * + * 출력측(직렬화) - WRITE_BIGDECIMAL_AS_PLAIN 이 추가로 필요하다. + * DecimalNode 직렬화는 결국 BigDecimal.toString() 이고, 이것은 + * scale 이 음수이거나 adjusted exponent 가 -6 미만일 때 지수 표기를 쓴다. + * 즉 위 2개 옵션으로 파싱을 제대로 해도, 내보낼 때 작은 소수가 깨진다. + * 0.00000012 -> 1.2E-7 , -0.0000005 -> -5E-7 + * 금액처럼 scale 이 0 이상인 큰 값은 영향이 없지만, 이율/환율은 깨진다. + * (Jackson 2.12.7 실측, 2026-08-27) */ public static ObjectMapper newNumberSafeMapper() { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); objectMapper.setNodeFactory(JsonNodeFactory.withExactBigDecimals(true)); + objectMapper.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN); return objectMapper; } @@ -256,7 +268,9 @@ public final class JacksonUtil { if (target == null || !target.isArray() || lastIdx < 0 || lastIdx >= target.size()) { return false; } - ((ArrayNode) target).set(lastIdx, value); + // set(int, String) 오버로드는 jackson-databind 2.13 부터다. 실제 런타임은 2.12.7 이므로 + // TextNode 로 감싸 set(int, JsonNode) 에 바인딩해야 NoSuchMethodError 가 나지 않는다. + ((ArrayNode) target).set(lastIdx, TextNode.valueOf(value)); return true; } else { // 객체 필드 값 교체 (target은 fieldName으로 이미 이동된 상태이므로, parent 기준 재설정 필요) diff --git a/src/main/java/com/eactive/eai/common/util/MessageKeyExtractor.java b/src/main/java/com/eactive/eai/common/util/MessageKeyExtractor.java index 07e8e96..a41a4ef 100644 --- a/src/main/java/com/eactive/eai/common/util/MessageKeyExtractor.java +++ b/src/main/java/com/eactive/eai/common/util/MessageKeyExtractor.java @@ -9,7 +9,6 @@ import com.eactive.eai.common.messagekey.MessageKeyGroupVO; import com.eactive.eai.common.messagekey.MessageKeyManager; import com.eactive.eai.common.messagekey.MessageKeyVO; import com.eactive.eai.transformer.message.ISO8583MessageFactory; -import com.fasterxml.jackson.databind.ObjectMapper; import com.solab.iso8583.IsoMessage; import org.apache.commons.lang3.StringUtils; import org.json.simple.JSONValue; @@ -23,8 +22,6 @@ public final class MessageKeyExtractor { static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); - private static final ObjectMapper objectMapper = new ObjectMapper(); - /** * Private 생성자 * Instance를 생성하지 못함 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 a25aeb5..e27d9c9 100644 --- a/src/main/java/com/eactive/eai/message/parser/JsonReader.java +++ b/src/main/java/com/eactive/eai/message/parser/JsonReader.java @@ -8,21 +8,25 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.eactive.eai.common.util.JacksonUtil; import com.eactive.eai.message.EncodingVar; import com.eactive.eai.message.StandardItem; import com.eactive.eai.message.StandardMessage; import com.eactive.eai.message.StandardType; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.JsonNodeType; public class JsonReader implements StandardReader { static Logger logger = LoggerFactory.getLogger(JsonReader.class); + + // ObjectMapper 는 설정이 끝나면 thread-safe 하고 생성 비용이 크므로 재사용한다. + // 숫자 보존 옵션(입력/출력)은 JacksonUtil.newNumberSafeMapper() 참조. + private static final ObjectMapper MAPPER = JacksonUtil.newNumberSafeMapper(); + private char FIELD_SEPARATOR = '.'; private boolean ZERO_BASE_INDEX = true; @@ -39,15 +43,9 @@ public class JsonReader implements StandardReader { } JsonNode jsonNode = null; - ObjectMapper mapper = null; + ObjectMapper mapper = MAPPER; JsonFactory factory = null; JsonParser parser = null; - mapper = new ObjectMapper(); - mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); - // USE_BIG_DECIMAL_FOR_FLOATS 만 켜면 기본 JsonNodeFactory 가 DecimalNode 생성 시 - // stripTrailingZeros() 를 적용해 scale 이 음수가 되고, 그 결과 - // 100000000.00 이 1E+8 로 변형된다. exact 모드로 수신한 자릿수를 보존한다. - mapper.setNodeFactory(JsonNodeFactory.withExactBigDecimals(true)); factory = mapper.getFactory(); try { parser = factory.createParser(jsonString);