Skip to content

[Chore/#9] Spotless 도입 및 전체 코드 포맷 적용 - #10

Open
sangrae2325 wants to merge 3 commits into
mainfrom
chore/#9-spotless-setup
Open

[Chore/#9] Spotless 도입 및 전체 코드 포맷 적용#10
sangrae2325 wants to merge 3 commits into
mainfrom
chore/#9-spotless-setup

Conversation

@sangrae2325

Copy link
Copy Markdown
Collaborator

#️⃣연관된 이슈

🎯 해결하려는 문제가 무엇인가요?

저장소에 코드 포맷을 강제하는 장치가 전혀 없습니다.

  • .editorconfig가 없습니다
  • coding-style.md에 들여쓰기·import 순서·줄 길이에 대한 규정이 한 줄도 없습니다
  • 그래서 포맷은 각자 IDE 기본값을 따릅니다

이 상태로 인원이 늘면 "로직은 그대로인데 공백·import 순서만 바뀐 diff"가 PR에 섞여 들어옵니다. 리뷰어가 진짜 변경을 찾느라 시간을 쓰게 되고, 서로 다른 IDE 설정끼리 같은 줄을 번갈아 고치는 충돌도 생깁니다.

❓ 왜 해결해야 하나요?

지금이 가장 싼 시점이기 때문입니다.

  • Java 파일이 34개뿐입니다. 전체 포맷 diff가 22파일 +101/−125로, 리뷰 가능한 규모입니다
  • 현재 열린 PR이 0개입니다. 일괄 포맷이 다른 사람 작업과 충돌할 여지가 없습니다
  • 4팀(core·event·internal·welfare)으로 나뉘어 개발이 본격화되면 파일 수와 동시 작업 브랜치가 동시에 늘어납니다. 그때는 리포맷 diff도 커지고 충돌도 실제로 발생합니다

⭐ 어떻게 해결했나요?

설정 (87bfc20)

gradle/libs.versions.toml에 플러그인을 등록하고, 루트 build.gradle.ktssubprojects {}에 적용했습니다.

extensions.configure<SpotlessExtension> {
    java {
        // AOSP 프로파일 — 들여쓰기 4칸, 한 줄 100자
        googleJavaFormat().aosp()
    }
}

기존 alias(libs.plugins.springBoot) apply false + subprojects {} 공통 설정(Java 21 toolchain, lombok, JUnit) 패턴을 그대로 따랐습니다. 새 패턴을 만들지 않았습니다.

  • 포맷터: google-java-format AOSP — 들여쓰기 4칸, 한 줄 100자. 기본 프로파일은 2칸이라 기존 코드와 차이가 큽니다
  • 적용 범위: 16개 모듈 전체
  • spotlessCheck플러그인이 자동으로 check에 연결합니다. 배선 코드를 따로 쓰지 않았습니다

실행으로 확인한 것

항목 결과
Gradle 9.5.1 + spotless 8.10.0 호환
spotlessCheckcheck 자동 연결 (16개 모듈) check --dry-run으로 확인
JDK 21에서 google-java-format 구동 --add-exports 옵션 불필요
포맷 위반 시 check 실패 ✅ 들여쓰기 한 줄 망가뜨려 BUILD FAILED 확인
.git-blame-ignore-revs 동작 ✅ 적용 시 원저자 복원 확인

게이트 동작 검증BusinessException.java의 들여쓰기를 한 줄 망가뜨리고 ./gradlew :core:common:check를 돌렸습니다.

> Task :core:common:spotlessJavaCheck FAILED
> The following files had format violations:
  Run './gradlew spotlessApply' to fix all violations.
BUILD FAILED

blame 보존 (f06fb1e)

전체 포맷의 실질적 부작용은 git blame이 망가지는 것입니다. 포맷된 모든 줄의 마지막 수정자가 포맷 커밋으로 찍혀서 "이 코드 왜 이렇게 짰지"를 추적할 수 없게 됩니다.

.git-blame-ignore-revs2befe6b를 등록했습니다. GitHub 웹 blame은 이 파일을 자동으로 인식합니다. 로컬 CLI에서도 쓰시려면 각자 한 번만 설정하시면 됩니다.

git config blame.ignoreRevsFile .git-blame-ignore-revs

실제 효과 (SecurityConfig.java 29~34줄):

# 설정 전 — 포맷 커밋이 원저자를 가림
2befe6b3 (sangrae     2026-08-25 29)         return http.csrf(...)

# 설정 후 — 원저자 복원
4c2449d0 (Sumin Hwang 2026-08-18 29)         return http.csrf(...)

🧩 이 PR의 한계 & 트레이드오프

1. CI 워크플로가 없어서 실제로는 강제되지 않습니다 ⚠️

이게 가장 큰 한계입니다.

git-convention.md 3-4절은 "CI 필수 체크: gradle check가 통과해야 Merge할 수 있다"고, 4절은 deploy-dev.yml/deploy-prod.yml을 설명합니다. 그런데 .github/workflows 디렉터리 자체가 없습니다. 문서에 적힌 CI 게이트가 실제로는 존재하지 않습니다.

따라서 이 PR로 spotlessCheckcheck에 물려도, 실제 강제력은 각자 로컬에서 ./gradlew check를 돌릴 때만 생깁니다. PR 단계에서 자동으로 막히지 않습니다.

CI 워크플로 작성을 별도 이슈로 올려야 이 작업이 실효를 갖습니다.

2. Spring Security 체인 가독성이 나빠졌습니다

google-java-format은 fluent 빌더 체인을 잘 다루지 못합니다. SecurityConfig가 직격탄입니다.

// 변경 전 — "경로 → 권한" 쌍이 한 줄
.authorizeHttpRequests(request -> request
    .requestMatchers(PublicEndpoints.allPatterns()).permitAll()
    .requestMatchers("/v1/admin/**").hasAuthority(Role.ADMIN.name())
    .anyRequest().authenticated())

// 변경 후 — 쌍이 갈라지고 들여쓰기가 24칸까지
.authorizeHttpRequests(
        request ->
                request.requestMatchers(PublicEndpoints.allPatterns())
                        .permitAll()
                        .requestMatchers("/v1/admin/**")
                        .hasAuthority(Role.ADMIN.name())
                        .anyRequest()
                        .authenticated())

이건 개선이 아니라 후퇴입니다. 감수하고 가는 트레이드오프입니다 — 아래 "검토한 대안"에 palantir와 실측 비교를 적었습니다.

3. IDE 설정 안내가 필요합니다

google-java-format은 import를 알파벳순 단일 블록으로 정렬하고 4칸/100자를 강제합니다. IntelliJ 기본 설정과 다르면 저장할 때마다 IDE와 spotless가 서로 다른 결과를 만들어 매번 spotlessApply로 되돌리게 됩니다.

머지 전에 각자 IntelliJ에 google-java-format 플러그인을 설치하고 AOSP 스타일로 설정해주셔야 합니다.

4. 이번 범위에서 제외한 것

  • pre-commit 훅 — 팀원 각자 설치해야 하고 커밋이 느려집니다
  • Java 외 파일(yaml/.gradle.kts/md) 포맷 규칙
  • .editorconfig — IDE 플러그인으로 충분한지 먼저 보고 판단하는 게 낫다고 봤습니다

5. 87bfc20 시점에는 check가 실패합니다

포맷터는 깔렸는데 코드는 아직 포맷 전이라 중간 상태가 red입니다. 2befe6b에서 green으로 돌아옵니다. 이 PR이 merge commit으로 머지되면 그 중간 커밋이 main 히스토리에 남아, git bisect가 그 한 커밋에 걸릴 경우 포맷 때문에 빌드가 깨진 것처럼 보일 수 있습니다.

설정과 포맷을 한 커밋으로 합치면 이 문제는 사라지지만, 리뷰어가 "설정 13줄"과 "22파일 리포맷"을 한 diff에서 봐야 합니다. 리뷰 편의를 택했습니다.

⛓️ 기존 기능에 미치는 영향

런타임 동작 변경 없습니다. 포맷 커밋은 공백·줄바꿈 재배치뿐입니다.

  • import 변경 0건git diff -U0 | grep '^[+-]import' 결과가 비어 있습니다. 미사용 import 제거가 일어나지 않아 의존성이 사라질 위험이 없습니다
  • ./gradlew clean build 통과 (93개 태스크)
  • ModularityTests 2건 + StreamServerApplicationTests 1건 통과 — 모듈 경계 verify() 정상
  • 빌드 설정 변경은 spotless 적용뿐이며, architecture.md의 의존 방향이나 Modulith 경계에 영향 없습니다

다른 브랜치와의 충돌 — 현재 열린 PR이 0개라 지금은 문제가 없습니다. 다만 이 PR이 머지된 뒤 파생된 브랜치가 있다면 main을 머지할 때 광범위한 충돌을 겪습니다. 해결법은 아래에 적었습니다.

🔀 Edge Case & 실패 시나리오

상황 동작 대응
포맷 어긋난 코드 커밋 ./gradlew check 실패 (검증 완료) ./gradlew spotlessApply
다른 브랜치에서 main 머지 시 충돌 포맷 커밋과 광범위 충돌 자기 쪽 내용을 살려 충돌 해결 → ./gradlew spotlessApply → 커밋
git blame이 포맷 커밋으로 덮임 GitHub 웹은 자동 제외 로컬은 git config blame.ignoreRevsFile .git-blame-ignore-revs
IDE가 spotless와 다르게 저장 저장할 때마다 포맷이 어긋남 IntelliJ google-java-format 플러그인 설치 (AOSP)
CI에서 포맷 위반 통과 막지 못함 — 워크플로가 없음 별도 이슈로 CI 구축 필요

📋 검토한 대안과 선택 이유

palantir-java-format — 실제로 돌려서 비교했습니다

체인 가독성 문제 때문에 palantir로 전체 포맷을 한 번 돌려 같은 파일을 비교했습니다.

google-java-format AOSP palantir-java-format
변경 파일 22개 20개
변경 라인 +101 / −125 +72 / −107
sessionManagement 2줄로 쪼갬 원본 그대로 한 줄
exceptionHandling 3단 들여쓰기로 재배치 아예 안 건드림
ALL_PATH_PATTERNS 체인 한 줄로 뭉갬 원본 유지
.requestMatchers()/.permitAll() 분리됨 분리됨 (동일)

palantir가 객관적으로 덜 망가뜨립니다. 그럼에도 google-java-format을 선택했습니다:

  • 문제의 핵심인 .requestMatchers(...).permitAll() 쌍 분리는 palantir도 똑같습니다. 체인이 한 줄에 안 들어가면 메서드당 한 줄로 끊는 건 두 포맷터 공통 동작이라, palantir로 바꿔도 SecurityConfig의 핵심 가독성 문제는 해결되지 않습니다
  • google-java-format이 사실상 업계 표준입니다. IntelliJ 플러그인, 문서, 트러블슈팅 자료가 비교가 안 되게 많습니다. 팀에 새로 합류하는 사람이 겪을 마찰이 적습니다

바꾸실 거면 지금이 제일 쌉니다. 팀이 쓰기 시작한 뒤에는 전체 리포맷 커밋을 한 번 더 해야 합니다. 이견 있으시면 이 PR에서 말씀해주세요.

ratchet (ratchetFrom("origin/main")) — 채택하지 않음

매 빌드마다 origin/main 대비 변경된 파일만 검사하는 옵션입니다. 대규모 리포맷 커밋 없이 손대는 파일부터 점진적으로 포맷할 수 있습니다.

  • ratchet의 장점은 "리뷰 불가능한 대규모 diff 회피"인데, 22파일 +101/−125면 그 장점이 없습니다
  • 아무도 안 건드리는 파일은 무기한 미포맷으로 남습니다. 포맷 속도가 "그 파일을 고칠 일이 생기는 속도"에 묶입니다
  • CI에서 shallow clone(actions/checkout 기본 fetch-depth: 1)과 충돌해 빌드가 깨집니다. 나중에 CI 만들 때 fetch-depth: 0을 기억해야 하는 함정이 생깁니다

pre-commit 훅 — 이번엔 제외

spotlessApply를 자동 실행할 수 있지만 팀원 각자 설치해야 하고 커밋이 느려집니다. CI가 먼저 있어야 의미가 있다고 봤습니다.

💬 리뷰 포인트

  • [r] 포맷터 선택 — google-java-format AOSP vs palantir. 위 실측 비교를 보고 판단해주세요. 바꾸려면 지금이 가장 쌉니다
  • [r] SecurityConfig 가독성 후퇴를 감수할 만한가gateway/auth/.../SecurityConfig.javaauthorizeHttpRequests 블록입니다. 이게 받아들이기 어려우면 포맷터 선택을 다시 논의해야 합니다
  • [c] 설정 커밋(87bfc20)의 13줄 — 포맷 커밋은 spotlessApply 출력이라 넘기셔도 됩니다. subprojects {} 배치가 기존 패턴과 맞는지만 봐주세요
  • [c] CI 이슈를 누가 언제 만들지 — 이게 없으면 이 PR은 로컬 규율에 그칩니다
  • [a] .git-blame-ignore-revs 주석 문구

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

코드 포맷 자동화를 위한 Spotless 도입

1 participant