From 87649f4fca94906c329bb93c6b76d369ee3be310 Mon Sep 17 00:00:00 2001 From: curry772 Date: Tue, 15 Sep 2026 09:01:35 +0900 Subject: [PATCH] =?UTF-8?q?JSON=20=EA=B2=BD=EB=A1=9C=EA=B0=80=20/=20?= =?UTF-8?q?=EC=97=86=EC=9D=B4=20=EC=99=80=EB=8F=84=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=C2=B7=EC=84=A4=EC=A0=95=EC=9D=B4=20=EB=90=98?= =?UTF-8?q?=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 - mergeAtRoot / setAt 에서 "encrypted_data" 처럼 선행 / 가 없는 경로를 "/encrypted_data" 로 맞춤 (substring(0, -1) 예외로 병합이 조용히 실패하던 문제) - mergeAtRoot 실패 시 원본을 반환하던 catch 에 WARN 로그 추가 --- .../com/eactive/eai/util/JsonPathUtil.java | 21 +++- .../eactive/eai/util/JsonPathUtilTest.java | 98 +++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/eactive/eai/util/JsonPathUtilTest.java diff --git a/src/main/java/com/eactive/eai/util/JsonPathUtil.java b/src/main/java/com/eactive/eai/util/JsonPathUtil.java index ae9a733..0853ad8 100644 --- a/src/main/java/com/eactive/eai/util/JsonPathUtil.java +++ b/src/main/java/com/eactive/eai/util/JsonPathUtil.java @@ -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; } } diff --git a/src/test/java/com/eactive/eai/util/JsonPathUtilTest.java b/src/test/java/com/eactive/eai/util/JsonPathUtilTest.java new file mode 100644 index 0000000..1ead1f6 --- /dev/null +++ b/src/test/java/com/eactive/eai/util/JsonPathUtilTest.java @@ -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")); + } +}