-
Notifications
You must be signed in to change notification settings - Fork 0
Refactor: Json파싱 표준화로 MCP Servie 리팩토링 #412
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,11 +2,15 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.fasterxml.jackson.databind.JsonNode; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.fasterxml.jackson.databind.node.ArrayNode; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.fasterxml.jackson.databind.node.ObjectNode; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import io.modelcontextprotocol.client.McpSyncClient; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import io.modelcontextprotocol.spec.McpError; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import io.modelcontextprotocol.spec.McpSchema.CallToolRequest; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import io.modelcontextprotocol.spec.McpSchema.CallToolResult; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Locale; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Map; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.extern.slf4j.Slf4j; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -18,11 +22,38 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public class BraveSearchMcpService { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final String BRAVE_WEB_TOOL = "brave_web_search"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final int MAX_JSON_RECURSION_DEPTH = 3; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final List<McpSyncClient> mcpClients; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private final ObjectMapper objectMapper; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static boolean looksLikeJson(String s) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (s == null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String t = s.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return (!t.isEmpty()) && (t.charAt(0) == '{' || t.charAt(0) == '['); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static String truncate(String s, int max) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (s == null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return s.length() > max ? s.substring(0, max) + "…" : s; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static String getTrimmed(JsonNode obj, String field) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (obj == null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JsonNode n = obj.get(field); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (n == null || n.isNull()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String v = n.asText(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return v == null ? null : v.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public JsonNode search(String query, int count, int offset) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| McpSyncClient braveClient = resolveBraveClient(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -33,31 +64,210 @@ public JsonNode search(String query, int count, int offset) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CallToolResult result = braveClient.callTool(request); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JsonNode content = objectMapper.valueToTree(result.content()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.info("[Brave MCP Response Raw content]: {}", content.toPrettyString()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JsonNode raw = objectMapper.valueToTree(result.content()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.info("[Brave MCP Response Raw content]: {}", raw); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ArrayNode normalized = objectMapper.createArrayNode(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| normalizeContent(raw, normalized); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (content != null && content.isArray()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var root = objectMapper.createObjectNode(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| root.set("results", content); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return root; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ObjectNode root = objectMapper.createObjectNode(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| root.set("results", normalized); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return content != null ? content : objectMapper.createObjectNode(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.info("Brave 검색 결과 정규화 완료: {}건", normalized.size()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return root; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * MCP 클라이언트들 중 brave_web_search 툴을 가진 클라이언트 선택. 초기화되지 않은 경우 1회 initialize() 후 재시도. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private McpSyncClient resolveBraveClient() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (McpSyncClient client : mcpClients) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ListToolsResult tools = client.listTools(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (tools != null && tools.tools() != null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| boolean found = tools.tools().stream() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .anyMatch(tool -> BRAVE_WEB_TOOL.equalsIgnoreCase(tool.name())); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (found) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ListToolsResult tools = client.listTools(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (hasBraveTool(tools)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return client; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (McpError e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String msg = e.getMessage() == null ? "" : e.getMessage().toLowerCase(Locale.ROOT); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| boolean likelyUninitialized = msg.contains("not initialized") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| || msg.contains("uninitialized") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| || (msg.contains("initialize") && msg.contains("required")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (likelyUninitialized) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 1회만 초기화 재시도 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.warn("MCP 클라이언트 미초기화 감지 → initialize() 재시도"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client.initialize(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ListToolsResult tools = client.listTools(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (hasBraveTool(tools)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return client; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (Exception initError) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.error("MCP 클라이언트 초기화 실패: {}", initError.getMessage()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.debug("listTools() 예외: {}", e.getMessage()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.debug("listTools() 일반 예외: {}", e.getMessage()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new IllegalStateException("Brave MCP 서버에서 brave_web_search 툴을 찾을 수 없습니다."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private boolean hasBraveTool(ListToolsResult tools) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return tools != null && tools.tools() != null && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tools.tools().stream().anyMatch(t -> BRAVE_WEB_TOOL.equalsIgnoreCase(t.name())); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * raw JSON(any shape) → results[{url,title,description}] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private void normalizeContent(JsonNode raw, ArrayNode out) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (raw == null || raw.isNull()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (raw.isArray()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (JsonNode n : raw) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| normalizeOne(n, out); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| normalizeOne(raw, out); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private void normalizeOne(JsonNode node, ArrayNode out) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (node == null || node.isNull()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 이미 {url,title,description} 형태 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (node.isObject() && (node.has("url") || node.has("title") || node.has("description"))) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| addObjectToResults((ObjectNode) node, out); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // MCP가 주는 content item: { "type":"text", "text":"{...json...}" } 같은 형태를 방어 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (node.isObject() && node.has("text")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String text = node.path("text").asText(""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (looksLikeJson(text)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parseJsonBlockIntoResults(text, out, 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // "Title: ..." 라인 포맷 등 레거시 텍스트 포맷 처리(옵션) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parseLegacyBlock(text, out); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+139
to
+161
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion normalizeOne가 객체 루트의 "results" 배열을 직접 처리하지 않습니다 현재는 문자열 JSON으로 들어온 경우(parseJsonBlockIntoResults)만 루트에 results 배열을 순회합니다. 실제로 raw가 이미 객체(JSON)인 경우에도 results 배열을 지원하도록 분기를 추가해야 누락이 없습니다. 아래와 같이 분기를 추가해 주세요. private void normalizeOne(JsonNode node, ArrayNode out) {
if (node == null || node.isNull()) {
return;
}
// 이미 {url,title,description} 형태
if (node.isObject() && (node.has("url") || node.has("title") || node.has("description"))) {
addObjectToResults((ObjectNode) node, out);
return;
}
+ // 루트에 results 배열이 있는 객체 형태 처리
+ if (node.isObject() && node.has("results") && node.get("results").isArray()) {
+ for (JsonNode n : node.get("results")) {
+ normalizeOne(n, out);
+ }
+ return;
+ }
+
// MCP가 주는 content item: { "type":"text", "text":"{...json...}" } 같은 형태를 방어
if (node.isObject() && node.has("text")) {
String text = node.path("text").asText("");
if (looksLikeJson(text)) {
parseJsonBlockIntoResults(text, out, 0);
} else {
// "Title: ..." 라인 포맷 등 레거시 텍스트 포맷 처리(옵션)
parseLegacyBlock(text, out);
}
return;
} |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 순수 문자열이지만 안에 JSON이 들어 있는 경우 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (node.isTextual() && looksLikeJson(node.asText())) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parseJsonBlockIntoResults(node.asText(), out, 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private void parseJsonBlockIntoResults(String json, ArrayNode out, int depth) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (depth > MAX_JSON_RECURSION_DEPTH) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.warn("JSON 파싱 재귀 깊이 초과: {}", truncate(json, 100)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JsonNode parsed = objectMapper.readTree(json); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (parsed.isArray()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (JsonNode n : parsed) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (n.isObject()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| addObjectToResults((ObjectNode) n, out); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (n.isTextual() && looksLikeJson(n.asText())) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parseJsonBlockIntoResults(n.asText(), out, depth + 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (parsed.isObject()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ObjectNode obj = (ObjectNode) parsed; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 루트에 results 배열이 있는 형태 처리 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (obj.has("results") && obj.get("results").isArray()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (JsonNode n : obj.get("results")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (n.isObject()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| addObjectToResults((ObjectNode) n, out); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (n.isTextual() && looksLikeJson(n.asText())) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parseJsonBlockIntoResults(n.asText(), out, depth + 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (obj.has("text") && obj.get("text").isTextual() && looksLikeJson( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| obj.get("text").asText())) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // {text:"{...}"} 같은 한 번 더 감싼 케이스 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parseJsonBlockIntoResults(obj.get("text").asText(), out, depth + 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| addObjectToResults(obj, out); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (parsed.isTextual() && looksLikeJson(parsed.asText())) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parseJsonBlockIntoResults(parsed.asText(), out, depth + 1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log.warn("Brave MCP JSON 파싱 실패: {}\n원인: {}", truncate(json, 400), e.getMessage()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private void addObjectToResults(ObjectNode obj, ArrayNode out) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String url = getTrimmed(obj, "url"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String title = getTrimmed(obj, "title"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String desc = getTrimmed(obj, "description"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 세 필드 중 하나라도 있으면 결과로 채택 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ((url != null && !url.isBlank()) || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (title != null && !title.isBlank()) || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (desc != null && !desc.isBlank())) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ObjectNode one = objectMapper.createObjectNode(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (url != null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| one.put("url", url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (title != null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| one.put("title", title); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (desc != null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| one.put("description", desc); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| out.add(one); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 레거시 "Title:..., URL:..., 본문..." 형태의 텍스트 파서(있으면 도움, 없어도 무방) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private void parseLegacyBlock(String text, ArrayNode out) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (text == null || text.isBlank()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String[] lines = text.split("\\r?\\n"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String title = null, url = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| StringBuilder body = new StringBuilder(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (String line : lines) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String trimmed = line.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (trimmed.regionMatches(true, 0, "Title:", 0, 6)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (title != null && url != null && body.length() > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ObjectNode one = objectMapper.createObjectNode(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| one.put("title", title); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| one.put("url", url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| one.put("description", body.toString().trim()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| out.add(one); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body.setLength(0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title = trimmed.substring(6).trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (trimmed.regionMatches(true, 0, "URL:", 0, 4)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url = trimmed.substring(4).trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+246
to
+259
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 레거시 파서의 중간 flush 조건에서 본문(body) 유무를 요구하면 유효 결과가 누락됩니다 Title/URL만 있는 케이스도 결과로 인정해야 합니다. 중간 flush 시 body.length() > 0 조건을 제거하세요. - if (trimmed.regionMatches(true, 0, "Title:", 0, 6)) {
- if (title != null && url != null && body.length() > 0) {
+ if (trimmed.regionMatches(true, 0, "Title:", 0, 6)) {
+ if (title != null && url != null) {
ObjectNode one = objectMapper.createObjectNode();
one.put("title", title);
one.put("url", url);
one.put("description", body.toString().trim());
out.add(one);
body.setLength(0);
}
title = trimmed.substring(6).trim();📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body.append(line).append('\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (title != null && url != null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ObjectNode one = objectMapper.createObjectNode(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| one.put("title", title); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| one.put("url", url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| one.put("description", body.toString().trim()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| out.add(one); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
과도한 원본 응답 로그는 크기/민감도 리스크가 있습니다 — 로그 레벨 하향 및 트렁케이션 제안
대용량/다중 중첩 응답이 그대로 info 로그에 남습니다. 디버깅 구간에서만 필요하므로 debug 레벨과 길이 제한을 권장합니다.
아래와 같이 수정을 제안합니다.
📝 Committable suggestion
🤖 Prompt for AI Agents