diff --git a/.jules/bolt.md b/.jules/bolt.md index 284d180..c3aa752 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-17 - HexFormat 재사용 최적화 +**Learning:** `java.util.HexFormat.of()`를 매번 호출하는 것은 불필요한 객체 할당을 발생시킵니다. +**Action:** `private static final java.util.HexFormat HEX_FORMAT = java.util.HexFormat.of();` 로 선언하고 재사용해야 합니다. diff --git a/src/main/java/com/clearfolio/viewer/artifact/FileSystemArtifactStore.java b/src/main/java/com/clearfolio/viewer/artifact/FileSystemArtifactStore.java index fe69737..8770c4e 100644 --- a/src/main/java/com/clearfolio/viewer/artifact/FileSystemArtifactStore.java +++ b/src/main/java/com/clearfolio/viewer/artifact/FileSystemArtifactStore.java @@ -35,6 +35,8 @@ interface BytesReader { private static final String PDF_SUFFIX = ".pdf"; private static final String METADATA_SUFFIX = ".meta.properties"; + private static final java.util.HexFormat HEX_FORMAT = java.util.HexFormat.of(); + private final Path rootDir; private final BytesWriter bytesWriter; private final BytesReader bytesReader; @@ -134,7 +136,7 @@ private static String sha256Hex(final byte[] bytes) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] raw = digest.digest(bytes); - return java.util.HexFormat.of().formatHex(raw); + return HEX_FORMAT.formatHex(raw); } catch (NoSuchAlgorithmException ex) { throw new IllegalStateException("SHA-256 digest unavailable", ex); } diff --git a/src/main/java/com/clearfolio/viewer/controller/ConversionController.java b/src/main/java/com/clearfolio/viewer/controller/ConversionController.java index 604edb5..daecfc8 100644 --- a/src/main/java/com/clearfolio/viewer/controller/ConversionController.java +++ b/src/main/java/com/clearfolio/viewer/controller/ConversionController.java @@ -55,6 +55,8 @@ public class ConversionController { */ public static final String OPERATOR_ID_HEADER = "X-Clearfolio-Operator-Id"; + private static final java.util.HexFormat HEX_FORMAT = java.util.HexFormat.of(); + private final DocumentConversionService conversionService; private final TenantAccessService tenantAccessService; private final ArtifactLinkService artifactLinkService; @@ -294,9 +296,9 @@ private String calculateSha256(final byte[] data) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hash = digest.digest(data); - // Optimization: java.util.HexFormat.of().formatHex() is faster + // Optimization: HexFormat.formatHex() is faster // and allocates less memory than String.format. - return java.util.HexFormat.of().formatHex(hash); + return HEX_FORMAT.formatHex(hash); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("SHA-256 algorithm not available", e); } diff --git a/src/test/java/com/clearfolio/viewer/controller/ConversionControllerMultipartLimitTest.java b/src/test/java/com/clearfolio/viewer/controller/ConversionControllerMultipartLimitTest.java index ff77627..5241d90 100644 --- a/src/test/java/com/clearfolio/viewer/controller/ConversionControllerMultipartLimitTest.java +++ b/src/test/java/com/clearfolio/viewer/controller/ConversionControllerMultipartLimitTest.java @@ -51,6 +51,8 @@ ) class ConversionControllerMultipartLimitTest { + private static final HexFormat HEX_FORMAT = HexFormat.of(); + @SpringBootConfiguration @EnableAutoConfiguration @EnableConfigurationProperties(ConversionProperties.class) @@ -149,7 +151,7 @@ private String generateSignature(String approverId, String extension, String sec mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); String payload = approverId.length() + ":" + approverId + extension; byte[] hashed = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); - return HexFormat.of().formatHex(hashed); + return HEX_FORMAT.formatHex(hashed); } catch (Exception ex) { throw new RuntimeException(ex); } diff --git a/src/test/java/com/clearfolio/viewer/service/DefaultDocumentValidationServiceTest.java b/src/test/java/com/clearfolio/viewer/service/DefaultDocumentValidationServiceTest.java index 4f27bdc..3e8b8e8 100644 --- a/src/test/java/com/clearfolio/viewer/service/DefaultDocumentValidationServiceTest.java +++ b/src/test/java/com/clearfolio/viewer/service/DefaultDocumentValidationServiceTest.java @@ -26,6 +26,8 @@ class DefaultDocumentValidationServiceTest { + private static final HexFormat HEX_FORMAT = HexFormat.of(); + @Test void sanitizeFilenameReturnsNullWhenFilenameIsNull() throws Exception { ConversionProperties conversionProperties = new ConversionProperties(); @@ -89,7 +91,7 @@ private String generateSignature(String approverId, String extension, String sec mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); String payload = approverId.length() + ":" + approverId + extension; byte[] hashed = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)); - return HexFormat.of().formatHex(hashed); + return HEX_FORMAT.formatHex(hashed); } catch (Exception ex) { throw new RuntimeException(ex); }