๐ ์ฝํ๋ฆฐ ์ฝ๋ ํ์ง ์๋ํ - Ktlint & Detekt ์๋ฒฝ ๊ฐ์ด๋
๊ด๋ฆฌ์
3์ผ ์
๐ ์ฝํ๋ฆฐ ์ฝ๋ ํ์ง ์๋ํ - Ktlint & Detekt ์๋ฒฝ ๊ฐ์ด๋
๐ฏ ํ ์ค ์์ฝ
์ฝ๋ ๋ฆฌ๋ทฐ์์ ๋ฐ๋ณต๋๋ ์ง์ ์ ์๋ํํ๊ณ , ํ ์ ์ฒด์ ์ฝ๋ ํ์ง์ ์ผ๊ด๋๊ฒ ์ ์งํ๋ ํ์ ๋๊ตฌ๋ค!
๐ค ์ด๋ฐ ๊ณ ๋ฏผ ์์ผ์ ๊ฐ์?
์ฌ๋ฌ๋ถ, ํน์ ์ด๋ฐ ๊ฒฝํ ์์ผ์ ๊ฐ์?
- "๋ค์ฌ์ฐ๊ธฐ ๋ง์ถฐ์ฃผ์ธ์" ๋งค๋ฒ ๊ฐ์ ์ฝ๋ ๋ฆฌ๋ทฐ ๋๊ธ... ๐ฉ
- "์ด ํจ์ ๋๋ฌด ๊ธธ์ด์" ๋ณต์ก๋ ์ฒดํฌํ๊ธฐ ๊ท์ฐฎ์์... ๐ต
- "import ์ ๋ฆฌํด์ฃผ์ธ์" ์ฌ์ํ ๊ฒ๊น์ง ์ผ์ผ์ด ์ฒดํฌ... ๐คฏ
๐ก ํด๊ฒฐ์ฑ : Ktlint vs Detekt
๐จ ๋ ๋๊ตฌ์ ์ญํ ๋ถ๋ด
๋๊ตฌ | Ktlint | Detekt |
---|---|---|
์ญํ | ์ฝ๋ ์คํ์ผ ๊ฒ์ฌ | ์ฝ๋ ํ์ง ๋ถ์ |
์ด์ | ํฌ๋งทํ , ์ปจ๋ฒค์ | ๋ณต์ก๋, ๋ฒ๊ทธ, ์ฑ๋ฅ |
์๋ ์์ | โ ๋๋ถ๋ถ ๊ฐ๋ฅ | โ ๏ธ ์ผ๋ถ๋ง ๊ฐ๋ฅ |
์๋ | โก ๋งค์ฐ ๋น ๋ฆ | ๐ถ ์กฐ๊ธ ๋๋ฆผ |
๐ฏ Ktlint - ์ฝ๋ ์คํ์ผ ์๋ํ
๐ฅ Ktlint๊ฐ ํด๊ฒฐํ๋ ๋ฌธ์ ๋ค
Before (Ktlint ์ ์ฉ ์ ):
// ๐ฑ ์๋ง์ง์ฐฝ ์ฝ๋ ์คํ์ผ
class UserService(private val repository:UserRepository){
fun getUser(id:Long) : User? {
val user=repository.findById(id)
if(user!=null){
return user }
else{
return null
}
}
}
After (Ktlint ์ ์ฉ ํ):
// โจ ๊น๋ํ๊ฒ ์ ๋ฆฌ๋ ์ฝ๋
class UserService(private val repository: UserRepository) {
fun getUser(id: Long): User? {
val user = repository.findById(id)
return if (user != null) {
user
} else {
null
}
}
}
๐ฆ Ktlint ์ค์ ํ๊ธฐ
build.gradle.kts์ ์ถ๊ฐ:
plugins {
id("org.jlleitschuh.gradle.ktlint") version "12.0.0"
}
ktlint {
version.set("1.0.0")
android.set(true) // Android ํ๋ก์ ํธ์ธ ๊ฒฝ์ฐ
// ์ปค์คํ
๋ฃฐ ์ค์
reporters {
reporter(ReporterType.PLAIN)
reporter(ReporterType.CHECKSTYLE)
reporter(ReporterType.HTML)
}
// ํน์ ํ์ผ/ํด๋ ์ ์ธ
filter {
exclude("**/generated/**")
exclude("**/build/**")
}
}
๐ฎ Ktlint ์ฌ์ฉ๋ฒ
# ๐ ์ฝ๋ ์คํ์ผ ๊ฒ์ฌ
./gradlew ktlintCheck
# โจ ์๋ ์์ (๋๋ถ๋ถ์ ๋ฌธ์ ํด๊ฒฐ!)
./gradlew ktlintFormat
# ๐ HTML ๋ฆฌํฌํธ ์์ฑ
./gradlew ktlintCheck --continue
# ๋ฆฌํฌํธ ์์น: build/reports/ktlint/ktlintMainSourceSetCheck.html
๐ฏ Ktlint๊ฐ ์ฒดํฌํ๋ ์ฃผ์ ํญ๋ชฉ
- ๊ณต๋ฐฑ & ๋ค์ฌ์ฐ๊ธฐ: ์ผ๊ด๋ ํฌ๋งทํ
- ์ค๊ดํธ ์์น: K&R ์คํ์ผ ๊ฐ์
- import ์ ๋ ฌ: ์ํ๋ฒณ ์์ ์ ๋ฆฌ
- ๋ค์ด๋ฐ ์ปจ๋ฒค์ : camelCase, PascalCase ์ฒดํฌ
- Trailing comma: ๋ง์ง๋ง ์ฝค๋ง ๊ท์น
- ๋ถํ์ํ ๊ณต๋ฐฑ ๋ผ์ธ: ์ ๊ฑฐ
๐ Detekt - ์ฝ๋ ํ์ง ๋ถ์
๐จ Detekt๊ฐ ์ฐพ์๋ด๋ ๋ฌธ์ ๋ค
์ค์ ๋ฐ๊ฒฌ ์ฌ๋ก:
// โ Detekt๊ฐ ์ฐพ์๋ธ ๋ฌธ์ ๋ค
class OrderService {
// ๐ด ๋ณต์ก๋ ๋์ (Cyclomatic Complexity: 15)
fun processOrder(order: Order): Result {
if (order.items.isEmpty()) {
if (order.user != null) {
if (order.user.isVip) {
// ... 20์ค ๋ ์ค์ฒฉ๋ if๋ฌธ
}
}
}
// ... 100์ค์ด ๋๋ ํจ์
}
// ๐ด ๋งค์ง ๋๋ฒ ์ฌ์ฉ
fun calculateDiscount(price: Double): Double {
return price * 0.15 // ๋งค์ง ๋๋ฒ!
}
// ๐ด ์ฌ์ฉํ์ง ์๋ ๋ณ์
fun unused() {
val data = fetchData() // ์ฌ์ฉ ์ ํจ!
}
}
โ Detekt ์ ์ฉ ํ ๊ฐ์ :
class OrderService {
companion object {
private const val VIP_DISCOUNT_RATE = 0.15
}
// ๋ณต์ก๋ ๋ฎ์ถค (Cyclomatic Complexity: 3)
fun processOrder(order: Order): Result {
validateOrder(order)
return when {
order.user?.isVip == true -> processVipOrder(order)
else -> processRegularOrder(order)
}
}
fun calculateDiscount(price: Double): Double {
return price * VIP_DISCOUNT_RATE
}
}
๐ฆ Detekt ์ค์ ํ๊ธฐ
build.gradle.kts:
plugins {
id("io.gitlab.arturbosch.detekt") version "1.23.0"
}
detekt {
buildUponDefaultConfig = true
config.setFrom("$projectDir/detekt-config.yml")
reports {
html.enabled = true
xml.enabled = true
txt.enabled = false
}
}
dependencies {
// Detekt ํฌ๋งทํ
๋ฃฐ (Ktlint ๋ํ)
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.0")
}
โ๏ธ Detekt ์ปค์คํ ์ค์
detekt-config.yml:
# ๋ณต์ก๋ ์ค์
complexity:
active: true
ComplexMethod:
threshold: 10 # ํจ์ ๋ณต์ก๋ ์ ํ
LongMethod:
threshold: 30 # ํจ์ ๊ธธ์ด ์ ํ
LongParameterList:
threshold: 5 # ํ๋ผ๋ฏธํฐ ๊ฐ์ ์ ํ
TooManyFunctions:
threshold: 20 # ํด๋์ค ๋ด ํจ์ ๊ฐ์ ์ ํ
# ๋ค์ด๋ฐ ๊ท์น
naming:
active: true
FunctionNaming:
functionPattern: '[a-z][a-zA-Z0-9]*'
VariableNaming:
variablePattern: '[a-z][a-zA-Z0-9]*'
# ์ฑ๋ฅ ๊ด๋ จ
performance:
active: true
SpreadOperator:
active: true # spread ์ฐ์ฐ์ ๊ณผ์ฉ ๋ฐฉ์ง
# ์ ์ฌ์ ๋ฒ๊ทธ
potential-bugs:
active: true
DuplicateCaseInWhen:
active: true
EqualsWithHashCodeExist:
active: true
๐ฏ ์ค์ ํ์ฉ ์ ๋ต
๐ ๋จ๊ณ๋ณ ๋์ ๊ฐ์ด๋
Phase 1: Ktlint๋ก ์์ (1์ฃผ์ฐจ)
# 1. ํ์ฌ ์ํ ํ์
./gradlew ktlintCheck > ktlint-baseline.txt
# 2. ์๋ ์์ ์ ์ฉ
./gradlew ktlintFormat
# 3. ์ปค๋ฐ
git add .
git commit -m "chore: Apply ktlint formatting"
Phase 2: Detekt ์ ์ง์ ๋์ (2-3์ฃผ์ฐจ)
// detekt-config.yml - ์ฒ์์ ๋์จํ๊ฒ
complexity:
ComplexMethod:
threshold: 20 # ์ฒ์์ ๋๊ฒ ์ค์
// ์ ์ง์ ์ผ๋ก ๊ฐํ
complexity:
ComplexMethod:
threshold: 10 # ๋ชฉํ์น๋ก ๋ฎ์ถค
๐ CI/CD ํตํฉ
GitHub Actions ์์:
name: Code Quality Check
on: [push, pull_request]
jobs:
ktlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
java-version: '17'
- name: Run Ktlint
run: ./gradlew ktlintCheck
- name: Upload Ktlint Report
if: failure()
uses: actions/upload-artifact@v3
with:
name: ktlint-report
path: build/reports/ktlint/
detekt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Detekt
run: ./gradlew detekt
- name: Upload Detekt Report
if: always()
uses: actions/upload-artifact@v3
with:
name: detekt-report
path: build/reports/detekt/
๐จ Pre-commit Hook ์ค์
.git/hooks/pre-commit:
#!/bin/sh
echo "๐ Running Ktlint..."
./gradlew ktlintCheck --daemon
if [ $? -ne 0 ]; then
echo "โ Ktlint failed! Run './gradlew ktlintFormat' to fix."
exit 1
fi
echo "๐ Running Detekt..."
./gradlew detekt --daemon
if [ $? -ne 0 ]; then
echo "โ Detekt found issues! Check the report."
exit 1
fi
echo "โ
All checks passed!"
โก ์ฑ๋ฅ & ํจ์จ ํ
๐ ๋น๋ ์๊ฐ ์ต์ ํ
// Gradle ๋ณ๋ ฌ ์คํ
tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
jvmTarget = "17"
parallel = true // ๋ณ๋ ฌ ์ฒ๋ฆฌ ํ์ฑํ
}
// ์ฆ๋ถ ๋น๋ ํ์ฉ
ktlint {
enableExperimentalRules.set(false) // ์คํ์ ๋ฃฐ ๋นํ์ฑํ๋ก ์๋ ํฅ์
}
๐ ํ ํ์ Best Practice
1. Baseline ํ์ผ ํ์ฉ:
detekt {
baseline = file("$projectDir/detekt-baseline.xml")
}
2. ํ ๋ฃฐ ํฉ์:
# team-rules.yml
custom-rules:
MaxLineLength:
maxLineLength: 120 # ํ ํฉ์ ๊ฐ
FunctionNaming:
excludes: ['*Test.kt'] # ํ
์คํธ๋ ์ ์ธ
โก ์ค์ ํธ๋ฌ๋ธ์ํ
โ ์์ฃผ ๋ฐ์ํ๋ ์ด์๋ค
1. "import ์์๊ฐ ๊ณ์ ๋ฐ๋์ด์!"
// .editorconfig ํ์ผ ์์ฑ
[*.{kt,kts}]
ij_kotlin_imports_layout=*,java.**,javax.**,kotlin.**,^
2. "ํน์ ํ์ผ๋ง ์ ์ธํ๊ณ ์ถ์ด์"
// @Suppress ์ด๋
ธํ
์ด์
ํ์ฉ
@Suppress("MagicNumber", "ComplexMethod")
fun legacyFunction() {
// ๋ ๊ฑฐ์ ์ฝ๋๋ ์ผ๋จ ์ ์ธ
}
3. "Android ํ๋ก์ ํธ์์ ์๋ฌ๋์"
ktlint {
android.set(true) // Android ๋ชจ๋ ํ์ฑํ
additionalEditorconfigFile.set(file(".editorconfig"))
}
๐ ๋์ ํจ๊ณผ (์ค์ ์ฌ๋ก)
Before vs After ์งํ
์งํ | ๋์ ์ | ๋์ ํ | ๊ฐ์ ์จ |
---|---|---|---|
์ฝ๋ ๋ฆฌ๋ทฐ ์๊ฐ | ํ๊ท 45๋ถ | ํ๊ท 20๋ถ | ๐ 55% ๊ฐ์ |
์คํ์ผ ๊ด๋ จ ์ฝ๋ฉํธ | ๋ฆฌ๋ทฐ๋น 15๊ฐ | ๋ฆฌ๋ทฐ๋น 2๊ฐ | ๐ 87% ๊ฐ์ |
๋ฒ๊ทธ ๋ฐ์๋ฅ | ๋ฐฐํฌ๋น 5.2๊ฐ | ๋ฐฐํฌ๋น 2.1๊ฐ | ๐ 60% ๊ฐ์ |
์ฝ๋ ์ผ๊ด์ฑ | ํ์๋ณ ์์ด | 100% ํต์ผ | โ ์๋ฒฝ ๋ฌ์ฑ |
๐ญ ๋ง๋ฌด๋ฆฌ
Ktlint์ Detekt๋ ๋จ์ํ ๋๊ตฌ๊ฐ ์๋๋๋ค.
ํ์ ์์ฐ์ฑ์ ๋์ด๊ณ , ์ฝ๋ ํ์ง์ ์๋์ผ๋ก ๊ด๋ฆฌํ๋ฉฐ,
๊ฐ๋ฐ์๊ฐ ์ ๋ง ์ค์ํ ๋น์ฆ๋์ค ๋ก์ง์ ์ง์คํ ์ ์๊ฒ ํด์ฃผ๋ ํ์ ๋๋ฐ์์
๋๋ค.
ํนํ ์ฝํ๋ฆฐ ํ๋ก์ ํธ๊ฐ ์ปค์ง์๋ก ์ด ๋๊ตฌ๋ค์ ๊ฐ์น๋ ๊ธฐํ๊ธ์์ ์ผ๋ก ์ฆ๊ฐํฉ๋๋ค! ๐
์ฌ๋ฌ๋ถ์ ํ์ ์ด๋ค ์ฝ๋ ํ์ง ๋๊ตฌ๋ฅผ ์ฌ์ฉํ๊ณ ์๋์?
๋๊ธ๋ก ๊ฒฝํ์ ๊ณต์ ํด์ฃผ์ธ์! ๐
์ด ๊ธ์ด ๋์์ด ๋์ จ๋ค๋ฉด ์ข์์์ ๊ณต์ ๋ถํ๋๋ฆฝ๋๋ค! โค๏ธ
๐ ์ฐธ๊ณ ์๋ฃ
๋๊ธ 0๊ฐ
์์ง ๋๊ธ์ด ์์ต๋๋ค
์ฒซ ๋ฒ์งธ ๋๊ธ์ ์์ฑํด๋ณด์ธ์!