
buildSrc)
실질적으로 remote 모듈에서 SDK를 호출하지만, FirebaseApp 초기화 작업이 필요하기 때문에 다음과 같은 설정을 사전에 진행했습니다:
build.gradle 에서 com.google.gms:google-services 플러그인 추가app 수준의 build.gradle 에서 com.google.gms.google-services 플러그인 추가app 모듈의 루트 경로에 google-services.json (Firebase Console 제공) 추가Firebase는 외부 데이터 소스이므로 API나 SDK와 같은 원격 데이터 소스를 관리하는
remote모듈에서 작업을 시작하게 되었습니다. 추후 테스트 시 Mock 구현체를 만들기 용이하다는 점을 고려했을 때도 적절한 위치라고 판단했습니다.
Firebase는 SDK 내부에서 자체적으로 API 호출을 처리하기 때문에 기존의
ApiService개념은 생략할 수도 있습니다. 그럼에도 비슷한 개념을 구현하려 하는건, 원격 데이터 소스(Remote Data Source)를 명확히 분리하고 설계함으로써 추후 실제 서버 API로 교체하는 등의 유지보수 또는 확장 작업에 유리한 구조을 설계하기 위해서 입니다.
어찌 보면 FirebaseAuth는 이미 메서드까지 완성된 구현체입니다. @Binds 작업이나 @Module 작업 없이 DataSource에 구현체를 바로 주입하면 단기적인 관점에서는 작업 소요를 줄일 수는 있습니다.
// 상위 책임자(Repository)에 값을 제공할 원격 데이터 소스 구현체
class AuthRemoteDataSourceImpl @Inject constructor(
private val firebaseAuth: FirebaseAuth // SDK 구현체를 주입
) : AuthRemoteDataSource {
// 인스턴스 생성 구현(getInstance()) 또는 외부에서 @Provides 작업
override suspend fun register(email: String, password: String):
firebaseAuth...
// 생성된 인스턴스를 통해 바로 SDK 요청 로직 구현
하지만 조금 더 넓은 관점에서 본다면 다음과 같은 문제가 발생할 수 있습니다:
FirebaseAuth)에 의존하게 됨AuthRemoteDataSourceImpl을 직접 수정해야 함 → 확장에는 닫혀 있고 변경이 필요해짐이러한 원칙을 위배하면 장기적으로 오히려 작업 소요가 늘어날 수 있습니다.
ApiService와 FirebaseService는 모두 추상체이지만 다음과 같은 차이점이 있습니다:
| 구분 | ApiService (일반적인 REST API) | FirebaseService (Firebase SDK) |
|---|---|---|
| 역할 | 엔드포인트를 추상화한 서비스 | Firebase SDK 요청을 추상화한 서비스 |
| 호출 방식 | @GET, @POST 등 어노테이션 기반 호출 | 구현체에서 SDK 메서드를 직접 호출 |
두 방식 모두 요청을 추상화하지만, FirebaseService는 SDK를 직접 호출해야 하므로 별도의 구현체가 필요합니다.
// 추상체
interface FirebaseService {
suspend fun login(email: String, password: String): String
}
// 구현체
class FirebaseServiceImpl @Inject constructor(
private val firebaseAuth: FirebaseAuth
) : FirebaseService {
override suspend fun register(email: String, password: String): String {
return runCatching {
firebaseAuth.createUserWithEmailAndPassword(email, password).await().user!!.uid
}.getOrElse { throw Exception("회원가입 실패: ${it.message}") }
}
override fun another(){...}
}
구현체인 FirebaseServiceImpl에는 FirebaseAuth가 주입되어 있으며, 실질적인 로직이 작성되어 있습니다. 이 구조를 사용하려면 인스턴스 등록과 모듈 구성 작업이 필요합니다.
FirebaseAuth SDK를 사용하려면 인스턴스 초기화 작업이 필수이며, 외부 객체이기 때문에 수동으로 등록해야 합니다.
@Module
@InstallIn(SingletonComponent::class)
object FirebaseModule {
@Provides
@Singleton
fun provideFirebaseAuth():
FirebaseAuth = Firebase.auth
@Provides
@Singleton
fun provideFirebaseMessaging():
FirebaseMessaging = FirebaseMessaging.getInstance()
// FirebaseDatabase, FirebaseFirestore 등등...
}
이처럼 바인딩 작업을 수행하면, FirebaseServiceImpl에 FirebaseAuth를 주입할 수 있습니다. 만약 REST api 환경이였다면 유사한 방식으로 Retrofit.Builder()를 반환하는 ApiService와 연결된 모듈을 작성했을 것입니다.
추가로, Hilt DI 작업의 효과를 직접적으로 확인할 수 있는 부분이 바로 @Singleton 어노테이션입니다. 이 어노테이션을 사용함으로써 SDK 인스턴스를 한 번만 생성하고 필요할 때 재사용할 수 있어, 리소스를 절약하고 중앙 집중형 관리로 인한 유지보수성 향상 효과를 얻을 수 있습니다.
ApiService는 추상체이므로 바로 RemoteDataSource에 주입할 수 있었겠지만, FirebaseServiceImpl은 구현체이기 때문에 DIP를 지키기 위해 FirebaseService 인터페이스를 주입하도록 설정해야 합니다.
@Module
@InstallIn(SingletonComponent::class)
abstract class FirebaseBindModule {
@Binds
@Singleton
abstract fun bindFirebaseService(
impl: FirebaseServiceImpl
): FirebaseService
}
이렇게 Module을 생성하고 @Binds를 사용해 FirebaseService와 FirebaseServiceImpl 간의 관계를 명시함으로써, Hilt가 주입 가능한 상태를 구성하게 됩니다.
지금까지의 설정을 바탕으로 외부 작업을 안전하게 내부에 주입할 수 있는 구조가 마련되었습니다. RemoteDataSource는 외부 통신의 진입점으로 동작하며 다음과 같이 작성됩니다.
// 추상체
interface AuthRemoteDataSource {
suspend fun register(email: String, password: String): String
}
// 구현체
class AuthRemoteDataSourceImpl @Inject constructor(
private val firebaseService: FirebaseService
) : AuthRemoteDataSource {
override suspend fun register(email: String, password: String): String =
firebaseService.register(email, password)
}
이처럼 ApiService 대신 동일한 역할을 수행하는 FirebaseService가 주입되며, 이 RemoteDataSource는 domain 계층에 위치한 Repository 인터페이스의 구현체 작성에 사용됩니다.
internal class AuthRepositoryImpl @Inject constructor(
private val authRemoteDataSource: AuthRemoteDataSource,
) : AuthRepository {
override fun register(
email: String,
password: String,
): Flow<DataResource<String>> =
flowDataResource { authRemoteDataSource.register(email, password) }
}
이렇게 구현된 Repository는 같은 domain 계층의 UseCase에 의해 호출되며, 이후 ViewModel을 통해 사용자 UI와 연결됩니다.
FirebaseService라는 추상화를 도입FirebaseServiceImpl은 SDK 호출을 담당하며, 외부 라이브러리 의존을 한 곳으로 모아 관리FirebaseService는 RemoteDataSource에서 주입받아 사용
:remote
/di - FirebaseModule, FirebaseBindModule
/impl - FirebaseServiceImpl, AuthRemoteDataSourceImpl
/service - FirebaseService:data
/remote - AuthRemoteDataSource