JSON 경로가 / 없이 와도 필드 제거·설정이 되도록 수정

- mergeAtRoot / setAt 에서 "encrypted_data" 처럼 선행 / 가 없는 경로를 "/encrypted_data" 로 맞춤
  (substring(0, -1) 예외로 병합이 조용히 실패하던 문제)
- mergeAtRoot 실패 시 원본을 반환하던 catch 에 WARN 로그 추가
This commit is contained in:
curry772
2026-09-15 09:01:35 +09:00
parent c8f7bfe1b1
commit 87649f4fca
2 changed files with 118 additions and 1 deletions
@@ -3,6 +3,7 @@ package com.eactive.eai.util;
import org.json.simple.JSONObject;
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;
import com.fasterxml.jackson.databind.node.ArrayNode;
@@ -21,6 +22,19 @@ public class JsonPathUtil {
// 기본 ObjectMapper 는 100000000.00 을 1.0E8 로 바꿔버린다.
private static final ObjectMapper objectMapper = JacksonUtil.newNumberSafeMapper();
private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
/**
* 경로를 "/" 로 시작하도록 맞춘다. ("encrypted_data" → "/encrypted_data")
* 부모 경로·필드명 계산이 선행 "/" 를 전제로 하므로, 없으면 substring(0, -1) 로 실패한다.
*/
private static String normalizePath(String path) {
if (path == null || path.startsWith("/")) {
return path;
}
return "/" + path;
}
// ---------------------------------------------------------------
// JsonNode 기반 단일 파싱 API — 연속 get/set 시 파싱 횟수 절감
// ---------------------------------------------------------------
@@ -53,6 +67,7 @@ public class JsonPathUtil {
/** 이미 파싱된 트리의 경로에 값을 설정 (in-place). */
public static void setAt(JsonNode root, String path, String value) {
path = normalizePath(path);
JsonNode parent = getParentNode(root, getParentPath(path));
if (parent == null) return;
String key = getFieldName(path);
@@ -159,7 +174,8 @@ public class JsonPathUtil {
ObjectNode result = (ObjectNode) bodyNode.deepCopy();
// removeFromPath 필드 제거
// removeFromPath 필드 제거 ("encrypted_data" 처럼 "/" 없이 와도 같은 경로로 본다)
removeFromPath = normalizePath(removeFromPath);
int lastSlash = removeFromPath.lastIndexOf('/');
String fieldName = removeFromPath.substring(lastSlash + 1);
if (lastSlash == 0) {
@@ -176,6 +192,8 @@ public class JsonPathUtil {
// return objectMapper.writeValueAsString(result);
return result;
} catch (Exception e) {
// 병합 실패 시 원본을 그대로 돌려주므로, 원인이 묻히지 않게 남긴다.
logger.warn("JsonPathUtil] mergeAtRoot failed. return original body. removeFromPath=" + removeFromPath, e);
return bodyNode;
}
}
@@ -199,6 +217,7 @@ public class JsonPathUtil {
try {
return objectMapper.writeValueAsString(mergeAtRoot(objectMapper.readTree(body), removeFromPath, mergeJson));
} catch (Exception e) {
logger.warn("JsonPathUtil] mergeAtRoot failed. return mergeJson. removeFromPath=" + removeFromPath, e);
return mergeJson;
}
}
@@ -0,0 +1,98 @@
package com.eactive.eai.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import com.fasterxml.jackson.databind.JsonNode;
/**
* JsonPathUtil 단위테스트
*
* 경로를 "/" 없이 넘겨도("encrypted_data") "/encrypted_data" 와 같게 동작하는지와,
* 기존 "/" 경로·배열 인덱스 동작이 유지되는지를 본다.
*/
class JsonPathUtilTest {
private static final String ENC_BODY = "{\"encrypted_data\":\"CIPHER\",\"requestId\":\"R1\"}";
private static final String PLAIN = "{\"userId\":\"U1\",\"name\":\"test\"}";
@Test
@DisplayName("1. mergeAtRoot - 경로 앞에 / 가 없어도 암호문 필드를 지우고 병합한다")
void mergeAtRoot_슬래시없음() throws Exception {
JsonNode withSlash = JsonPathUtil.mergeAtRoot(JsonPathUtil.toTree(ENC_BODY), "/encrypted_data", PLAIN);
JsonNode withoutSlash = JsonPathUtil.mergeAtRoot(JsonPathUtil.toTree(ENC_BODY), "encrypted_data", PLAIN);
assertEquals(withSlash, withoutSlash);
assertFalse(withoutSlash.has("encrypted_data"), "암호문 필드는 제거돼야 한다");
assertEquals("R1", withoutSlash.get("requestId").asText());
assertEquals("U1", withoutSlash.get("userId").asText());
assertEquals("test", withoutSlash.get("name").asText());
}
@Test
@DisplayName("2. mergeAtRoot - 중첩 경로도 / 유무와 관계없이 해당 필드를 지운다")
void mergeAtRoot_중첩경로() throws Exception {
String body = "{\"data\":{\"encrypted_data\":\"CIPHER\",\"k\":\"v\"},\"r\":\"1\"}";
JsonNode withSlash = JsonPathUtil.mergeAtRoot(JsonPathUtil.toTree(body), "/data/encrypted_data", PLAIN);
JsonNode withoutSlash = JsonPathUtil.mergeAtRoot(JsonPathUtil.toTree(body), "data/encrypted_data", PLAIN);
assertEquals(withSlash, withoutSlash);
assertFalse(withoutSlash.get("data").has("encrypted_data"));
assertEquals("v", withoutSlash.get("data").get("k").asText());
assertEquals("U1", withoutSlash.get("userId").asText());
}
@Test
@DisplayName("3. mergeAtRoot - 병합할 값이 JSON 객체가 아니면 원본을 그대로 반환한다")
void mergeAtRoot_객체아님() throws Exception {
JsonNode body = JsonPathUtil.toTree(ENC_BODY);
JsonNode result = JsonPathUtil.mergeAtRoot(body, "encrypted_data", "\"plain text\"");
assertSame(body, result);
}
@Test
@DisplayName("4. mergeAtRoot(String) - 문자열 오버로드도 / 없이 병합한다")
void mergeAtRoot_문자열() throws Exception {
String result = JsonPathUtil.mergeAtRoot(ENC_BODY, "encrypted_data", PLAIN);
JsonNode node = JsonPathUtil.toTree(result);
assertFalse(node.has("encrypted_data"));
assertEquals("U1", node.get("userId").asText());
}
@Test
@DisplayName("5. setAt - 경로 앞에 / 가 없어도 최상위·중첩 필드에 값을 설정한다")
void setAt_슬래시없음() throws Exception {
JsonNode root = JsonPathUtil.toTree("{\"data\":{\"value\":\"old\"}}");
JsonPathUtil.setAt(root, "encrypted_data", "CIPHER");
JsonPathUtil.setAt(root, "data/value", "new");
assertEquals("CIPHER", root.get("encrypted_data").asText());
assertEquals("new", root.get("data").get("value").asText());
}
@Test
@DisplayName("6. 기존 동작 유지 - / 경로와 배열 인덱스 경로의 조회·설정")
void 기존동작_유지() throws Exception {
JsonNode root = JsonPathUtil.toTree(
"{\"person\":{\"name\":\"John\",\"groups\":[{\"name\":\"A\"},{\"name\":\"B\"}]}}");
assertEquals("B", JsonPathUtil.getAt(root, "/person/groups/1/name"));
assertEquals("John", JsonPathUtil.getAt(root, "person/name"));
JsonPathUtil.setAt(root, "/person/groups/0/name", "Z");
JsonPathUtil.setAt(root, "/topLevel", "T");
assertEquals("Z", JsonPathUtil.getAt(root, "/person/groups/0/name"));
assertTrue(root.has("topLevel"));
}
}