From cd745c604162b8bf2cc7dcbdbd9d4633484771af Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:49:37 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94:=20String.replace("\u0000",=20"")=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=20=EC=A0=84=20indexOf('\u0000')=20=EC=82=AC=EC=A0=84?= =?UTF-8?q?=20=EA=B2=80=EC=82=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문자열 정제 과정에서 발생하는 불필요한 객체 할당과 GC 오버헤드를 방지하기 위해 `String.replace()` 호출 전 `indexOf()`를 통한 사전 검사를 추가했습니다. 대상 문자가 존재하지 않는 대부분의 경우(fast path)성능이 개선됩니다. --- .jules/bolt.md | 3 +++ CHANGELOG.md | 12 +++++------- .../viewer/artifact/ArtifactLinkService.java | 6 +++++- .../viewer/auth/TenantAccessService.java | 6 +++++- .../clearfolio/viewer/auth/TenantContext.java | 8 +++++--- .../viewer/config/ArtifactStoreProperties.java | 16 ++++++++++++++-- .../viewer/config/ConversionProperties.java | 7 +++++-- .../clearfolio/viewer/model/ConversionJob.java | 5 ++++- 8 files changed, 46 insertions(+), 17 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 284d180a..9ab69f33 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -19,3 +19,6 @@ ## 2026-07-13 - 단일 패스 문자열 치환 최적화 (O(N) 단일 스캔 및 지연 할당) **Learning:** `String.replace()`를 여러 번 체이닝하여 호출하면, 문자열 치환이 발생하지 않는 경우에도 내부적으로 불필요한 스캔이 중복 발생하고, 치환 시마다 새로운 문자열 객체와 char 배열이 할당되어 메모리 낭비와 성능 저하(GC 압박)가 발생한다. **Action:** 여러 문자를 한 번에 치환해야 하는 경우, O(N) 단일 스캔을 통해 `charAt()`으로 문자를 확인하고, 치환이 실제로 필요한 경우에만 `StringBuilder`를 지연 할당(Lazy allocation)하여 성능을 최적화하고 불필요한 메모리 할당을 방지한다. +## 2026-07-20 - 문자열 치환 시 불필요한 객체 할당 방지를 위한 사전 검사(pre-scan) 적용 +**Learning:** `String.replace("\u0000", "")` 와 같은 문자열 치환 메서드를 호출할 때, 대상 문자열에 치환할 문자가 없는 해피 패스(fast path)에서도 내부적으로 정규식 컴파일이나 불필요한 객체 할당 오버헤드가 발생할 수 있습니다. +**Action:** `String.replace`를 호출하기 전에 `indexOf()`를 사용하여 치환할 대상이 문자열에 실제로 존재하는지 먼저 확인(pre-scan)합니다. 대상 문자가 존재할 때만 `replace`를 호출하도록 하여 대부분의 정상적인 문자열 처리 시 발생하는 불필요한 메모리 할당 및 GC 압박을 방지해야 합니다. diff --git a/CHANGELOG.md b/CHANGELOG.md index e8e8c980..80a44133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,18 @@ -## [Unreleased] -### Added -- **UI UX 개선**: 'Details' 버튼 클릭 시, 작업 상세 정보 로드 중에 사용자가 명시적인 로딩 상태를 확인할 수 있도록 'Loading...' 텍스트와 비활성화 상태를 표시하도록 추가했습니다. - -# Changelog - ## [Unreleased] ### 추가된 기능 (Added) +- **문자열 치환(String.replace) 성능 최적화** + - `indexOf("\u0000")` 검사를 통한 사전 필터링(pre-scan)을 추가하여, 치환 대상 문자가 없는 해피 패스(fast path)에서 발생하는 불필요한 객체 할당과 GC 오버헤드를 방지했습니다. + - 적용 대상: `ConversionProperties`, `ArtifactStoreProperties`, `ConversionJob`, `ArtifactLinkService`, `TenantContext`, `TenantAccessService` 클래스의 문자열 정제(sanitize/clean) 메서드. +- **UI UX 개선**: 'Details' 버튼 클릭 시, 작업 상세 정보 로드 중에 사용자가 명시적인 로딩 상태를 확인할 수 있도록 'Loading...' 텍스트와 비활성화 상태를 표시하도록 추가했습니다. - **관리자용 단건 작업 삭제 및 재시도 API 추가** - 특정 변환 작업을 삭제할 수 있는 `DELETE /api/v1/admin/convert/jobs/{jobId}` 엔드포인트를 추가했습니다. - 실패(dead-lettered) 상태인 작업을 관리자가 재시도 큐에 등록할 수 있는 `POST /api/v1/admin/convert/jobs/{jobId}/retry` 엔드포인트를 추가했습니다. - - **비동기 버튼 로딩 피드백 및 상태 복원 개선** - KPI 스냅샷 증거를 다시 불러오는 `refreshKpiEvidence` 동작 중에 "Refresh evidence" 버튼을 비활성화하고 "Refreshing..." 이라는 피드백을 제공하여 사용자의 중복 클릭을 방지했습니다. - 버튼 상태 변경 시 내부 DOM 구조를 보존하기 위해 `Array.from(button.childNodes)`로 원래 노드를 저장하고, 성공 및 실패 후 `finally` 블록에서 `replaceChildren(...)`으로 안전하게 복원하도록 구현했습니다. +# Changelog ## [0.1.0] - 2026-06-25 diff --git a/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkService.java b/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkService.java index efb8ad5e..2c09b8a4 100644 --- a/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkService.java +++ b/src/main/java/com/clearfolio/viewer/artifact/ArtifactLinkService.java @@ -421,7 +421,11 @@ private static String nullableClean(String value) { if (value == null) { return null; } - String cleaned = value.replace("\u0000", "").strip(); + String cleaned = value; + if (cleaned.indexOf('\u0000') != -1) { + cleaned = cleaned.replace("\u0000", ""); + } + cleaned = cleaned.strip(); return cleaned.isEmpty() ? null : cleaned; } diff --git a/src/main/java/com/clearfolio/viewer/auth/TenantAccessService.java b/src/main/java/com/clearfolio/viewer/auth/TenantAccessService.java index ab02e96a..ec98c448 100644 --- a/src/main/java/com/clearfolio/viewer/auth/TenantAccessService.java +++ b/src/main/java/com/clearfolio/viewer/auth/TenantAccessService.java @@ -158,7 +158,11 @@ private static String clean(String value) { if (value == null) { return null; } - String cleaned = value.replace("\u0000", "").strip(); + String cleaned = value; + if (cleaned.indexOf('\u0000') != -1) { + cleaned = cleaned.replace("\u0000", ""); + } + cleaned = cleaned.strip(); return cleaned.isEmpty() ? null : cleaned; } } diff --git a/src/main/java/com/clearfolio/viewer/auth/TenantContext.java b/src/main/java/com/clearfolio/viewer/auth/TenantContext.java index 0d2859ae..6539a636 100644 --- a/src/main/java/com/clearfolio/viewer/auth/TenantContext.java +++ b/src/main/java/com/clearfolio/viewer/auth/TenantContext.java @@ -122,9 +122,11 @@ private static String sanitize(String value) { return null; } - String sanitized = value - .replace("\u0000", "") - .strip(); + String sanitized = value; + if (sanitized.indexOf('\u0000') != -1) { + sanitized = sanitized.replace("\u0000", ""); + } + sanitized = sanitized.strip(); return sanitized.isEmpty() ? null : sanitized; } } diff --git a/src/main/java/com/clearfolio/viewer/config/ArtifactStoreProperties.java b/src/main/java/com/clearfolio/viewer/config/ArtifactStoreProperties.java index c7c829ae..d9aab2d6 100644 --- a/src/main/java/com/clearfolio/viewer/config/ArtifactStoreProperties.java +++ b/src/main/java/com/clearfolio/viewer/config/ArtifactStoreProperties.java @@ -62,7 +62,15 @@ public String getRootDir() { * @param rootDir artifact storage root directory */ public void setRootDir(String rootDir) { - String sanitized = rootDir == null ? "" : rootDir.replace("\u0000", "").strip(); + if (rootDir == null) { + this.rootDir = DEFAULT_ROOT_DIR; + return; + } + String sanitized = rootDir; + if (sanitized.indexOf('\u0000') != -1) { + sanitized = sanitized.replace("\u0000", ""); + } + sanitized = sanitized.strip(); this.rootDir = sanitized.isEmpty() ? DEFAULT_ROOT_DIR : sanitized; } @@ -79,6 +87,10 @@ private static String sanitize(String value) { if (value == null) { return ""; } - return value.replace("\u0000", "").strip().toLowerCase(Locale.ROOT); + String sanitized = value; + if (sanitized.indexOf('\u0000') != -1) { + sanitized = sanitized.replace("\u0000", ""); + } + return sanitized.strip().toLowerCase(Locale.ROOT); } } diff --git a/src/main/java/com/clearfolio/viewer/config/ConversionProperties.java b/src/main/java/com/clearfolio/viewer/config/ConversionProperties.java index a86fc982..b29b1661 100644 --- a/src/main/java/com/clearfolio/viewer/config/ConversionProperties.java +++ b/src/main/java/com/clearfolio/viewer/config/ConversionProperties.java @@ -44,8 +44,11 @@ public void setBlockedExtensions(Set blockedExtensions) { if (extension == null) { continue; } - String sanitized = extension - .replace("\u0000", "") + String sanitized = extension; + if (sanitized.indexOf('\u0000') != -1) { + sanitized = sanitized.replace("\u0000", ""); + } + sanitized = sanitized .trim() .toLowerCase(Locale.ROOT); if (!sanitized.isEmpty()) { diff --git a/src/main/java/com/clearfolio/viewer/model/ConversionJob.java b/src/main/java/com/clearfolio/viewer/model/ConversionJob.java index c5b553ba..f8b2bc81 100644 --- a/src/main/java/com/clearfolio/viewer/model/ConversionJob.java +++ b/src/main/java/com/clearfolio/viewer/model/ConversionJob.java @@ -122,7 +122,10 @@ private String sanitize(String value) { if (value == null) { return null; } - return value.replace("\u0000", ""); + if (value.indexOf('\u0000') != -1) { + return value.replace("\u0000", ""); + } + return value; } private String normalizeOrDefault(String value, String fallback) { From 2e58f5064a99af8242892ec24e3b173f58d9eda4 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 21 Jul 2026 01:02:44 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94:=20String.replace("\u0000",=20"")=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=20=EC=A0=84=20indexOf('\u0000')=20=EC=82=AC=EC=A0=84?= =?UTF-8?q?=20=EA=B2=80=EC=82=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문자열 정제 과정에서 발생하는 불필요한 객체 할당과 GC 오버헤드를 방지하기 위해 `String.replace()` 호출 전 `indexOf()`를 통한 사전 검사를 추가했습니다. 대상 문자가 존재하지 않는 대부분의 경우(fast path)성능이 개선됩니다. --- .../viewer/config/ArtifactStoreProperties.java | 16 ++-------------- .../viewer/config/ConversionProperties.java | 7 ++----- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/clearfolio/viewer/config/ArtifactStoreProperties.java b/src/main/java/com/clearfolio/viewer/config/ArtifactStoreProperties.java index d9aab2d6..c7c829ae 100644 --- a/src/main/java/com/clearfolio/viewer/config/ArtifactStoreProperties.java +++ b/src/main/java/com/clearfolio/viewer/config/ArtifactStoreProperties.java @@ -62,15 +62,7 @@ public String getRootDir() { * @param rootDir artifact storage root directory */ public void setRootDir(String rootDir) { - if (rootDir == null) { - this.rootDir = DEFAULT_ROOT_DIR; - return; - } - String sanitized = rootDir; - if (sanitized.indexOf('\u0000') != -1) { - sanitized = sanitized.replace("\u0000", ""); - } - sanitized = sanitized.strip(); + String sanitized = rootDir == null ? "" : rootDir.replace("\u0000", "").strip(); this.rootDir = sanitized.isEmpty() ? DEFAULT_ROOT_DIR : sanitized; } @@ -87,10 +79,6 @@ private static String sanitize(String value) { if (value == null) { return ""; } - String sanitized = value; - if (sanitized.indexOf('\u0000') != -1) { - sanitized = sanitized.replace("\u0000", ""); - } - return sanitized.strip().toLowerCase(Locale.ROOT); + return value.replace("\u0000", "").strip().toLowerCase(Locale.ROOT); } } diff --git a/src/main/java/com/clearfolio/viewer/config/ConversionProperties.java b/src/main/java/com/clearfolio/viewer/config/ConversionProperties.java index b29b1661..a86fc982 100644 --- a/src/main/java/com/clearfolio/viewer/config/ConversionProperties.java +++ b/src/main/java/com/clearfolio/viewer/config/ConversionProperties.java @@ -44,11 +44,8 @@ public void setBlockedExtensions(Set blockedExtensions) { if (extension == null) { continue; } - String sanitized = extension; - if (sanitized.indexOf('\u0000') != -1) { - sanitized = sanitized.replace("\u0000", ""); - } - sanitized = sanitized + String sanitized = extension + .replace("\u0000", "") .trim() .toLowerCase(Locale.ROOT); if (!sanitized.isEmpty()) { From d4d837bf0ffb6f5f0b094ade81f6a2e3b684f51b Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:57:31 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81?= =?UTF-8?q?=ED=99=94:=20String.replace("\u0000",=20"")=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=20=EC=A0=84=20indexOf('\u0000')=20=EC=82=AC=EC=A0=84?= =?UTF-8?q?=20=EA=B2=80=EC=82=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문자열 정제 과정에서 발생하는 불필요한 객체 할당과 GC 오버헤드를 방지하기 위해 `String.replace()` 호출 전 `indexOf()`를 통한 사전 검사를 추가했습니다. 대상 문자가 존재하지 않는 대부분의 경우(fast path)성능이 개선됩니다. --- .jules/bolt.md | 3 +++ .../viewer/config/ArtifactStoreProperties.java | 16 ++++++++++++++-- .../viewer/config/ConversionProperties.java | 7 +++++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 9ab69f33..53af0957 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -22,3 +22,6 @@ ## 2026-07-20 - 문자열 치환 시 불필요한 객체 할당 방지를 위한 사전 검사(pre-scan) 적용 **Learning:** `String.replace("\u0000", "")` 와 같은 문자열 치환 메서드를 호출할 때, 대상 문자열에 치환할 문자가 없는 해피 패스(fast path)에서도 내부적으로 정규식 컴파일이나 불필요한 객체 할당 오버헤드가 발생할 수 있습니다. **Action:** `String.replace`를 호출하기 전에 `indexOf()`를 사용하여 치환할 대상이 문자열에 실제로 존재하는지 먼저 확인(pre-scan)합니다. 대상 문자가 존재할 때만 `replace`를 호출하도록 하여 대부분의 정상적인 문자열 처리 시 발생하는 불필요한 메모리 할당 및 GC 압박을 방지해야 합니다. +## 2026-07-20 - 문자열 치환 시 불필요한 객체 할당 방지를 위한 사전 검사(pre-scan) 적용 +**Learning:** `String.replace("\u0000", "")` 와 같은 문자열 치환 메서드를 호출할 때, 대상 문자열에 치환할 문자가 없는 해피 패스(fast path)에서도 내부적으로 정규식 컴파일이나 불필요한 객체 할당 오버헤드가 발생할 수 있습니다. +**Action:** `String.replace`를 호출하기 전에 `indexOf()`를 사용하여 치환할 대상이 문자열에 실제로 존재하는지 먼저 확인(pre-scan)합니다. 대상 문자가 존재할 때만 `replace`를 호출하도록 하여 대부분의 정상적인 문자열 처리 시 발생하는 불필요한 메모리 할당 및 GC 압박을 방지해야 합니다. diff --git a/src/main/java/com/clearfolio/viewer/config/ArtifactStoreProperties.java b/src/main/java/com/clearfolio/viewer/config/ArtifactStoreProperties.java index c7c829ae..d9aab2d6 100644 --- a/src/main/java/com/clearfolio/viewer/config/ArtifactStoreProperties.java +++ b/src/main/java/com/clearfolio/viewer/config/ArtifactStoreProperties.java @@ -62,7 +62,15 @@ public String getRootDir() { * @param rootDir artifact storage root directory */ public void setRootDir(String rootDir) { - String sanitized = rootDir == null ? "" : rootDir.replace("\u0000", "").strip(); + if (rootDir == null) { + this.rootDir = DEFAULT_ROOT_DIR; + return; + } + String sanitized = rootDir; + if (sanitized.indexOf('\u0000') != -1) { + sanitized = sanitized.replace("\u0000", ""); + } + sanitized = sanitized.strip(); this.rootDir = sanitized.isEmpty() ? DEFAULT_ROOT_DIR : sanitized; } @@ -79,6 +87,10 @@ private static String sanitize(String value) { if (value == null) { return ""; } - return value.replace("\u0000", "").strip().toLowerCase(Locale.ROOT); + String sanitized = value; + if (sanitized.indexOf('\u0000') != -1) { + sanitized = sanitized.replace("\u0000", ""); + } + return sanitized.strip().toLowerCase(Locale.ROOT); } } diff --git a/src/main/java/com/clearfolio/viewer/config/ConversionProperties.java b/src/main/java/com/clearfolio/viewer/config/ConversionProperties.java index a86fc982..b29b1661 100644 --- a/src/main/java/com/clearfolio/viewer/config/ConversionProperties.java +++ b/src/main/java/com/clearfolio/viewer/config/ConversionProperties.java @@ -44,8 +44,11 @@ public void setBlockedExtensions(Set blockedExtensions) { if (extension == null) { continue; } - String sanitized = extension - .replace("\u0000", "") + String sanitized = extension; + if (sanitized.indexOf('\u0000') != -1) { + sanitized = sanitized.replace("\u0000", ""); + } + sanitized = sanitized .trim() .toLowerCase(Locale.ROOT); if (!sanitized.isEmpty()) {