From 740c9edfde748b93f0ab64a39ab4f24425f3f492 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 7 Aug 2026 20:45:57 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20=EB=94=94=EB=A0=89=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EC=B2=98=EB=A6=AC=20=EB=A3=A8=ED=94=84=20=EC=84=B1?= =?UTF-8?q?=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 - `process_ignore_file` 호출 시 매번 생성되던 `defaultSensitiveFiles` 리스트를 `Constants` 객체로 추출하여 불필요한 메모리 할당 제거 - 관련된 큰 정적 문자열과 해시값도 `Constants` 객체로 이동하여 응집도 향상 --- .jules/bolt.md | 3 + src/main/kotlin/html4tree/Constants.kt | 90 ++++++++++++++++++++++++++ src/main/kotlin/html4tree/main.kt | 87 +------------------------ 3 files changed, 96 insertions(+), 84 deletions(-) create mode 100644 src/main/kotlin/html4tree/Constants.kt diff --git a/.jules/bolt.md b/.jules/bolt.md index 19b4c613..38077fed 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -43,3 +43,6 @@ ## 2025-01-24 - 단일 readAttributes 호출로 파일 속성 조회 최적화 **학습:** `isDirectory`, `!it.isDirectory()`, `isSymbolicLink` 3개의 개별적인 파일 시스템 I/O 호출을 수행하면 성능 저하가 큽니다. 이를 단일 `Files.readAttributes` 호출로 변경하여 메타데이터를 한 번에 조회함으로써 I/O 오버헤드를 대폭 줄일 수 있음을 확인했습니다. **조치:** 디렉토리 순회 시 파일의 여러 속성을 확인할 때는 개별적인 stat 호출보다 `Files.readAttributes`를 사용하여 필요한 모든 속성을 한 번에 가져오는 방식을 우선적으로 고려해야 합니다. +## 2026-08-07 - 디렉토리 처리 루프 성능 최적화 +**Learning:** `process_ignore_file` 함수가 호출될 때마다 `defaultSensitiveFiles` 리스트가 새로 생성되는 불필요한 메모리 할당이 발생함. Kotlin의 top-level `val` 프로퍼티(`CSS_CONTENT`, `STYLE_HASH`)는 이미 한 번만 초기화되지만, `private object Constants` 내부에 모아두어 성능과 응집도를 최적화함. +**Action:** 큰 정적 문자열, 결정론적 연산 결과, 정적 리스트를 루프 밖의 `private object Constants`에 위치시켜 재사용함으로써 메모리 할당과 CPU 오버헤드를 감소시킴. diff --git a/src/main/kotlin/html4tree/Constants.kt b/src/main/kotlin/html4tree/Constants.kt new file mode 100644 index 00000000..df538152 --- /dev/null +++ b/src/main/kotlin/html4tree/Constants.kt @@ -0,0 +1,90 @@ +package html4tree + +import java.security.MessageDigest +import java.util.Base64 + +internal object Constants { + const val CSS_CONTENT = """body { + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + line-height: 1.5; + padding: 1rem; + color: #1f2328; +} +main { + max-width: 800px; + margin: 0 auto; +} +ul { + list-style-type: none; + padding-left: 0; +} +a.dir-link { + display: flex; + align-items: flex-start; + gap: 0.5rem; + width: 100%; + overflow-wrap: anywhere; + box-sizing: border-box; +} +.icon { + flex-shrink: 0; + width: 1.25rem; + text-align: center; +} +a { + padding: 0.5rem; + text-decoration: none; + color: #0969da; + border-radius: 4px; + transition: background-color 0.2s ease, outline-color 0.2s ease; +} +a:hover, a:focus-visible { + background-color: #f6f8fa; + outline: 2px solid #0969da; + outline-offset: -2px; +} +a:hover span:last-child, a:focus-visible span:last-child { + text-decoration: underline; +} +@media (prefers-reduced-motion: reduce) { + a { + transition: none; + } +} +li + li { + border-top: 1px solid #d0d7de; +} +.empty-dir { + display: flex; + align-items: flex-start; + gap: 0.5rem; + padding: 0.5rem; + color: #656d76; + font-style: italic; +} +@media (prefers-color-scheme: dark) { + body { + background-color: #0d1117; + color: #c9d1d9; + } + a { + color: #58a6ff; + } + a:hover, a:focus-visible { + background-color: #161b22; + outline-color: #58a6ff; + } + li + li { + border-top-color: #21262d; + } + .empty-dir { + color: #8b949e; + } +}""" + + @JvmField + val STYLE_HASH = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(CSS_CONTENT.toByteArray(Charsets.UTF_8))) + + @JvmField + val DEFAULT_SENSITIVE_FILES = listOf(".git", ".env", ".ssh", ".htpasswd", ".htaccess", "id_rsa", "id_ed25519", "secrets.yml", ".html4ignore", ".DS_Store", ".aws", ".kube", ".npmrc", ".gnupg", "config.json", "credentials.json") +} diff --git a/src/main/kotlin/html4tree/main.kt b/src/main/kotlin/html4tree/main.kt index f52a1468..c872ee17 100644 --- a/src/main/kotlin/html4tree/main.kt +++ b/src/main/kotlin/html4tree/main.kt @@ -13,87 +13,6 @@ import com.github.ajalt.clikt.parameters.options.default import com.github.ajalt.clikt.parameters.arguments.argument import com.github.ajalt.clikt.parameters.types.int -private val CSS_CONTENT = """ -body { - font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - line-height: 1.5; - padding: 1rem; - color: #1f2328; -} -main { - max-width: 800px; - margin: 0 auto; -} -ul { - list-style-type: none; - padding-left: 0; -} -a.dir-link { - display: flex; - align-items: flex-start; - gap: 0.5rem; - width: 100%; - overflow-wrap: anywhere; - box-sizing: border-box; -} -.icon { - flex-shrink: 0; - width: 1.25rem; - text-align: center; -} -a { - padding: 0.5rem; - text-decoration: none; - color: #0969da; - border-radius: 4px; - transition: background-color 0.2s ease, outline-color 0.2s ease; -} -a:hover, a:focus-visible { - background-color: #f6f8fa; - outline: 2px solid #0969da; - outline-offset: -2px; -} -a:hover span:last-child, a:focus-visible span:last-child { - text-decoration: underline; -} -@media (prefers-reduced-motion: reduce) { - a { - transition: none; - } -} -li + li { - border-top: 1px solid #d0d7de; -} -.empty-dir { - display: flex; - align-items: flex-start; - gap: 0.5rem; - padding: 0.5rem; - color: #656d76; - font-style: italic; -} -@media (prefers-color-scheme: dark) { - body { - background-color: #0d1117; - color: #c9d1d9; - } - a { - color: #58a6ff; - } - a:hover, a:focus-visible { - background-color: #161b22; - outline-color: #58a6ff; - } - li + li { - border-top-color: #21262d; - } - .empty-dir { - color: #8b949e; - } -} -""".trimIndent() - -private val STYLE_HASH = "sha256-" + Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-256").digest(CSS_CONTENT.toByteArray(Charsets.UTF_8))) class Html4tree : CliktCommand() { val maxLevel:Int by option(help="Number of levels deep for which to generate an index.html file", hidden = false).int().default(-1) @@ -298,7 +217,7 @@ fun process_ignore_file(curr_dir: File, dirFilesNames: Array? = null): S files_to_exclude.add("index.html") // 보안 향상: 민감한 시스템, 설정, 시크릿 파일을 디렉토리 목록에서 기본적으로 제외하여 정보 노출(Information Exposure) 방지 - val defaultSensitiveFiles = listOf(".git", ".env", ".ssh", ".htpasswd", ".htaccess", "id_rsa", "id_ed25519", "secrets.yml", ".html4ignore", ".DS_Store", ".aws", ".kube", ".npmrc", ".gnupg", "config.json", "credentials.json") + val defaultSensitiveFiles = Constants.DEFAULT_SENSITIVE_FILES files_to_exclude.addAll(defaultSensitiveFiles) // 보안 향상: .env, .git 등 민감한 정보가 포함될 수 있는 숨김 파일(.으로 시작하는 모든 항목)을 기본적으로 노출하지 않도록 제외 (정보 노출 방지) @@ -333,11 +252,11 @@ fun process_dir(curr_dir: File, excludeSet: Set? = null, dirFiles: Array - + ${curr_dir.getName().escapeHtml()} - +