perf(parse): #326 parseJson truncated-array O(N²) → 단일 패스

OciGenAiService.parseJson:
- 잘린 배열 복구 시 각 idx에서 end를 1씩 늘려 readValue 시도하던 O(N²) 로직 제거
- findObjectEnd: brace depth counter (문자열/escape 처리) 단일 패스 O(N)
- 8192 token 응답 처리 시간 수백 ms → 10ms 이하 예상
- 매 try마다 Jackson 예외 객체/스택트레이스 생성하던 부담 제거

설계서: docs/design/326-parsejson-optimization/README.md (Approved)

Refs: #326
This commit is contained in:
joungmin
2026-06-15 15:35:48 +09:00
parent ed61d29632
commit 648ccde4d7
2 changed files with 115 additions and 12 deletions

View File

@@ -150,26 +150,25 @@ public class OciGenAiService {
return mapper.readValue(raw, Object.class);
} catch (Exception ignored) {}
// Try to recover truncated array
// #326 — Recover truncated array. Brace depth counter로 단일 패스 O(N).
// 이전: 각 idx에서 end를 1씩 늘려가며 매번 readValue → O(N²) + 예외 스택트레이스 양산.
if (raw.trim().startsWith("[")) {
List<Object> items = new ArrayList<>();
int idx = raw.indexOf('[') + 1;
while (idx < raw.length()) {
while (idx < raw.length() && " \t\n\r,".indexOf(raw.charAt(idx)) >= 0) idx++;
if (idx >= raw.length() || raw.charAt(idx) == ']') break;
if (raw.charAt(idx) != '{') break; // 객체 시작이 아니면 복구 중단
// Try to parse next object
boolean found = false;
for (int end = idx + 1; end <= raw.length(); end++) {
try {
Object obj = mapper.readValue(raw.substring(idx, end), Object.class);
items.add(obj);
idx = end;
found = true;
break;
} catch (Exception ignored2) {}
int end = findObjectEnd(raw, idx);
if (end < 0) break; // 잘린 객체 — 거기서 멈춤
try {
Object obj = mapper.readValue(raw.substring(idx, end + 1), Object.class);
items.add(obj);
} catch (Exception ignored2) {
break; // 불가해 객체 — 멈춤
}
if (!found) break;
idx = end + 1;
}
if (!items.isEmpty()) {
log.info("Recovered {} items from truncated JSON", items.size());
@@ -179,4 +178,27 @@ public class OciGenAiService {
throw new RuntimeException("JSON parse failed: " + raw.substring(0, Math.min(80, raw.length())));
}
/**
* #326 — JSON 객체 시작 위치(`{`)에서 매칭되는 닫는 `}` 인덱스를 반환.
* 문자열 안의 `{` `}`와 escape는 무시. 매칭 못 찾으면 -1.
*/
private static int findObjectEnd(String raw, int start) {
int depth = 0;
boolean inString = false;
boolean escaped = false;
for (int i = start; i < raw.length(); i++) {
char c = raw.charAt(i);
if (escaped) { escaped = false; continue; }
if (c == '\\') { escaped = true; continue; }
if (c == '"') { inString = !inString; continue; }
if (inString) continue;
if (c == '{') depth++;
else if (c == '}') {
depth--;
if (depth == 0) return i;
}
}
return -1;
}
}