perf: encryptField/decryptField JSON 파싱 횟수 절감

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 <noreply@anthropic.com>
This commit is contained in:
curry772
2026-05-14 13:55:29 +09:00
parent 74302df5a2
commit 29b8b7eaac
2 changed files with 90 additions and 55 deletions
@@ -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\" } ] } }";