코딩하는 일용직 노동자

Hilt에서 @Provides와 @Binds: 언제, 어떻게 사용해야 할까? 본문

안드로이드

Hilt에서 @Provides와 @Binds: 언제, 어떻게 사용해야 할까?

bacass 2025. 8. 10. 15:05
 
안드로이드 개발에서 Hilt를 사용하여 의존성 주입(Dependency Injection)을 할 때, 모듈(Module) 내에서 의존성을 제공하는 방법으로 `@Provides`와 `@Binds` 두 가지 애노테이션을 사용합니다.
둘 다 의존 객체를 제공하는 역할을 하지만, 사용 목적과 방식, 그리고 성능에서 차이가 있어 상황에 맞게 사용하는 것이 중요합니다.

결론부터 말하자면, 인터페이스(interface)와 구현체(implementation)를 묶어주는 단순한 작업에는 `@Binds`를, 복잡한 객체 생성 로직이 필요할 때는 `@Provides`를 사용하는 것이 좋습니다.



@Provides: 객체 생성의 모든 것을 책임진다

`@Provides`는 Hilt가 직접 생성할 수 없는 타입의 객체를 어떻게 만들어서 제공할지 알려주는 역할을 합니다.
다음과 같은 경우에 주로 사용됩니다.

- 외부 라이브러리 객체 생성:** Retrofit, OkHttpClient, Room 데이터베이스처럼 프로젝트 소스 코드에 속하지 않아 생성자에 `@Inject`를 추가할 수 없는 클래스의 인스턴스를 제공해야 할 때 사용합니다.
- 빌더 패턴(Builder Pattern) 사용:** 객체 생성을 위해 빌더 패턴을 사용해야 하는 경우, `@Provides` 함수 본문에 빌더를 통한 생성 로직을 구현할 수 있습니다.
- 인스턴스 생성 전 조건부 로직 필요:** 특정 조건에 따라 다른 구현체를 제공해야 하는 등 복잡한 로직이 필요한 경우에 유용합니다.

`@Provides` 함수는 반드시 **객체 생성 로직을 포함한 본문(body)을 가지며**, 보통 `object`로 선언된 모듈 안에 위치합니다.

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .connectTimeout(20, TimeUnit.SECONDS)
            .readTimeout(20, TimeUnit.SECONDS)
            .build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .client(okHttpClient)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
}



@Binds: 인터페이스와 구현체를 연결하는 가장 효율적인 방법

`@Binds`는 **인터페이스에 대한 구현체를 Hilt에게 알려주는, 보다 전문화되고 효율적인 방법**입니다. 특정 인터페이스가 요청될 때 어떤 구현체를 주입해야 할지 지정하는 역할을 합니다.

`@Binds`를 사용하기 위한 몇 가지 제약 조건이 있습니다.

- 추상(abstract) 클래스 모듈:** `@Binds` 함수는 반드시 추상 클래스(abstract class)로 선언된 모듈 안에 위치해야 합니다.
- 추상(abstract) 함수:** 함수 자체도 추상 함수여야 하며, 본문을 가질 수 없습니다.
- 단일 파라미터:** 함수는 반드시 하나의 파라미터를 가져야 하며, 이 파라미터가 바로 주입될 구현체입니다.

`@Binds`는 단순히 "이 인터페이스가 필요하면 저 구현체를 사용해"라고 알려주기만 하므로, Hilt가 더 적은 양의 코드를 생성하게 됩니다.

interface UserRepository {
    fun getUser(): User
}

class UserRepositoryImpl @Inject constructor(
    private val remoteDataSource: UserRemoteDataSource
) : UserRepository {
    override fun getUser(): User {
        return remoteDataSource.fetchUser()
    }
}

@Module
@InstallIn(ViewModelComponent::class)
abstract class RepositoryModule {

    @Binds
    abstract fun bindUserRepository(
        userRepositoryImpl: UserRepositoryImpl
    ): UserRepository
}

@Provides vs. @Binds: 성능 차이는 왜 발생할까?

가장 큰 차이점은 **코드 생성량과 런타임 성능**입니다.

- @Provides:** Hilt는 `@Provides` 함수를 위해 별도의 팩토리(Factory) 클래스를 생성합니다.
의존성을 주입할 때마다 이 팩토리의 메소드를 호출하여 객체를 생성하므로, 약간의 런타임 오버헤드가 발생할 수 있습니다.

- @Binds:** `@Binds`는 Hilt에게 단순히 타입 정보만 알려줍니다.
Hilt는 이 정보를 바탕으로 별도의 팩토리 클래스 없이 의존성 그래프를 구성합니다.
결과적으로 생성되는 코드가 더 적고 간결하며, 런타임 시 메소드 호출이 줄어들어 더 효율적입니다.



따라서 인터페이스와 그 구현체를 1:1로 바인딩하는 경우에는 성능상의 이점을 위해 항상 `@Binds`를 사용하는 것이 권장됩니다.
`@Provides`로도 동일한 기능을 구현할 수 있지만, 불필요한 오버헤드가 발생하게 됩니다.