Spring 의존 자동 주입

Spring 의존 자동 주입

의존 자동 주입은 스프링 설정 파일에서 객체를 주입할 때, 명시를 하지 않아도 스프링 컨테이너가 자동으로 필요한 의존 대상 객체를 찾아서 필요한 객체에게 주입해주는 기능이다.

여기에서는 타입이 같은 빈 객체가 설정하고 자동으로 주입을 받게 되었을 시에 원하지 않는 객체를 받아오는 경우가 있다. 어떤한 경우에 그렇게 되는지 예제 코드를 통해서 알아보도록 하자.

프로젝트 생성

먼저, 아래와 같이 curl 명령어을 사용하여 Spring Boot 초기 프로젝트를 생성한다.

curl https://start.spring.io/starter.tgz \
-d bootVersion=2.6.6 \
-d baseDir=kotlin-spring-bean-injection \
-d groupId=com.devkuma \
-d artifactId=kotlin-spring-bean-injection \
-d packageName=com.devkuma.bean.injection \
-d applicationName=BeanInjectionApplication \
-d packaging=jar \
-d language=kotlin \
-d jvmVersion=11 \
-d type=gradle-project | tar -xzvf -

위 명령어에서는 특별히 의존성을 넣지 않았다.

타입이 같은 빈 객체가 1개인 경우

타입 같은 빈 객체가 중복으로 존재하지 않고 1개만 있는 경우이다.

아래는 빈으로 만들 클래스이다.

package com.devkuma.bean.injection.ex1

class FooBean1(
    val id: Int,
    val name: String
)

설정 객체에서는 FooBean1 클래스로 빈을 1개만 설정을 하였다.

package com.devkuma.bean.injection.ex1

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class FooBeanConfig1 {

    @Bean
    fun fooBean1() =
        FooBean1(
            id = 1,
            name = "fooBean1"
        )
}

서비스 객체에서는 설정으로 만든 FooBean1 타입의 빈을 주입 받는다.

package com.devkuma.bean.injection.ex1

import org.springframework.stereotype.Service

@Service
class FooService1(
    private var fooBean: FooBean1
) {
    fun printBean() {
        println("fooBean: id=${fooBean.id}, name=${fooBean.name}")
    }
}

이제 printBean 메서드를 호출해서 객체를 표시해 본다.

package com.devkuma.bean.injection

import com.devkuma.bean.injection.ex1.FooService1
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class BeanInjectionApplication(
    private val fooService1: FooService1,
) : CommandLineRunner {

    override fun run(vararg args: String?) {
        println("Ex 1) fooService1 ---------")
        fooService1.printBean()
    }
}

fun main(args: Array<String>) {
    runApplication<BeanInjectionApplication>(*args)
}

실행 결과:

Ex 1) fooService1 ---------
fooBean: id=1, name=fooBean1

예상 했듯이 설정에서 넣은 id, name이 표시되었다.

타입이 같은 빈 객체가 2개인 경우

@Primary, @Qualifier 사용함

두번째로는 타입이 같은 빈 객체를 2개를 만들어서 @Primary, @Qualifier 사용하는 설정하고, 주입 받는 곳에서 @Qualifier 사용한 경우와 사용하지 않는 경우를 보도록 하겠다.

빈으로 만들 클래스을 아래와 같이 작성한다.

package com.devkuma.bean.injection.ex2

class FooBean2(
    val id: Int,
    val name: String
)

설정 객체에서는 FooBean2 클래스로 빈을 2개를 설정하였다. 1개의 객체는 @Primary 어노테이션을 설정하였고, 또 다른 1개에는 @Qualifier 어노테이션에 빈 이름을 설정하였다.

package com.devkuma.bean.injection.ex2

import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary

@Configuration
class FooBeanConfig2 {

    @Primary
    @Bean
    fun fooBean20() =
        FooBean2(
            id = 20,
            name = "fooBean20"
        )

    @Qualifier("fooBean21")
    @Bean
    fun fooBean21() =
        FooBean2(
            id = 21,
            name = "fooBean21"
        )
}

서비스 객체에서는 설정으로 만든 FooBean2 타입의 빈 2개를 각각 주입 받았다.

package com.devkuma.bean.injection.ex2

import org.springframework.stereotype.Service

@Service
class BadFooService2(
    private val fooBean20: FooBean2,
    private val fooBean21: FooBean2
) {
    fun printBean() {
        println("fooBean20: id=${fooBean20.id}, name=${fooBean20.name}")
        println("fooBean21: id=${fooBean21.id}, name=${fooBean21.name}")
    }
}

또 다른 서비스 객체에서는 설정으로 만든 FooBean2 타입의 빈 2개를 각각 주입 받았는데, @Qualifier를 사용하여 주입할 빈의 이름을 명시하였다.

package com.devkuma.bean.injection.ex2

import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Service

@Service
class GoodFooService2(
    @Qualifier("fooBean20") private val fooBean20: FooBean2,
    @Qualifier("fooBean21") private val fooBean21: FooBean2
) {
    fun printBean() {
        println("fooBean20: id=${fooBean20.id}, name=${fooBean20.name}")
        println("fooBean21: id=${fooBean21.id}, name=${fooBean21.name}")
    }
}

이제 printBean 메서드를 호출해서 객체를 표시해 본다.

package com.devkuma.bean.injection

import com.devkuma.bean.injection.ex2.BadFooService2
import com.devkuma.bean.injection.ex2.GoodFooService2
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class BeanInjectionApplication(
    private val badFooService2: BadFooService2
) : CommandLineRunner {

    override fun run(vararg args: String?) {
        println("Ex 2) badFooService2 ------")
        badFooService2.printBean()
    }
}

fun main(args: Array<String>) {
    runApplication<BeanInjectionApplication>(*args)
}

실행 결과:

Ex 2) badFooService2 ------
fooBean20: id=20, name=fooBean20
fooBean21: id=20, name=fooBean20

결과를 보면 첫번째 badFooService2인 경우에는 fooBean20, fooBean21 빈의 내용이 동일하게 표시가 되었다. fooBean21에 fooBean20의 빈 내용이 표시된 것이다.

두번째 goodFooService2인 경우에는 @Qualifier로 명시한 대로 빈의 내용이 표시되고 있는 것을 뵬수 있다.

@Primary, @Qualifier 사용 않함

마지막으로 타입이 같은 빈 객체를 2개를 만들어서 @Primary, @Qualifier를 설정하지 않는 경우를 보도록 하겠다.

빈으로 만들 클래스을 아래와 같이 작성하였다.

package com.devkuma.bean.injection.ex3

class FooBean3(
    val id: Int,
    val name: String
)

설정 객체에서는 FooBean3 클래스로 빈을 2개를 설정하였다. 위에서 언급한 대로 @Primary, @Qualifier은 따로 설정하지 않았다.

package com.devkuma.bean.injection.ex3

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class FooBeanConfig3 {

    @Bean
    fun fooBean31() =
        FooBean3(
            id = 30,
            name = "fooBean31"
        )

    @Bean
    fun fooBean32() =
        FooBean3(
            id = 31,
            name = "fooBean32"
        )
}

서비스 객체에서는 설정으로 만든 FooBean3 타입의 빈 2개를 각각 주입 받았는데, 주입시에 @Qualifier를 사용하지 않았다.

package com.devkuma.bean.injection.ex3

import org.springframework.stereotype.Service

@Service
class FooService3(
    private var fooBean31: FooBean3,
    private var fooBean32: FooBean3
) {
    fun printBean() {
        println("fooBean31: id=${fooBean31.id}, name=${fooBean31.name}")
        println("fooBean30: id=${fooBean32.id}, name=${fooBean32.name}")
    }
}

이번에도 printBean 메서드를 호출해서 객체를 표시해 본다.

package com.devkuma.bean.injection

import com.devkuma.bean.injection.ex3.FooService3
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class BeanInjectionApplication(
    private val goodFooService2: GoodFooService2
) : CommandLineRunner {

    override fun run(vararg args: String?) {
        println("Ex 3) fooService3 ---------")
        fooService3.printBean()
    }
}

fun main(args: Array<String>) {
    runApplication<BeanInjectionApplication>(*args)
}

실행 결과:

Ex 3) fooService3 ---------
fooBean31: id=30, name=fooBean31
fooBean30: id=31, name=fooBean32

결과를 보면 fooBean31, fooBean32 둘다 설정에서 넣은대로 id, name이 표시되었다.

정리

  1. 타입이 같은 빈 객체가 한개면 그 빈 객체를 사용한다.
  2. 타입이 같은 빈 객체가 두개 이상이면, @Primary@Qualifier와 설정이 있다면 같은 값을 갖는 빈 객체를 찾고 존재하면 그 객체를 사용한다.
  3. 타입이 같은 빈 객체가 두개 이상이고, @Qualifier@Primary 설정이 없다면 @Bean 메서드 명이 같은 빈 객체를 찾는다. 존재하면 그 객체를 사용한다.

결론

@Primary@Qualifier는 꼭 필요한 경우가 아니라면 사용하지 않고 @Bean 메서드 명과 주입받은 변수명을 맞추기를 추천한다.

위에 예제 코드는 GitHub에서 확인해 볼 수 있다.