From 29b8b7eaac958770499ceb9c3a32aa7aa0de87b3 Mon Sep 17 00:00:00 2001 From: curry772 Date: Thu, 14 May 2026 13:55:29 +0900 Subject: [PATCH] =?UTF-8?q?perf:=20encryptField/decryptField=20JSON=20?= =?UTF-8?q?=ED=8C=8C=EC=8B=B1=20=ED=9A=9F=EC=88=98=20=EC=A0=88=EA=B0=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JsonPathUtil에 JsonNode 기반 저수준 API(toTree/getAt/setAt/fromTree) 추가. 기존 getValueAtPath/setValueAtPath/mergeAtRoot는 내부적으로 이를 위임. AbstractCryptoFilter.encryptField, decryptField는 파싱된 JsonNode를 재사용하여 JSON 파싱을 기존 2~3회에서 1회로 단축. - encryptField non-root: 파싱 2회 → 1회 - decryptField non-root: 파싱 2회 → 1회 - decryptField toPath="/": 파싱 3회 → 2회 (body 1회 + mergeJson 1회) Co-Authored-By: Claude Sonnet 4.6 --- .../http/filter/AbstractCryptoFilter.java | 41 +++---- .../com/eactive/eai/util/JsonPathUtil.java | 104 ++++++++++++------ 2 files changed, 90 insertions(+), 55 deletions(-) diff --git a/src/main/java/com/eactive/eai/adapter/http/filter/AbstractCryptoFilter.java b/src/main/java/com/eactive/eai/adapter/http/filter/AbstractCryptoFilter.java index 59fba34..f83cc51 100644 --- a/src/main/java/com/eactive/eai/adapter/http/filter/AbstractCryptoFilter.java +++ b/src/main/java/com/eactive/eai/adapter/http/filter/AbstractCryptoFilter.java @@ -10,6 +10,7 @@ import org.apache.commons.lang3.StringUtils; import com.eactive.eai.common.security.CryptoModuleService; import com.eactive.eai.common.util.Logger; import com.eactive.eai.util.JsonPathUtil; +import com.fasterxml.jackson.databind.JsonNode; /** * InCryptoFilter / OutCryptoFilter 공통 기반 클래스. @@ -62,12 +63,13 @@ public abstract class AbstractCryptoFilter { } /** - * fromPath의 Base64 값을 복호화하여 toPath에 반영. + * fromPath의 Base64 값을 복호화하여 toPath에 반영. JSON 파싱 1회. * toPath = "/" → fromPath 필드를 제거하고 복호화된 JSON을 body에 병합. */ protected String decryptField(String body, String moduleName, Map runtimeCtx, byte[] aad, String fromPath, String toPath) throws Exception { - String encBase64 = JsonPathUtil.getValueAtPath(body, fromPath); + JsonNode root = JsonPathUtil.toTree(body); + String encBase64 = JsonPathUtil.getAt(root, fromPath); if (StringUtils.isBlank(encBase64)) { logger.warn("CryptoFilter] 복호화 대상 필드 없음: path=" + fromPath); return body; @@ -76,9 +78,10 @@ public abstract class AbstractCryptoFilter { byte[] plainBytes = CryptoModuleService.getInstance().decrypt(moduleName, runtimeCtx, aad, cipherBytes); String plainText = new String(plainBytes, StandardCharsets.UTF_8); if (PATH_ROOT.equals(toPath)) { - return JsonPathUtil.mergeAtRoot(body, fromPath, plainText); + return JsonPathUtil.mergeAtRoot(root, fromPath, plainText); } - return JsonPathUtil.setValueAtPath(body, toPath, plainText, false); + JsonPathUtil.setAt(root, toPath, plainText); + return JsonPathUtil.fromTree(root); } // ------------------------------------------------------------------ @@ -93,29 +96,29 @@ public abstract class AbstractCryptoFilter { } /** - * fromPath 값을 암호화하여 toPath(Base64)에 반영. - * fromPath = "/" → body 전체를 암호화하여 toPath 필드명으로 래핑. + * fromPath 값을 암호화하여 toPath(Base64)에 반영. JSON 파싱 1회. + * fromPath = "/" → body 전체를 암호화하여 toPath 필드명으로 래핑 (파싱 불필요). */ protected String encryptField(String body, String moduleName, Map runtimeCtx, byte[] aad, String fromPath, String toPath) throws Exception { - String plainText; if (PATH_ROOT.equals(fromPath)) { - plainText = body; - } else { - plainText = JsonPathUtil.getValueAtPath(body, fromPath); - if (StringUtils.isBlank(plainText)) { - logger.warn("CryptoFilter] 암호화 대상 필드 없음: path=" + fromPath); - return body; - } + byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad, + body.getBytes(StandardCharsets.UTF_8)); + String encBase64 = Base64.getEncoder().encodeToString(cipherBytes); + String fieldName = toPath.replaceFirst("^/", ""); + return "{\"" + fieldName + "\":\"" + encBase64 + "\"}"; + } + JsonNode root = JsonPathUtil.toTree(body); + String plainText = JsonPathUtil.getAt(root, fromPath); + if (StringUtils.isBlank(plainText)) { + logger.warn("CryptoFilter] 암호화 대상 필드 없음: path=" + fromPath); + return body; } byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad, plainText.getBytes(StandardCharsets.UTF_8)); String encBase64 = Base64.getEncoder().encodeToString(cipherBytes); - if (PATH_ROOT.equals(fromPath)) { - String fieldName = toPath.replaceFirst("^/", ""); - return "{\"" + fieldName + "\":\"" + encBase64 + "\"}"; - } - return JsonPathUtil.setValueAtPath(body, toPath, encBase64, false); + JsonPathUtil.setAt(root, toPath, encBase64); + return JsonPathUtil.fromTree(root); } // ------------------------------------------------------------------ diff --git a/src/main/java/com/eactive/eai/util/JsonPathUtil.java b/src/main/java/com/eactive/eai/util/JsonPathUtil.java index d7fc69d..ea1ad87 100644 --- a/src/main/java/com/eactive/eai/util/JsonPathUtil.java +++ b/src/main/java/com/eactive/eai/util/JsonPathUtil.java @@ -10,34 +10,55 @@ import com.fasterxml.jackson.databind.node.ObjectNode; // 해당 필드에 대한 get/set 을 수행 public class JsonPathUtil { private JsonPathUtil() { - + } - + private static final ObjectMapper objectMapper = new ObjectMapper(); + // --------------------------------------------------------------- + // JsonNode 기반 단일 파싱 API — 연속 get/set 시 파싱 횟수 절감 + // --------------------------------------------------------------- + + /** JSON 문자열을 JsonNode 트리로 파싱. */ + public static JsonNode toTree(String json) throws Exception { + return objectMapper.readTree(json); + } + + /** 이미 파싱된 트리에서 경로의 값을 추출. */ + public static String getAt(JsonNode root, String path) { + JsonNode node = getNodeAtPath(root, path); + return node != null ? node.asText() : null; + } + + /** 이미 파싱된 트리의 경로에 값을 설정 (in-place). */ + public static void setAt(JsonNode root, String path, String value) { + JsonNode parent = getParentNode(root, getParentPath(path)); + if (parent == null) return; + String key = getFieldName(path); + if (parent.isArray()) { + ((ArrayNode) parent).set(Integer.parseInt(key), objectMapper.convertValue(value, JsonNode.class)); + } else if (parent.isObject()) { + ((ObjectNode) parent).put(key, value); + } + } + + /** JsonNode 트리를 JSON 문자열로 직렬화. */ + public static String fromTree(JsonNode root) throws Exception { + return objectMapper.writeValueAsString(root); + } + + // --------------------------------------------------------------- + // 문자열 기반 API (내부적으로 위 메서드에 위임) + // --------------------------------------------------------------- + // 특정 경로의 값을 설정하는 함수 public static String setValueAtPath(String jsonString, String path, String newValue, boolean isPretty) { try { - // JSON 문자열을 JsonNode 객체로 파싱 JsonNode rootNode = objectMapper.readTree(jsonString); - - // 경로에 해당하는 부모 노드를 가져옴 - JsonNode parentNode = getParentNode(rootNode, getParentPath(path)); - - // 경로의 마지막 노드 이름 또는 배열 인덱스 추출 - String fieldNameOrIndex = getFieldName(path); - - // 배열 또는 객체에서 값을 설정 - if (parentNode.isArray()) { - ((ArrayNode) parentNode).set(Integer.parseInt(fieldNameOrIndex), objectMapper.convertValue(newValue, JsonNode.class)); - } else if (parentNode.isObject()) { - ((ObjectNode) parentNode).put(fieldNameOrIndex, newValue); - } - - // 수정된 JSON 문자열 반환 + setAt(rootNode, path, newValue); if(isPretty) return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(rootNode); - else + else return objectMapper.writeValueAsString(rootNode); } catch (Exception e) { e.printStackTrace(); @@ -48,13 +69,7 @@ public class JsonPathUtil { // 특정 경로의 값을 가져오는 함수 public static String getValueAtPath(String jsonString, String path) { try { - // JSON 문자열을 JsonNode 객체로 파싱 - JsonNode rootNode = objectMapper.readTree(jsonString); - - // 경로에 해당하는 노드의 값을 반환 - JsonNode resultNode = getNodeAtPath(rootNode, path); - return resultNode != null ? resultNode.asText() : null; - + return getAt(objectMapper.readTree(jsonString), path); } catch (Exception e) { e.printStackTrace(); return null; @@ -102,25 +117,19 @@ public class JsonPathUtil { } /** - * body에서 removeFromPath 필드를 제거한 뒤, mergeJson의 필드를 body에 병합하여 반환한다. + * 이미 파싱된 bodyNode에서 removeFromPath 필드를 제거하고 mergeJson을 병합. + * body의 재파싱 없이 호출 가능하여 파싱 횟수를 1회 절감. * * body와 mergeJson이 모두 JSON 객체인 경우에만 병합을 수행한다. * 어느 한쪽이 JSON 객체가 아니면 mergeJson을 그대로 반환한다 (전체 교체 fallback). * - * 사용 예 (toPath="/" 복호화): - * body = {"encryptedData":"...","requestId":"REQ001"} - * removeFrom = /encryptedData - * mergeJson = {"userId":"U001","name":"test"} - * 결과 = {"requestId":"REQ001","userId":"U001","name":"test"} - * - * @param body 원본 JSON 문자열 + * @param bodyNode 이미 파싱된 원본 JsonNode * @param removeFromPath 제거할 필드의 경로 (예: /encryptedData) * @param mergeJson 병합할 JSON 문자열 * @return 병합된 JSON 문자열, 병합 불가 시 mergeJson 그대로 반환 */ - public static String mergeAtRoot(String body, String removeFromPath, String mergeJson) { + public static String mergeAtRoot(JsonNode bodyNode, String removeFromPath, String mergeJson) { try { - JsonNode bodyNode = objectMapper.readTree(body); JsonNode mergeNode = objectMapper.readTree(mergeJson); if (!bodyNode.isObject() || !mergeNode.isObject()) { @@ -149,6 +158,29 @@ public class JsonPathUtil { } } + /** + * body에서 removeFromPath 필드를 제거한 뒤, mergeJson의 필드를 body에 병합하여 반환한다. + * (문자열 기반 overload — 내부적으로 body를 1회 파싱하여 위 메서드에 위임) + * + * 사용 예 (toPath="/" 복호화): + * body = {"encryptedData":"...","requestId":"REQ001"} + * removeFrom = /encryptedData + * mergeJson = {"userId":"U001","name":"test"} + * 결과 = {"requestId":"REQ001","userId":"U001","name":"test"} + * + * @param body 원본 JSON 문자열 + * @param removeFromPath 제거할 필드의 경로 (예: /encryptedData) + * @param mergeJson 병합할 JSON 문자열 + * @return 병합된 JSON 문자열, 병합 불가 시 mergeJson 그대로 반환 + */ + public static String mergeAtRoot(String body, String removeFromPath, String mergeJson) { + try { + return mergeAtRoot(objectMapper.readTree(body), removeFromPath, mergeJson); + } catch (Exception e) { + return mergeJson; + } + } + public static void main(String[] args) { String jsonString = "{ \"person\": { \"name\": \"John\", \"age\": 30, \"groups\": [ { \"id\": 1, \"name\": \"Group A\" }, { \"id\": 2, \"name\": \"Group B\" }, { \"id\": 3, \"name\": \"Group C\" } ] } }";