newNumberSafeMapper 출력측 옵션 추가 및 ArrayNode.set 버전 호환 수정

JacksonUtil.newNumberSafeMapper() 는 USE_BIG_DECIMAL_FOR_FLOATS +
withExactBigDecimals 로 입력(파싱)측만 막고 있었다. DecimalNode 직렬화는
BigDecimal.toString() 이라 scale 이 음수이거나 adjusted exponent < -6 일 때
지수 표기가 나온다. 즉 파싱을 제대로 해도 내보낼 때 작은 소수가 깨졌다.
  0.00000012 -> 1.2E-7 , -0.0000005 -> -5E-7
WRITE_BIGDECIMAL_AS_PLAIN 을 추가해 출력측까지 보존한다.
(금액처럼 scale >= 0 인 큰 값은 원래 영향 없음. 이율/환율이 대상)

또한 JacksonUtil.setValue() 가 ArrayNode.set(int, String) 을 쓰고 있었다.
이 오버로드는 jackson-databind 2.13 부터이고, 실제 런타임은 2.12.7 이다
(2.12.7 은 set(int, JsonNode) 하나뿐). jackson-dataformat-xml:2.13.1 이
compileOnly 로 컴파일 클래스패스에만 2.13.1 을 올려 컴파일만 통과하던
상태였고, 배열 경로에 setText() 하면 운영에서 NoSuchMethodError 가 났다.
TextNode.valueOf() 로 감싸 set(int, JsonNode) 에 바인딩한다.

그 외
- TemplateCodeConvertAdapterErrorMsgHandler: 응답 JSON 왕복 지점 전환
- HttpAdapterServiceSupport, HttpClient5AdapterServiceRestAddFilter: 직렬화 지점 전환
- JsonReader: parse() 호출마다 ObjectMapper 를 새로 만들던 것을 static 싱글턴으로
  바꾸고 중복 설정 코드를 JacksonUtil 에 위임
- MessageKeyExtractor: 선언만 하고 쓰지 않는 ObjectMapper 필드 제거
- build.gradle: 미사용 jackson-dataformat-xml 제거 (common 은 Ignite 경유로 jackson 확보)

검증: gradlew compileJava BUILD SUCCESSFUL, databind 2.12.7 런타임에서
숫자 왕복 4건 + setText 배열/객체/미존재 경로 5건 전부 통과.
This commit is contained in:
curry772
2026-08-27 18:09:03 +09:00
parent bb8a91f5f2
commit dd7c8a29bf
7 changed files with 40 additions and 23 deletions
+4 -2
View File
@@ -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"
@@ -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(
@@ -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. 처리 개요 : <br>
@@ -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;
@@ -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 기준 재설정 필요)
@@ -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를 생성하지 못함
@@ -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);