[Spring] @Primary와 @Qualifier 사용해 하나의 인터페이스에 둘 이상의 구현체가 있을 때 의존성 주입 우선 순위 관리하기
Bean의 의존성 주입 우선 순위 관리가 필요한 이유
다음과 같은 모양의 UserRepository와 두 개의 구현체 InMemoryUserRepository, DbUserRepository 가 있다고 해보자.
interface UserRepository {
fun createUser(id: String, user: String)
}
class InMemoryUserRepository(): UserRepository {
override fun createUser(id: String, user: String) {
println("User $user created with id: $id in InMemoryUserRepository")
}
}
class DbUserRepository(): UserRepository {
override fun createUser(id: String, user: String) {
println("User $user created with id: $id in DbUserRepository")
}
}
UserCreator은 다음과 같이 UserRepository를 사용해 사용자 생성 동작을 수행한다.
class UserCreator(private val repository: UserRepository) {
fun createUserWithRandomId(user: String) {
val id = UUID.randomUUID().toString()
repository.createUser(id, user)
println("[User Creation Success] User $user id: $id")
}
}
만약 UserRepository의 인스턴스가 다음과 같이 두 개 이상 Bean으로 등록되고, UserCreator가 UserRepository를 주입 받는다면 어떤 일이 벌어질까?
@Configuration
class UserContainerConfiguration {
@Bean
fun inMemoryUserRepository(): UserRepository {
return InMemoryUserRepository()
}
@Bean
fun dbUserRepository(): UserRepository {
return DbUserRepository()
}
@Bean
fun userCreator(repository: UserRepository): UserCreator {
return UserCreator(repository)
}
}
이를 확인하기 위해 다음 코드를 실행해보자. 이 코드에서는 getBean을 통해 UserCreator을 가져와 createUserWithRandomId를 실행한다.
fun main(args: Array<String>) {
val context = AnnotationConfigApplicationContext(UserContainerConfiguration::class.java)
context.getBean(UserCreator::class.java).createUserWithRandomId("세영")
}
그러면 다음과 같은 오류가 생기는 것을 볼 수 있다.
Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userCreator' defined in com.kotlinworld.spring.di.section5.UserContainerConfiguration: Unsatisfied dependency expressed through method 'userCreator' parameter 0: No qualifying bean of type 'com.kotlinworld.spring.di.section5.UserRepository' available: expected single matching bean but found 2: inMemoryUserRepository,dbUserRepository
오류 메세지를 요약해보면, UserCreator을 생성하려고 하는데 UserRepository 타입을 가진 Bean이 하나만 필요한데 두개가 있다는 뜻이다.
이 문제를 해결하기 위해서는 Bean의 우선 순위를 설정해야 한다. 이어서 우선 순위를 설정하는 방법에 대해 알아보자.
Bean의 의존성 주입 우선 순위 관리하기
Bean의 의존성 주입 우선 순위를 설정하기 위해서는 @Primary와 @Qualifier 두가지 어노테이션을 사용할 수 있다. 각각을 사용해 우선 순위를 관리하는 방법을 알아보자.
@Primary 사용해 우선 순위 설정하기
@Primary 어노테이션을 사용하면 만약 특정 타입의 Bean이 두 개 이상 있을 때 기본 값으로 설정할 Bean을 설정할 수 있다. 예를 들어 UserRepository중 InMemoryUserRepository를 기본 Bean으로 설정하고 싶다면 다음과 같이 작성하면 된다.
@Configuration
class UserContainerConfiguration {
@Primary
@Bean
fun inMemoryUserRepository(): UserRepository {
return InMemoryUserRepository()
}
@Bean
fun dbUserRepository(): UserRepository {
return DbUserRepository()
}
@Bean
fun userCreator(repository: UserRepository): UserCreator {
return UserCreator(repository)
}
}
그러면 UserCreator가 생성될 때 주입되는 UserRepository는 InMemoryUserRepository가 된다.
이를 확인하기 위해 다음 코드를 실행해보자.
fun main(args: Array<String>) {
val context = AnnotationConfigApplicationContext(UserContainerConfiguration::class.java)
context.getBean(UserCreator::class.java).createUserWithRandomId("세영")
}
그러면 다음과 같은 결과가 나오는 것을 볼 수 있다. 유저가 InMemoryUserRepository에서 생성됐다.
하지만, @Primary 어노테이션을 사용하는 방식만으로는 하나의 타입에 하나의 주요 Bean만 사용할 수 있게 만드는 한계가 있다. 만약 InMemoryUserRepository 뿐만 아니라 DbUserRepository도 사용하고 싶다면 @Primary만으로는 충분하지 않다.
이를 위해 @Qualifier 어노테이션이 필요하다. 이어서 @Qualifier 어노테이션에 대해 알아보자.
@Qualifier 사용해 우선 순위 설정하기
@Qualifier을 사용하면 Bean의 구분자를 설정할 수 있다. 예를 들어 DbUserRepository의 구분자를 dbUserRepository로 설정하고 싶다면 다음과 같이 작성하면 된다.
@Qualifier("dbUserRepository")
@Bean
fun dbUserRepository(): UserRepository {
return DbUserRepository()
}
이제 이렇게 만든 DbUserRepository를 UserCreator 생성 시 사용하고 싶다면 다음과 같이 함수의 UserRepository 파라미터 앞에 Qualifier을 붙여 주면 된다.
@Configuration
class UserContainerConfiguration {
@Primary
@Bean
fun inMemoryUserRepository(): UserRepository {
return InMemoryUserRepository()
}
@Qualifier("dbUserRepository")
@Bean
fun dbUserRepository(): UserRepository {
return DbUserRepository()
}
@Bean
fun userCreator(@Qualifier("dbUserRepository") repository: UserRepository): UserCreator {
return UserCreator(repository)
}
}
이제 UserCreator이 DbUserRepository를 잘 사용하고 있는지 확인하기 위해 이전에 만든 main 함수를 다시 실행해보자.
fun main(args: Array<String>) {
val context = AnnotationConfigApplicationContext(UserContainerConfiguration::class.java)
context.getBean(UserCreator::class.java).createUserWithRandomId("세영")
}
그러면 다음과 같은 결과가 나오는 것을 볼 수 있다.
UserCreator가 DbUserRepository를 사용해 유저 생성 동작을 실행하고 있다.
즉, @Primary는 기본으로 사용할 Bean을 설정하기 위한 어노테이션이고, @Qualifier은 Bean을 구분하기 위해 사용되는 어노테이션이다.
여기까지 @Primary와 @Qualifier 사용해 하나의 인터페이스에 둘 이상의 구현체가 있을 때 의존성 주입 우선 순위를 관리하는 방법을 알아봤다.
전체 코드: GitHub
이 프로젝트가 도움이 되셨다면 저장소에 Star⭐️를 눌러주세요! Stargazers는 다음 페이지에서 확인할 수 있습니다.