Kotlin 프로젝트에서 팀 전체의 코드 품질과 커밋 규칙을 자동으로 통일하는 방법

이 글을 쓰게 된 계기부터 이야기하려고 한다.
회사에서 같은 팀원분과 함께 Kotlin 스터디를 하고 있다. 그중에 프론트엔드 한 분이 계신데, 그분이 Kotlin 코드를 처음 작성하면서 자연스럽게 물어본 질문이 있었다. "Kotlin에는 ESLint 같은 거 없어?" JavaScript나 TypeScript 생태계에서 ESLint는 코드 스타일 검사와 자동 수정을 담당하는 도구인데, 프론트엔드 개발을 해본 사람이라면 프로젝트 초기 세팅에 당연히 포함시키는 도구다. 그 질문이 꽂혔다. Kotlin에도 당연히 비슷한 도구가 있는데, 정작 우리 회사 내부에서는 이런 기본 설정이 프로젝트마다 통일되어 있지 않았다.
그때 든 생각은 이런 거였다. 만약 사내에서 Kotlin 프로젝트를 시작할 때마다 코드 스타일 검사, 커밋 규칙 강제, 자동 포맷팅 같은 기본 설정이 이미 갖춰진 템플릿이 있다면, 새로 합류하는 사람이 프론트엔드 출신이든 백엔드 출신이든 상관없이 같은 기준선에서 출발할 수 있겠다는 거다.

나는 팀원의 공수를 줄여주는 일에 재미를 느끼는 편이다. 단순히 "일을 덜 하게 해준다"는 의미가 아니다. 코드 스타일이나 커밋 규칙 같은 것에 머리를 쓰지 않아도 되는 환경을 만들어주면, 그만큼 비워진 인지적 여유가 진짜 중요한 곳에 쓰인다고 생각한다. 예를 들어 도메인 로직을 어떻게 설계할지, 이 기능의 엣지 케이스가 뭐가 있을지, 사용자 경험을 어떻게 개선할지 같은 본질적인 고민에 에너지를 쏟을 수 있게 된다.
여기에는 하나 더 중요한 전제가 있다. 같은 핏, 같은 사고방식을 공유하는 팀원들이 동일한 출발선에 서 있어야 한다는 거다. 서로 다른 코드 스타일 규칙을 쓰고, 커밋 메시지 형식이 각자 다르고, 개발 환경 설정이 제각각인 상태에서는 아무리 뛰어난 사람들이 모여 있어도 브레인스토밍의 효과가 떨어진다. 왜냐하면 커뮤니케이션에 불필요한 비용이 발생하기 때문이다. "여기 왜 이렇게 포맷팅했어?", "이 커밋 뭘 고친 거야?", "내 로컬에서는 빌드 되는데?" 같은 대화는 전부 코드 품질이나 설계와는 무관한 노이즈다.

코드를 작성하는 규칙이 통일되어 있고, 커밋 메시지를 보면 무슨 변경인지 바로 알 수 있고, 누구의 로컬에서든 같은 검사가 같은 기준으로 돌아가는 환경이 갖춰져 있으면, 팀원 간의 소통에서 "형식"에 대한 마찰이 사라진다. 그래야 비로소 "내용"에 대한 소통이 시작된다. 도메인 지식을 가진 사람들이 서로의 관점을 부딪히면서 더 나은 설계를 만들어내는 그 과정이, 형식적인 잡음 없이 깨끗하게 일어날 수 있게 된다.
그래서 이 글을 쓴다. Kotlin 프로젝트에서 ktlint, EditorConfig, Git Hooks 세 가지를 조합해서, 코드 스타일 위반이나 규칙에 맞지 않는 커밋이 아예 리포지토리에 들어올 수 없도록 막는 구조를 만드는 방법이다. 사람의 의지가 아니라 자동화된 도구로 품질을 보장하는 방식이다.
실제로 운영 중인 멀티 모듈 Kotlin Spring Boot 프로젝트(Gradle 기반)의 설정을 그대로 기반으로 작성했다.
이 설정이 해결하려는 문제 세 가지
: 첫 번째 문제는 코드 스타일이 사람마다 다르다는 점.
같은 팀에서 같은 프로젝트를 하고 있는데, 한 사람은 함수 파라미터를 한 줄에 몰아쓰고, 다른 사람은 줄마다 하나씩 나눠서 쓴다. 둘 다 동작하는 코드이고, 둘 다 나름의 근거가 있다. 문제는 이 차이가 Git diff에 잡힌다는 거다. 실제로 변경된 로직은 한 줄인데, 포맷팅 차이 때문에 diff가 20줄이 되기도 한다. 리뷰하는 사람 입장에서는 진짜 변경 사항을 찾아내느라 시간을 쓰게 되고, 이건 순수한 낭비다.
// 한 사람의 스타일
fun createUser(name:String,age:Int,email:String){
// ...
}
// 다른 사람의 스타일
fun createUser(
name: String,
age: Int,
email: String,
) {
// ...
}
: 두 번째 문제는 커밋 메시지에 규칙이 없다는 점.
프로젝트 초기에는 별로 느끼지 못하지만, 코드가 쌓이고 시간이 지나면 git log가 프로젝트의 변경 이력을 추적하는 유일한 수단이 된다. 그런데 커밋 메시지가 수정함이면, 3개월 뒤에 그 커밋을 보는 사람은 (본인 포함) 대체 뭘 수정한 건지 알 수가 없다. 더 큰 문제는 자동화 도구와의 호환성이다. Conventional Commits 같은 규칙을 따르면 릴리즈 노트 자동 생성, 시맨틱 버저닝 자동화 같은 도구를 바로 연결할 수 있는데, 규칙이 없으면 이 모든 게 불가능해진다.
# 이런 히스토리로는 아무것도 추적할 수 없다
abc123 수정함
def456 버그 고침
ghi789 업데이트
# 이런 히스토리라면 무엇이 언제 왜 바뀌었는지 바로 보인다
abc123 feat(auth): JWT 기반 사용자 인증 추가
def456 fix(payment): 결제 처리 중 동시성 문제 해결
ghi789 docs(api): API 엔드포인트 문서 업데이트
: 세 번째 문제는 품질 검사를 "각자 알아서" 해야 한다는 점.
테스트를 안 돌리고 커밋한다거나, 린트 검사를 건너뛰고 푸시한다거나. 본인은 괜찮다고 생각했는데, CI/CD 파이프라인에서 빌드가 깨지면 그 영향은 팀 전체로 번진다. 누군가 한 명이 실수하면 다른 팀원들의 배포가 전부 막히는 구조가 되는 거다.
이 세 가지 문제의 공통점은, 전부 개인의 의지에 의존한다는 점이다. "조심하자", "규칙을 지키자"로는 해결되지 않는다. 사람은 실수하니까. 그래서 도구로 강제하는 게 맞다고 생각한다.
전체 구조 이해하기
이 설정이 만들어내는 구조는 이렇다.
프로젝트 루트/
+-- .editorconfig # 코드 스타일 규칙 정의
+-- .githooks/ # Git 이벤트에 연결되는 스크립트
| +-- pre-commit # 커밋 직전에 ktlint 검사 실행
+-- Makefile # 초기 설정 자동화
+-- build.gradle.kts # ktlint Gradle 플러그인 설정
각 파일의 역할을 정리하면 이렇다.
.editorconfig는 코드가 어떤 모양이어야 하는지를 정의한다. IDE가 IntelliJ든 VS Code든 상관없이, 이 파일에 적힌 규칙대로 코드 포맷이 통일된다.
.githooks/pre-commit은 규칙을 어긴 코드가 커밋되지 못하게 막는 역할을 한다. git commit 명령을 실행하면, 실제 커밋이 만들어지기 전에 이 스크립트가 먼저 실행된다. 여기서 ktlint 검사를 돌려서, 스타일 위반이 있으면 커밋 자체를 차단한다.
Makefile은 이 모든 설정을 "make init" 한 줄로 끝내기 위한 자동화 도구다. 새 팀원이 프로젝트를 클론받고 make init만 치면, Git hooks가 자동으로 연결된다.
build.gradle.kts에는 ktlint Gradle 플러그인이 설정되어 있다. 이 플러그인이 있어야 ./gradlew ktlintCheck 명령으로 코드 스타일 검사를 실행할 수 있고, ./gradlew ktlintFormat 명령으로 자동 수정도 가능해진다.
이 네 가지가 맞물려서, "코드를 작성하고 -> 커밋하려고 하면 -> 자동으로 스타일 검사가 돌고 -> 통과해야만 커밋이 완료되는" 흐름이 만들어진다.
EditorConfig 설정하기
EditorConfig는 프로젝트 루트에 .editorconfig 파일을 두면, 대부분의 IDE와 에디터가 이 파일의 규칙을 자동으로 읽어서 적용하는 표준이다. IntelliJ IDEA는 별도 플러그인 없이도 이 파일을 인식한다.
실제로 사용 중인 설정은 이렇다.
root = true
[*.{kt,kts}]
max_line_length=130
insert_final_newline = true
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
ij_kotlin_packages_to_use_import_on_demand = unset
ktlint_code_style = INTELLIJ_IDEA
ktlint_standard_package-name = disabled
ktlint_standard_function-signature = disabled
ktlint_standard_import-ordering = disabled
ktlint_standard_indent = disabled
[*Test.kt]
max_line_length = off
각 설정이 왜 이렇게 되어 있는지 하나씩 살펴보겠다.
max_line_length=130은 한 줄의 최대 글자 수를 130자로 제한한다는 의미다. 기본값인 80자는 요즘 모니터 해상도를 생각하면 너무 짧고, 제한이 없으면 가로 스크롤이 필요한 코드가 나올 수 있다. 130자는 와이드 모니터가 아니어도 분할 화면에서 편하게 읽을 수 있는 길이다.
insert_final_newline = true는 파일 마지막에 빈 줄을 하나 추가하라는 규칙이다. 이건 POSIX 표준에서 "텍스트 파일은 줄바꿈 문자로 끝나야 한다"고 정의하고 있는 것에서 비롯된 관례인데, 실질적인 이유도 있다. 파일 끝에 줄바꿈이 없으면 git diff에서 "No newline at end of file"이라는 불필요한 경고가 뜬다. 이걸 없애려면 그냥 줄바꿈을 넣는 게 낫다.
ij_kotlin_allow_trailing_comma = true는 "마지막 요소 뒤에 쉼표를 허용한다"는 설정이다. 이게 왜 중요한지 예를 들어 보겠다.
// trailing comma가 없는 경우, 새 파라미터를 추가하면
fun createUser(
name: String,
age: Int,
- email: String // 이 줄도 변경됨 (쉼표 추가)
+ email: String, // 쉼표가 추가되어야 하므로
+ phone: String // 실제 추가된 줄
)
// trailing comma가 있는 경우
fun createUser(
name: String,
age: Int,
email: String, // 이 줄은 변경되지 않음
+ phone: String, // 추가된 줄만 diff에 잡힘
)
trailing comma가 없으면 파라미터를 하나 추가했을 뿐인데 git diff에 두 줄이 변경된 것으로 잡힌다. trailing comma를 허용하면 실제로 추가된 줄만 diff에 나타나서, 변경 사항을 더 명확하게 파악할 수 있다.
ij_kotlin_name_count_to_use_star_import = 2147483647은 star import(와일드카드 import)를 사실상 금지하는 설정이다. 2147483647은 Integer의 최대값이라서, "같은 패키지에서 21억 개 이상의 클래스를 import할 때만 star import를 쓰겠다"는 뜻이 된다. 현실적으로는 "절대 쓰지 않겠다"와 같다.
star import를 금지하는 이유는 명확하다. import java.util.*이라고 적혀 있으면 실제로 이 파일에서 java.util 패키지의 어떤 클래스를 쓰는지 한눈에 알 수 없다. 반면 import java.util.List, import java.util.Map처럼 명시적으로 적으면 이 파일이 어떤 외부 클래스에 의존하는지 import 구문만 봐도 바로 파악된다.
ktlint_code_style = INTELLIJ_IDEA는 ktlint가 코드 스타일을 검사할 때 IntelliJ IDEA의 기본 포맷팅 규칙을 기준으로 삼겠다는 설정이다. 대부분의 Kotlin 개발자가 IntelliJ IDEA를 쓰기 때문에, IDE에서 자동 포맷팅한 결과와 ktlint 검사 결과가 일치하게 된다. 즉, IDE에서 Ctrl+Alt+L(Windows) 또는 Cmd+Option+L(Mac)로 포맷팅하면 그게 곧 ktlint를 통과하는 코드가 된다.
ktlint_standard_package-name = disabled 같은 설정들은 ktlint의 특정 규칙을 비활성화한 것이다. 프로젝트의 기존 코드 구조와 충돌하거나, 팀 내에서 해당 규칙이 오히려 생산성을 떨어뜨린다고 판단한 경우에 이렇게 개별 규칙을 끌 수 있다. 모든 규칙을 맹목적으로 따르는 것보다, 팀 상황에 맞게 조율하는 게 더 합리적이다.
[*Test.kt] 섹션의 max_line_length = off는 테스트 파일에서는 줄 길이 제한을 해제하겠다는 뜻이다. 테스트 코드는 길고 서술적인 함수명을 쓰는 경우가 많고, 테스트 데이터를 한 줄에 나열하는 게 가독성에 더 유리한 경우도 많다. 예를 들어 "사용자가 유효하지 않은 이메일로 가입하면 예외가 발생해야 한다" 같은 테스트 함수명을 130자에 맞추려고 줄을 나누면 오히려 읽기 어려워진다.
Git Hooks로 커밋 시점에 자동 검사 걸기
Git Hooks는 Git이 특정 이벤트를 처리할 때 자동으로 실행되는 스크립트다. "커밋 직전", "푸시 직전", "머지 직전" 같은 시점에 원하는 스크립트를 끼워넣을 수 있다.
여기서는 pre-commit 훅을 사용한다. 이름 그대로 "커밋 전"에 실행되는 스크립트인데, 이 스크립트가 실패(exit code가 0이 아닌 값을 반환)하면 커밋 자체가 만들어지지 않는다.
실제로 사용 중인 pre-commit 스크립트는 아주 단순하다.
#!/bin/bash
GIT_DIR=$(git rev-parse --show-toplevel)
$GIT_DIR/gradlew ktlintCheck
두 줄이 전부다. 하는 일은 이렇다.
첫 번째 줄에서 git rev-parse --show-toplevel 명령으로 프로젝트의 루트 디렉토리 경로를 가져온다. 이렇게 하는 이유는, 개발자가 프로젝트의 하위 디렉토리에서 git commit을 실행할 수도 있기 때문이다. gradlew 파일은 항상 프로젝트 루트에 있으니까, 루트 경로를 먼저 찾아서 그 경로를 기준으로 gradlew를 실행해야 한다.
두 번째 줄에서 해당 경로의 gradlew를 이용해 ktlintCheck 태스크를 실행한다. 이 태스크는 프로젝트의 모든 Kotlin 소스 파일을 .editorconfig에 정의된 규칙에 따라 검사한다. 규칙을 위반한 코드가 하나라도 있으면 태스크가 실패하고, 그러면 pre-commit 스크립트도 실패하고, 결과적으로 커밋이 차단된다.
이 스크립트가 효과적인 이유는, 개발자가 아무것도 기억할 필요가 없다는 점이다. 그냥 평소처럼 코드를 쓰고, 평소처럼 git commit을 하면 된다. 스타일이 맞으면 커밋이 되고, 안 맞으면 안 된다. 의지력이 아니라 시스템이 품질을 보장하는 구조다.
Makefile로 초기 설정 자동화하기
Git Hooks를 쓰려면 "이 프로젝트는 .githooks 디렉토리에 있는 스크립트를 Git Hooks로 사용하겠다"는 설정을 Git에 알려줘야 한다. 그리고 스크립트 파일에 실행 권한도 부여해야 한다.
이걸 팀원 각자가 수동으로 하라고 하면, 누군가는 까먹고, 누군가는 경로를 틀리고, 누군가는 권한 설정을 빼먹는다. 그래서 Makefile에 이 과정을 담아두고, make init 한 줄로 끝나게 만든다.
init:
git config core.hooksPath .githooks
chmod +x .githooks/pre-commit
git config core.hooksPath .githooks는 Git에게 "이 프로젝트의 Git Hooks는 .githooks 디렉토리에 있다"고 알려주는 명령이다. Git의 기본 hooks 디렉토리는 .git/hooks인데, .git 디렉토리는 Git이 관리하는 내부 디렉토리라서 리포지토리에 커밋할 수 없다. 즉, .git/hooks에 스크립트를 넣으면 본인 로컬에서만 동작하고, 다른 팀원에게는 공유되지 않는다.
반면 .githooks는 프로젝트 디렉토리 안에 있는 일반 디렉토리이므로, Git으로 추적하고 커밋할 수 있다. 그래서 .githooks에 스크립트를 넣고, core.hooksPath 설정으로 Git이 이 디렉토리를 바라보게 만드는 방식을 쓰는 거다. 이렇게 하면 hooks 스크립트가 코드와 함께 버전 관리되고, 모든 팀원이 동일한 hooks를 사용하게 된다.
chmod +x .githooks/pre-commit은 스크립트 파일에 실행 권한을 부여하는 명령이다. 리눅스와 맥에서는 파일에 실행 권한이 없으면 스크립트로 실행할 수 없다. Git은 파일 권한을 완벽하게 보존하지 않는 경우도 있기 때문에, 클론 받은 뒤에 명시적으로 실행 권한을 부여해주는 게 안전하다.
새로운 팀원의 온보딩 절차는 이렇게 된다.
git clone <리포지토리 주소>
cd <프로젝트 디렉토리>
make init
이 세 줄이면 개발 환경 설정이 끝난다. 이후부터는 코드를 작성하고 커밋할 때마다 자동으로 ktlint 검사가 실행된다.
build.gradle.kts에서 ktlint 플러그인 설정하기
위에서 설명한 모든 것이 동작하려면, Gradle 프로젝트에 ktlint 플러그인이 설정되어 있어야 한다. 이 플러그인이 ./gradlew ktlintCheck와 ./gradlew ktlintFormat 명령을 제공하기 때문이다.
멀티 모듈 프로젝트에서의 설정을 기준으로 설명한다. 루트 build.gradle.kts에서 플러그인을 선언하되 apply는 하지 않고, subprojects 블록에서 각 모듈에 적용하는 방식이다.
// 루트 build.gradle.kts
plugins {
// ... 다른 플러그인들
id("org.jlleitschuh.gradle.ktlint") apply false
}
subprojects {
apply(plugin = "org.jlleitschuh.gradle.ktlint")
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
version.set(properties["ktLintVersion"] as String)
}
}
id("org.jlleitschuh.gradle.ktlint") apply false에서 apply false가 붙어 있는 이유는, 루트 프로젝트 자체에는 Kotlin 소스 파일이 없기 때문이다. 멀티 모듈 프로젝트에서 루트 프로젝트는 하위 모듈들의 빌드를 조율하는 역할만 하지, 직접 소스 코드를 담고 있지는 않다. 그래서 플러그인을 "선언만 하고 적용은 하지 않는다"는 의미로 apply false를 붙인다.
subprojects 블록 안에서 실제로 각 하위 모듈에 플러그인을 적용하고, ktlint 버전을 설정한다. 버전을 gradle.properties 파일에서 읽어오는 방식을 쓰고 있는데, 이렇게 하면 ktlint 버전을 바꿀 때 한 곳만 수정하면 모든 모듈에 반영된다.
이 설정이 완료되면 다음 두 가지 Gradle 태스크를 사용할 수 있게 된다.
./gradlew ktlintCheck는 모든 Kotlin 소스 파일을 .editorconfig 규칙에 따라 검사하고, 위반 사항이 있으면 어떤 파일의 몇 번째 줄에서 어떤 규칙을 위반했는지 출력한다. 코드를 수정하지는 않는다.
./gradlew ktlintFormat은 위반 사항을 자동으로 수정한다. 대부분의 스타일 문제는 이 명령 한 줄로 해결된다. 단, 로직에 영향을 주는 변경은 하지 않고, 순수하게 포맷팅만 바꾼다.
실제 사용 흐름
이 모든 설정이 완료된 프로젝트에서 실제 개발 흐름은 이렇다.
평소처럼 코드를 작성한다. IDE의 자동 포맷팅 기능을 쓰든 안 쓰든 상관없다.
코드 작성이 끝나면 git add로 변경 파일을 스테이징하고, git commit으로 커밋을 시도한다.
git add .
git commit -m "feat(user): 사용자 프로필 조회 API 추가"
이 순간 pre-commit 훅이 실행되면서 ./gradlew ktlintCheck가 돌아간다. 스타일 위반이 없으면 커밋이 정상적으로 완료된다.
만약 스타일 위반이 있으면 이런 메시지가 출력되면서 커밋이 차단된다.
> Task :apps:commerce-api:ktlintMainSourceSetCheck FAILED
/path/to/SomeFile.kt:15:1: Unexpected blank line(s) before "}"
이때 해결 방법은 두 가지다.
첫 번째 방법은 자동 수정을 실행하는 거다.
./gradlew ktlintFormat
이 명령이 스타일 위반을 자동으로 수정해준다. 수정된 파일을 다시 스테이징하고 커밋하면 된다.
git add .
git commit -m "feat(user): 사용자 프로필 조회 API 추가"
두 번째 방법은 에러 메시지를 보고 직접 해당 위치를 수정하는 거다. ktlint가 어떤 파일의 몇 번째 줄에서 무슨 규칙을 위반했는지 정확하게 알려주니까, 그 위치로 가서 고치면 된다.
이 구조를 다른 프로젝트에 적용할 때 고려할 점
이 글에서 다룬 설정을 새 프로젝트에 그대로 가져다 쓸 수도 있지만, 몇 가지 상황에 따라 조정이 필요할 수 있다.
기존에 코드가 이미 많이 쌓인 프로젝트에 도입하는 경우, ./gradlew ktlintFormat을 먼저 한 번 돌려서 기존 코드를 전부 정리한 뒤에 "style: 전체 코드베이스에 ktlint 포맷팅 적용" 같은 커밋을 하나 만들고, 그 이후부터 pre-commit 훅을 활성화하는 게 자연스럽다. 기존 코드가 전부 스타일 위반인 상태에서 훅을 먼저 걸면, 아무도 커밋을 할 수 없게 되니까.
팀 내에서 EditorConfig 규칙에 대한 합의가 필요하다. 이 글에서 다룬 설정은 하나의 예시이고, max_line_length를 120으로 할지 130으로 할지, trailing comma를 허용할지 말지 같은 결정은 팀마다 다를 수 있다. 중요한 건 무슨 규칙이냐가 아니라 규칙이 하나로 통일되어 있고, 자동으로 강제되느냐다.
ktlint 규칙 중에서 프로젝트 상황과 맞지 않는 것이 있을 수 있다. 그런 경우에는 .editorconfig에서 해당 규칙을 disabled로 설정하면 된다. 위에서 본 것처럼 ktlint_standard_package-name = disabled 같은 형태로 개별 규칙을 끌 수 있다. 모든 규칙을 무조건 켜는 것보다, 팀이 동의하는 규칙만 켜고 나머지는 점진적으로 추가하는 접근이 현실적이다.
커밋 메시지 규칙까지 자동화하고 싶다면
이 프로젝트에서는 pre-commit 훅만 사용하고 있지만, 커밋 메시지 규칙까지 자동으로 강제하고 싶다면 commit-msg 훅을 추가할 수 있다.
commit-msg 훅은 커밋 메시지가 작성된 직후, 커밋이 확정되기 직전에 실행된다. 여기서 커밋 메시지의 형식을 검증해서, 규칙에 맞지 않으면 커밋을 차단할 수 있다.
#!/bin/bash
COMMIT_MSG_FILE=$1
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
PATTERN="^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .{1,100}"
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
echo ""
echo "커밋 메시지가 Conventional Commits 형식에 맞지 않습니다."
echo ""
echo "올바른 형식: <type>(<scope>): <subject>"
echo ""
echo "예시:"
echo " feat(auth): 사용자 로그인 API 추가"
echo " fix(cart): null pointer 예외 수정"
echo " refactor(payment): 결제 검증 로직 분리"
echo ""
echo "현재 메시지: \"$COMMIT_MSG\""
echo ""
exit 1
fi
exit 0
이 스크립트는 커밋 메시지가 "feat:", "fix:", "docs:" 같은 타입 접두어로 시작하는지 확인한다. Conventional Commits라는 규약을 따르는 형식인데, 이 규약의 핵심은 커밋 메시지의 첫 단어로 "이 커밋이 어떤 종류의 변경인지"를 명시하는 거다.
feat는 새로운 기능 추가, fix는 버그 수정, docs는 문서 변경, refactor는 동작 변경 없는 코드 구조 개선, test는 테스트 코드 추가나 수정, chore는 빌드 설정이나 도구 변경을 의미한다. 이 규칙이 지켜지면 git log만 훑어봐도 프로젝트에 어떤 변경이 있었는지 한눈에 파악할 수 있고, 나중에 자동으로 changelog를 생성하는 도구와도 바로 연동할 수 있다.
이 훅을 사용하려면 .githooks 디렉토리에 commit-msg라는 이름으로 저장하고, Makefile의 init 타겟에 chmod +x .githooks/commit-msg를 추가하면 된다.
푸시 전 테스트 자동 실행까지 추가하고 싶다면
한 단계 더 나아가서, git push 직전에 테스트를 자동으로 실행하도록 할 수도 있다. pre-push 훅을 사용하면 된다.
#!/bin/bash
GIT_DIR=$(git rev-parse --show-toplevel)
$GIT_DIR/gradlew test
이 스크립트는 pre-commit 훅과 구조가 동일하다. 차이점은 실행 시점이 "커밋 전"이 아니라 "푸시 전"이라는 점, 그리고 ktlintCheck 대신 test 태스크를 실행한다는 점이다.
테스트가 전부 통과해야만 푸시가 완료되므로, 깨진 테스트가 원격 리포지토리로 올라가는 걸 방지할 수 있다. 다만 주의할 점이 있다. 프로젝트 규모가 커지면 전체 테스트를 돌리는 데 시간이 꽤 걸릴 수 있다. 이런 경우에는 변경된 모듈의 테스트만 실행하도록 스크립트를 수정하거나, 빠른 단위 테스트만 실행하고 통합 테스트는 CI/CD에서 돌리는 방식으로 조율하는 게 현실적이다.

정리하면 이렇다.
.editorconfig로 코드가 어떤 모양이어야 하는지를 정의하고, ktlint Gradle 플러그인으로 그 규칙을 검사하고 자동 수정하는 도구를 갖추고, Git Hooks로 규칙을 어긴 코드가 커밋되지 못하게 차단하는 관문을 만들고, Makefile로 이 모든 설정을 한 줄에 끝내는 자동화를 제공한다.
핵심은 사람의 의지가 아니라 도구의 강제력에 기대는 거다. 코드 리뷰에서 스타일 지적을 하는 대신 도구가 알아서 걸러주고, 커밋 규칙을 문서로 공유하는 대신 규칙에 안 맞으면 커밋 자체가 안 되게 하고, "테스트 돌려보고 푸시하세요"라고 말하는 대신 테스트가 실패하면 푸시가 안 되게 한다.
이렇게 해두면 코드 리뷰에서 "여기 인덴트 틀렸어요" 같은 코멘트 대신 "이 로직의 엣지 케이스를 놓친 것 같은데요" 같은, 실질적인 이야기에 집중할 수 있게 된다. 그게 결국 팀 전체의 생산성 향상이라고 생각한다.
참고 자료
- 루프팩 BE L2 [ Volume 3 ] Repository for Kotlin https://www.loopers.im/
- 제목 원문: 이성복, "햇빛이 선명하게 나뭇잎을 핥고 있었다
- EditorConfig 공식 사이트: https://editorconfig.org/
- ktlint 공식 GitHub: https://github.com/pinterest/ktlint
- ktlint Gradle 플러그인: https://github.com/JLLeitschuh/ktlint-gradle
- Conventional Commits 규약: https://www.conventionalcommits.org/
- Git Hooks 공식 문서: https://git-scm.com/docs/githooks
- 대표 사진 링크: https://i.namu.wiki/i/Rbf_SbtcjT6z-QrazIZcPDbgvhP7QYjCwb03xuBtYFULL95IiJdbHC1Vfl57eB_sCJyp3TUp5qJVND87s2xJnQ.webp