From b4d01d9dea2609597512fa819032266bf8f5a341 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:13:55 +0000 Subject: [PATCH 01/10] =?UTF-8?q?=E2=9A=A1=20Bolt:=20String.format?= =?UTF-8?q?=EC=9D=84=20HexFormat=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=ED=95=B4=EC=8B=9C=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/bolt.md | 3 +++ .../service/DefaultDocumentConversionService.java | 11 ++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..6d33a9e --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2026-06-23 - Java 17+ HexFormat 최적화 활용 +**Learning:** 해시 값을 Hex 문자열로 변환할 때, 반복문 내에서 `String.format("%02x", b)`를 사용하는 것은 정규표현식 파싱 및 객체 생성 오버헤드로 인해 성능 저하의 주된 원인이 됩니다. Java 17 이상부터는 `java.util.HexFormat`을 사용하여 이 과정을 매우 빠르게 처리할 수 있습니다. +**Action:** 바이트 배열을 Hex 문자열로 변환할 때는 항상 `HexFormat.of().formatHex(bytes)`를 사용하여 성능과 가비지 컬렉션 부하를 최적화합니다. diff --git a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java index 9c59593..c0882a9 100644 --- a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java +++ b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java @@ -121,12 +121,13 @@ private String contentHash(MultipartFile file) { } byte[] raw = digest.digest(); - StringBuilder hex = new StringBuilder(raw.length * 2); - for (byte b : raw) { - hex.append(String.format("%02x", b)); - } - return hex.toString(); + // ⚡ Bolt: Replace slow String.format loop with HexFormat.of().formatHex() + // 🎯 Why: String.format parses the format string and allocates objects on every invocation, + // which is a known performance bottleneck inside loops. + // 📊 Impact: Significantly faster execution time for hex conversions (often >10x faster) + // and drastically reduced garbage collection overhead. + return java.util.HexFormat.of().formatHex(raw); } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException("SHA-256 digest unavailable", ex); } catch (IOException ex) { From 3c0b9187cada498d80bf1eb820ccf7575e0db109 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:43:27 +0000 Subject: [PATCH 02/10] =?UTF-8?q?=E2=9A=A1=20Bolt:=20String.format?= =?UTF-8?q?=EC=9D=84=20HexFormat=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=ED=95=B4=EC=8B=9C=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81=ED=99=94=20+=20opencode?= =?UTF-8?q?.jsonc=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencode.jsonc | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 opencode.jsonc diff --git a/opencode.jsonc b/opencode.jsonc new file mode 100644 index 0000000..732c46d --- /dev/null +++ b/opencode.jsonc @@ -0,0 +1,33 @@ +{ + "mcp": { + "codegraph": { + "command": "npx", + "args": ["-y", "@sourcegraph/codegraph-mcp", "serve", "--mcp"] + }, + "deepwiki": { + "url": "https://mcp.deepwiki.com/mcp" + }, + "context7": { + "command": "npx", + "args": ["-y", "@upstash/context7-mcp@3.1.0"] + }, + "web_search": { + "command": "npx", + "args": ["-y", "@guhcostan/web-search-mcp@1.0.5"] + } + }, + "models": [ + { + "id": "openai/gpt-5", + "context": 200000, + "output": 100000 + }, + { + "id": "deepseek/deepseek-r1-0528" + }, + { + "id": "deepseek/deepseek-v3-0324" + } + ], + "small_model": "github-models/deepseek/deepseek-v3-0324" +} From 8a55cd8481f8834f9774f04e5588d4a277b693a4 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:00:17 +0000 Subject: [PATCH 03/10] =?UTF-8?q?=E2=9A=A1=20Bolt:=20String.format?= =?UTF-8?q?=EC=9D=84=20HexFormat=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=ED=95=B4=EC=8B=9C=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencode.jsonc | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 opencode.jsonc diff --git a/opencode.jsonc b/opencode.jsonc deleted file mode 100644 index 732c46d..0000000 --- a/opencode.jsonc +++ /dev/null @@ -1,33 +0,0 @@ -{ - "mcp": { - "codegraph": { - "command": "npx", - "args": ["-y", "@sourcegraph/codegraph-mcp", "serve", "--mcp"] - }, - "deepwiki": { - "url": "https://mcp.deepwiki.com/mcp" - }, - "context7": { - "command": "npx", - "args": ["-y", "@upstash/context7-mcp@3.1.0"] - }, - "web_search": { - "command": "npx", - "args": ["-y", "@guhcostan/web-search-mcp@1.0.5"] - } - }, - "models": [ - { - "id": "openai/gpt-5", - "context": 200000, - "output": 100000 - }, - { - "id": "deepseek/deepseek-r1-0528" - }, - { - "id": "deepseek/deepseek-v3-0324" - } - ], - "small_model": "github-models/deepseek/deepseek-v3-0324" -} From 292944ca0857290c7777ac6b31cb6e4ba0eff1b5 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:32:03 +0000 Subject: [PATCH 04/10] =?UTF-8?q?=E2=9A=A1=20Bolt:=20String.format?= =?UTF-8?q?=EC=9D=84=20HexFormat=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=ED=95=B4=EC=8B=9C=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 6033a31a22955a2fc35042919611427486dd65ca Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 23 Jun 2026 19:30:14 +0000 Subject: [PATCH 05/10] =?UTF-8?q?=E2=9A=A1=20Bolt:=20String.format?= =?UTF-8?q?=EC=9D=84=20HexFormat=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=ED=95=B4=EC=8B=9C=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencode.jsonc | 33 +++++++++++++++++++ .../DefaultDocumentConversionService.java | 13 ++++---- 2 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 opencode.jsonc diff --git a/opencode.jsonc b/opencode.jsonc new file mode 100644 index 0000000..732c46d --- /dev/null +++ b/opencode.jsonc @@ -0,0 +1,33 @@ +{ + "mcp": { + "codegraph": { + "command": "npx", + "args": ["-y", "@sourcegraph/codegraph-mcp", "serve", "--mcp"] + }, + "deepwiki": { + "url": "https://mcp.deepwiki.com/mcp" + }, + "context7": { + "command": "npx", + "args": ["-y", "@upstash/context7-mcp@3.1.0"] + }, + "web_search": { + "command": "npx", + "args": ["-y", "@guhcostan/web-search-mcp@1.0.5"] + } + }, + "models": [ + { + "id": "openai/gpt-5", + "context": 200000, + "output": 100000 + }, + { + "id": "deepseek/deepseek-r1-0528" + }, + { + "id": "deepseek/deepseek-v3-0324" + } + ], + "small_model": "github-models/deepseek/deepseek-v3-0324" +} diff --git a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java index c0882a9..094b49b 100644 --- a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java +++ b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java @@ -121,13 +121,12 @@ private String contentHash(MultipartFile file) { } byte[] raw = digest.digest(); - - // ⚡ Bolt: Replace slow String.format loop with HexFormat.of().formatHex() - // 🎯 Why: String.format parses the format string and allocates objects on every invocation, - // which is a known performance bottleneck inside loops. - // 📊 Impact: Significantly faster execution time for hex conversions (often >10x faster) - // and drastically reduced garbage collection overhead. - return java.util.HexFormat.of().formatHex(raw); + // ⚡ Bolt: Replace slow String.format loop with HexFormat.of().formatHex() + // 🎯 Why: String.format parses the format string and allocates objects on every invocation, + // which is a known performance bottleneck inside loops. + // 📊 Impact: Significantly faster execution time for hex conversions (often >10x faster) + // and drastically reduced garbage collection overhead. + return java.util.HexFormat.of().formatHex(raw); } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException("SHA-256 digest unavailable", ex); } catch (IOException ex) { From 18faa5a54f9244f7e871ab0b987449e89c091003 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 23 Jun 2026 20:12:10 +0000 Subject: [PATCH 06/10] =?UTF-8?q?=E2=9A=A1=20Bolt:=20String.format?= =?UTF-8?q?=EC=9D=84=20HexFormat=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=ED=95=B4=EC=8B=9C=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81=ED=99=94=20+=20opencode?= =?UTF-8?q?.jsonc=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- opencode.jsonc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opencode.jsonc b/opencode.jsonc index 732c46d..5cf6dd6 100644 --- a/opencode.jsonc +++ b/opencode.jsonc @@ -2,7 +2,7 @@ "mcp": { "codegraph": { "command": "npx", - "args": ["-y", "@sourcegraph/codegraph-mcp", "serve", "--mcp"] + "args": ["-y", "@colbymchenry/codegraph@0.9.9", "serve", "--mcp"] }, "deepwiki": { "url": "https://mcp.deepwiki.com/mcp" From dddce2755175aa9ffb464a97b3fad27b97f7c02d Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:45:16 +0000 Subject: [PATCH 07/10] =?UTF-8?q?=E2=9A=A1=20Bolt:=20String.format?= =?UTF-8?q?=EC=9D=84=20HexFormat=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=ED=95=B4=EC=8B=9C=20=EB=B3=80=ED=99=98=20?= =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81=ED=99=94=20+=20opencode?= =?UTF-8?q?.jsonc=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From 47ebb8cfbe2c97485e799a0cc8c39438729a3c5d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 29 Jun 2026 02:18:37 +0900 Subject: [PATCH 08/10] Cover content hash hex output --- .../DefaultDocumentConversionService.java | 10 +++---- .../DefaultDocumentConversionServiceTest.java | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java index 094b49b..811e541 100644 --- a/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java +++ b/src/main/java/com/clearfolio/viewer/service/DefaultDocumentConversionService.java @@ -7,6 +7,7 @@ import java.io.InputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -22,6 +23,8 @@ @Service public class DefaultDocumentConversionService implements DocumentConversionService { + private static final HexFormat HEX_FORMAT = HexFormat.of(); + private final ConversionJobRepository repository; private final DocumentValidationService validationService; private final ConversionWorker conversionWorker; @@ -121,12 +124,7 @@ private String contentHash(MultipartFile file) { } byte[] raw = digest.digest(); - // ⚡ Bolt: Replace slow String.format loop with HexFormat.of().formatHex() - // 🎯 Why: String.format parses the format string and allocates objects on every invocation, - // which is a known performance bottleneck inside loops. - // 📊 Impact: Significantly faster execution time for hex conversions (often >10x faster) - // and drastically reduced garbage collection overhead. - return java.util.HexFormat.of().formatHex(raw); + return HEX_FORMAT.formatHex(raw); } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException("SHA-256 digest unavailable", ex); } catch (IOException ex) { diff --git a/src/test/java/com/clearfolio/viewer/service/DefaultDocumentConversionServiceTest.java b/src/test/java/com/clearfolio/viewer/service/DefaultDocumentConversionServiceTest.java index 7d07d6b..6de6ca1 100644 --- a/src/test/java/com/clearfolio/viewer/service/DefaultDocumentConversionServiceTest.java +++ b/src/test/java/com/clearfolio/viewer/service/DefaultDocumentConversionServiceTest.java @@ -174,6 +174,34 @@ void returnsDifferentJobIdsForDifferentPayloads() { assertEquals(2, worker.enqueuedCount()); } + @Test + void storesContentHashAsLowercaseSha256Hex() { + ConversionJobRepository repository = new InMemoryConversionJobRepository(); + RecordingConversionWorker worker = new RecordingConversionWorker(); + DocumentConversionService service = new DefaultDocumentConversionService( + repository, + new DefaultDocumentValidationService(new ConversionProperties()), + worker, + new ConversionProperties() + ); + + byte[] payload = "hello-viewer".getBytes(StandardCharsets.UTF_8); + MockMultipartFile file = new MockMultipartFile( + "file", + "contract.docx", + "application/octet-stream", + payload + ); + + UUID jobId = service.submit(file); + + ConversionJob job = repository.findById(jobId).orElseThrow(); + assertEquals( + "6908f2d58e1a44cd5f7fcc1c76a2a14e801e9158779d569466b4471ae4aae75d", + job.getContentHash() + ); + } + @Test void returnsSameJobForConcurrentDuplicatePayloads() throws Exception { ConversionJobRepository repository = new InMemoryConversionJobRepository(); From 7bfd6c4b54c29a50056b36355ea69efc6a29aa84 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 29 Jun 2026 02:45:53 +0900 Subject: [PATCH 09/10] Align PR metadata with main --- .jules/bolt.md | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md deleted file mode 100644 index 6d33a9e..0000000 --- a/.jules/bolt.md +++ /dev/null @@ -1,3 +0,0 @@ -## 2026-06-23 - Java 17+ HexFormat 최적화 활용 -**Learning:** 해시 값을 Hex 문자열로 변환할 때, 반복문 내에서 `String.format("%02x", b)`를 사용하는 것은 정규표현식 파싱 및 객체 생성 오버헤드로 인해 성능 저하의 주된 원인이 됩니다. Java 17 이상부터는 `java.util.HexFormat`을 사용하여 이 과정을 매우 빠르게 처리할 수 있습니다. -**Action:** 바이트 배열을 Hex 문자열로 변환할 때는 항상 `HexFormat.of().formatHex(bytes)`를 사용하여 성능과 가비지 컬렉션 부하를 최적화합니다. From d316c1e558d8bc6efeff5abeb386117117a2c36e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 29 Jun 2026 02:48:37 +0900 Subject: [PATCH 10/10] Remove PR-local opencode config --- opencode.jsonc | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 opencode.jsonc diff --git a/opencode.jsonc b/opencode.jsonc deleted file mode 100644 index 5cf6dd6..0000000 --- a/opencode.jsonc +++ /dev/null @@ -1,33 +0,0 @@ -{ - "mcp": { - "codegraph": { - "command": "npx", - "args": ["-y", "@colbymchenry/codegraph@0.9.9", "serve", "--mcp"] - }, - "deepwiki": { - "url": "https://mcp.deepwiki.com/mcp" - }, - "context7": { - "command": "npx", - "args": ["-y", "@upstash/context7-mcp@3.1.0"] - }, - "web_search": { - "command": "npx", - "args": ["-y", "@guhcostan/web-search-mcp@1.0.5"] - } - }, - "models": [ - { - "id": "openai/gpt-5", - "context": 200000, - "output": 100000 - }, - { - "id": "deepseek/deepseek-r1-0528" - }, - { - "id": "deepseek/deepseek-v3-0324" - } - ], - "small_model": "github-models/deepseek/deepseek-v3-0324" -}