Backend

๐Ÿš€ ์ฝ”ํ‹€๋ฆฐ ์ฝ”๋“œ ํ’ˆ์งˆ ์ž๋™ํ™” - Ktlint & Detekt ์™„๋ฒฝ ๊ฐ€์ด๋“œ

๊ด€๋ฆฌ์ž

3์ผ ์ „

19200
#Kotlin#Ktlint#Detekt#์ฝ”๋“œํ’ˆ์งˆ

๐Ÿš€ ์ฝ”ํ‹€๋ฆฐ ์ฝ”๋“œ ํ’ˆ์งˆ ์ž๋™ํ™” - 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 ์‹คํ–‰ ๊ฒฐ๊ณผ

๐ŸŽฏ 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

Detekt ๋ถ„์„ ๊ฒฐ๊ณผ

๐ŸŽฏ ์‹ค์ „ ํ™œ์šฉ ์ „๋žต

๐Ÿƒ ๋‹จ๊ณ„๋ณ„ ๋„์ž… ๊ฐ€์ด๋“œ

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๊ฐœ

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค

์ฒซ ๋ฒˆ์งธ ๋Œ“๊ธ€์„ ์ž‘์„ฑํ•ด๋ณด์„ธ์š”!