Koin의 어노테이션으로 컴파일 타임에서 의존성 검증 (Compile-Time-Safety)

Dependency Injection 이 필요한가요?

글의 주제에서 살짝 벗어나기에 Dependency Injection의 이유는 아래 정도로만 기술한다.

의존성 주입을 통해 우리가 얻을 수 있는건 협업하는 모듈 간 커플링이 낮아지고, 높은 응집도가 생긴다는 점이다. 커플링이 높은 코드 또는 응집도가 낮은 코드는 덩치가 커졌을 때 분리해내기 매우 어렵다. 모듈 간 커플링이 낮고 높은 응집도를 갖게된다면, 일반적으로 코드의 단순화 및 유지보수의 더 높은 수준의 자유를 얻을 수 있다.

또, 테스트 가능성(Testability)을 얻을 수도 있다. 테스트 코드를 작성할 때, 실제 데이터베이스 모듈이 아닌 Mock을 사용하는 등, 테스트 대상 객체를 다른 의존성으로부터 격리시킬 수 있는 아주 쉬운 구조가 될 수 있다.

- 출처 : Python과 FastAPI로 지속 성장 가능한 웹서비스 개발하기 - 1. 의존성 주입

Koin

위의 이유로 복잡도가 어느정도 있는 서비스를 개발할 때에는 “의존성 주입"이 많은 도움을 준다. Java, Kotlin으로 웹 서비스를 개발할 때 가장 많이 쓰이는 스프링 프레임워크 개발환경에서는 특별한 세팅 없이 의존성 주입을 할 수 있는 개발 환경을 갖고 있다. 하지만 Ktor 같은 미니멀한 웹 프레임워크를 사용할 때에는 이런 기능이 포함되지 않아서 써드파티 라이브러리를 사용해야만 한다. Ktor 와 Koin이 가장 잘 쓰이는 조합인 것 처럼 보인다. 실제로 본인도 이 조합으로 운영하는 서비스의 일부를 구현하고 있다.

Compile-Time-Safety

과거의 누군가의 블로그 포스팅, 그리고 지금도 재생산되고 있는 글 중 아쉽다고 지적되는 것들이 컴파일 타임에서의 의존성 검증이다. 하지만 지금 시점에서는 어느정도 만족스러운 검증 기능을 이미 제공하고 있다. Koin Annotation 기능과 KSP (Kotlin Symbol Processing API)를 통해 목표를 달성할 수 있다.

Koin Annotation

코인의 어노테이션 api를 살펴보기 위한 예시이다. 아래와 같이 서비스(AuthService), 서비스가 필요한 저장소의 인터페이스(AuthRepository) 그리고 그 인터페이스의 구현체(AuthRepositoryImpl)를 편의상 한 파일에 정의하였다.

package binaryflavor.example.domains.auth

import org.koin.core.annotation.Single


@Single
internal class AuthRepositoryImpl : AuthRepository {
    override fun find(authProvider: UserAuthProvider, providerId: String): UserAuth = TODO()
}

interface AuthRepository {
    fun find(authProvider: UserAuthProvider, providerId: String): UserAuth
}

@Single
class AuthService(private val authRepository: AuthRepository)

두 클래스는 모두 @Single 어노테이션이 붙어있다. 이 인스턴스를 싱글턴으로 관리하도록 선언함을 의미한다. Koin은 defaultModule 을 제공하고 이 defaultModule에 @Single로 선언된 정의들을 제공한다.

import org.koin.ksp.generated.*  // <- KSP

fun main() {
    startKoin {
        defaultModule()
    }
}

만약, 해당 도메인 모듈을 분리해서 관리하고싶다고 한다면 아래의 코드처럼 @Module을 선언한다. @ComponentScan 이라는 어노테이션은 KSP를 이용해서 해당 패키지 경로의 모든 클래스를 순회하며 컴포넌트들을 탐지하도록 한다.

@Module
@ComponentScan("binaryflavor.example.domains.auth")
class DomainAuthModule

AuthRepositoryImpl클래스의 선언을 제거하면 컴파일 시에 바로 에러를 발생한다. error

KSP (Kotlin Symbol Processing API)

Koin은 KSP를 통해서 Compile-Time-Safety 를 달성했다. KSP는 Kotlin 환경에 친화적이며, 가벼운 어노테이션 프로세싱 API이다. JVM 기반의 Kapt로 지원하던 많은 프로젝트들이 KSP에 대한 지원을 제공하고 있다. KSP에 대한 자세한 이야기는 다른 포스트를 통해 다뤄보도록 한다.

KSP process

필자는 Gradle 기반으로 Ktor 어플리케이션을 구현하고 있다. 설정은 아래와 같다.

# libs.versions.toml
koin-annotations-version = "1.4.0"
ksp-version = "2.1.10-1.0.29"

[libraries]
# ...
koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koin-annotations-version" }
koin-ksp-compiler = { module = "io.insert-koin:koin-ksp-compiler", version.ref = "koin-annotations-version" }

[plugins]
# ...
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp-version" }
// gradle.build.kts
plugins {
    // ...
    alias(libs.plugins.ksp)
}

dependencies {
    // ...
    implementation(libs.koin.annotations)
    ksp(libs.koin.ksp.compiler)
}


ksp {
    arg("KOIN_CONFIG_CHECK", "true")
    arg("KOIN_DEFAULT_MODULE", "false")
}

gradle.build.kts에서 ksp 블럭에 아래의 값들을 설정할 수 있다.

실제 프로젝트에서 Koin Annotation이 선언된 컴포넌트 안에서만 검증이 가능하다는 것 외에는 아쉬운 점 없이 잘 사용하고 있다.