포매터 신경 더 이상 쓰지 않아도 되는, Spotless Git Hook pre commit 적용하기 - DDD-Community/DDD-12-MOYORAK-API GitHub Wiki

1. Spotless란?

Java나 Groovy 등 여러 언어의 코드 스타일을 자동으로 포매팅해 주는 Gradle 플러그인 입니다.
(🔗 Github : https://github.com/diffplug/spotless)

Spotless의 장점은 아래와 같습니다.

  • 일관 된 코드 기준을 코드로 명시할 수 있습니다.
  • 개발자마다 다른 포매터 설정이나, IDE 스타일이 달라 발생하는 불필요한 커뮤니케이션 비용을 줄일 수 있습니다.
  • editorconfig는 IDE 설정일 뿐, 직접 코드를 수정해 주지는 않으나 Spotless는 코드를 직접 수정을 해준다는 차이점이 있습니다.

이를 통해 여러 명이 협업할 때, 일관된 코드 스타일을 유지하는 데 유용하게 활용할 수 있습니다.

2. 적용하기

2-1. 플러그인 등록

plugins {
    id("com.diffplug.spotless") version "7.0.3"
}

2-2. 옵션

Spotless는 다양한 포매터와 여러 기능을 지원하고 있습니다.
대표적으로 많이 사용되거나, 유용한 항목들에 대해 아래와 같이 정리해 보았습니다.

포매터

어떤 스타일의 포매터를 사용할 것인지 명시합니다.

  • googlejavaformat("버전")
    • 구글에서 제공하는 Java 코드 포매터
    • 버전 명시하면 고정되며, 생략 시 최신 버전으로 적용
    • .aosp() 옵션으로 안드로이드 스타일 적용 가능
  • eclipse
    • Eclipse IDE의 코드 포맷 설정을 Gradle에서도 사용
    • configFile을 통해서 직접 .xml 파일 지정 가능

import 관련

  • importOrder(...)
    • 패키지 상단에 import를 특정 순서대로 지정
  • removeUnusedImports
    • 사용하지 않는 import 제거

공백 / 줄 바꿈

  • trimTrailingWhitespace
    • 줄 끝 공백 제거
  • endWithNewline
    • 파일 끝에 개행 추가 (POSIX 표준 맞춤)

라이센스 헤더 추가

  • licenseHeader("문자")
  • licenseHeaderFile("파일 경로") 파일 상단에 공통 라이선스 문구 삽입 가능

커스텀 포매터

  • custom("이름", "정규식", "치환 문자열")
  • toggleOffOn()
    • 주석으로 포맷팅 적용/비적용 범위 설정 가능
public class Sample {

    // spotless:off
    public void messyMethod(){
        System.out.println(   "해당 부분 포맷팅 ❌"   );
    }
    
    // spotless:on
    public void cleanMethod() {
        System.out.println("해당 부분 포맷팅 ✅”");
    }
}

기타 파일 포매팅 (yaml, json 등)

format("json") {
    target("**/*.json")
    prettier().config(mapOf("tabWidth" to 2, "useTabs" to false))
}

format("yaml") {
    target("**/*.yml")
    trimTrailingWhitespace()
    endWithNewline()
}

실제 적용 버전

configure<SpotlessExtension> {  
    java {  
        googleJavaFormat().aosp()  
  
        trimTrailingWhitespace()  
        endWithNewline()  
        removeUnusedImports()  
    }  
}

현재 위와 같이 설정하여 사용 중이며, 혹시 더 좋은 방식이 있다면 공유 주세요. 😁

2-3. 실행해 보기

설정된 Spotless는 아래 명령어로 실행할 수 있습니다.

./gradlew spotlessApply`

해당 명령어를 실행하면, 설정한 포매터 규칙에 따라 코드가 자동 정리가 됩니다.

실행 전 코드

image

Spotless 실행 후 코드

image

단, 이 작업은 개발자가 직접 수동으로 명령어를 실행해야만 적용됩니다.

CI 단계에서 포매팅 검사하기

포매팅 체크를 하는 명령어는 아래와 같습니다.

./gradlew spotlessCheck

포매팅 체크에 실패한다면,아래와 같이 오류 메세지가 출력됩니다. 🚨

> spotless: Format violations were found. Run 'spotlessApply' to fix.

이처럼 spotlessCheck를 이용해, CI 파이프라인에 포함 시킨다면,
Spotless를 통해 정리가 된 코드만 통과될 수 있도록 강제할 수도 있습니다.

3. 자동화하기

매번 직접 명령어를 실행하는 방식보단, 커밋할 때마다 자동으로 실행된다면 좋지 않을까? 생각이 들었습니다.
그렇다면 정리되지 않은 코드가 커밋되는 것을 사전에 방지할 수 있게 됩니다.

이러한 자동화를 구성하기 위해 GitHook을 활용하기로 하였습니다.

3-1. 자동화 구조 설계

GitHook이 실행되기 위해서는 스크립트 파일이 .git/hooks디렉토리에 존재해야 합니다.
하지만, 해당 디렉터리는 형상 관리 대상이 아니기 때문에 직접 옮겨줘야 하고
이 작업 역시 자동으로 풀어보고자 합니다.

  1. 형상 관리 범위의 위치(`/githooks)에서 pre-commit 스크립트를 작성합니다.
  2. pre-commit.git/hooks로 복사해 주는 Gradle task를 생성합니다.
  3. clean 명령어에 만든 task를 의존시켜, 프로젝트 최초 설정 시 자동으로 적용되게 합니다.

3-2. pre-commit sh 작성

./githooks/pre-commit 생성

#!/bin/sh  
set -e  
  
echo "▶ 스테이지된 Java 파일만 spotlessApply 실행 중..."  
  
# 1. 현재 스테이지에 올라간 파일 목록만 추출  
files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.java$' || true)  
  
if [ -z "$files" ]; then  
  echo "✅ 포맷할 Java 파일이 없습니다. 커밋을 진행합니다."  
  exit 0  
fi  
  
# 2. Spotless는 지정된 파일만 직접 지정 불가 → 대신 전체 실행 후, 수정된 파일 중 대상만 git add./gradlew spotlessApply --quiet  
  
echo "▶ 다시 git add 중..."  
echo "$files" | xargs git add  
  
echo "▶ spotlessCheck 실행 중..."  
./gradlew spotlessCheck --quiet  
  
echo "✅ 포맷 완료. 커밋 진행 가능!"
  • files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.java$' || true)
    • 프로젝트 전체 코드를 대상으로 포매팅을 하기 때문에, 스테이지 파일만 조건으로 주고자 작성

⚠️ 주의할 점

  • 스테이지에 올린 파일만 커밋하더라도, 스테이지에 올라가지 않은 파일까지 함께 포매팅 되게 됩니다. image

위와 같은 상황에서 두 파일 모두 포매팅이 됩니다.

3-3. ./git/hooks로 이동시키는 task 생성

build.gradle.kts

tasks.register<Copy>("initGitHooks") {  
    from(file("$rootDir/githooks")) {  
        include("pre-commit")  
        rename("pre-commit", "pre-commit")  
    }  
  
    into(file("$rootDir/.git/hooks"))  
  
    doLast {  
        val hookFile = file("$rootDir/.git/hooks/pre-commit")  
        if (hookFile.exists()) {  
            hookFile.setExecutable(true)  
        }  
    }  
}
설명
단계 내용
register<Copy> initGitHooks라는 이름의 복사 task 생성
from(...) 복사할 원본 파일 지정
into(...) 복사 대상 지정
doLast 복사된 후, 실행 권한(chmod +x) 부여

새로 등록된 task는 아래와 같이 확인 가능합니다.
image

3-4. task 실행 자동화하기

대부분의 프로젝트 초기화를 할 때, .gradlew clean을 실행하므로,
clean작업 전에 만들어놓은 initGitHooks task가 자동으로 실행되도록 의존성을 설정합니다.

tasks.named("clean") {  
    dependsOn("initGitHooks")  
}

이렇게 설정해 두면, 프로젝트를 처음 설정하는 개발자라도 별도의 작업 없이
clean 명령만으로 GitHook이 자동으로 구성됩니다.
즉, Spotless 설정을 별도로 인지하지 못하더라도 자동으로 적용될 수 있는 구조가 됩니다.

clean 실행 후 결과

image

⚠️ 추가 고려 사항

  • /githookspre-commit 스크립트가 변경될 때 자동으로 ./git/hooks까지 반영되지 않습니다.
  • 해당 부분 역시 자동화를 고민해 볼 수 있지 않을까 싶습니다.
⚠️ **GitHub.com Fallback** ⚠️