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:
@@ -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<String, String> 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<String, String> 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);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@@ -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\" } ] } }";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user