김찬진의 개발 블로그

[23/04/11] 수동의존관계주입 VS 자동의존관계주입 본문

1일1배움/Spring (김영한 님)

[23/04/11] 수동의존관계주입 VS 자동의존관계주입

kim chan jin 2023. 4. 11. 16:07

수동 의존관계주입 (AppConfig.java)

1. 구성 정보(@Configuration) 작성, 수동 의존관계 표현 (미래형 표현에 주의. 아직 수행되지 않음)

- AppConfig클래스에서 @Configuration 어노테이션을 사용함으로써 자신(AppConfig클래스)이 스프링 컨테이너를 만들 때 필요한 구성 정보임을 표시합니다. 아직 스프링 컨테이너는 만들어지지 않았습니다. 
- @Configuration 어노테이션은 CGLIB라는 바이트코드 조작 라이브러리를 사용하여 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들 것 입니다. AppConfig 클래스는 싱글턴 패턴의 임의의 클래스만들어질 것입니다. 역시 아직 스프링 컨테이너는 만들어지지 않았고 Spring Bean 또한 만들어지지 않았습니다.
- AppConfig클래스에서 각 인터페이스 생성자에 @Bean 어노테이션을 사용하여 구성 정보에 들어갈 내용을 작성합니다. AppConfig클래스에서 각 인터페이스의 생성자는 파라미터를 받지 않고 구현체 객체를 반환하도록 작성합니다. 이것이 런타임 시 구성될 Spring Bean간의 의존관계를 표현하는 과정입니다. @Bean 하나당 BeanDefinition(빈 설정 메타정보) 하나가 생성될 것 입니다. 아직 스프링 컨테이너는 만들어지지 않았고 Spring Bean 또한 만들어지지 않았습니다.

2. 스프링 컨테이너 생성 및 객체(Spring Bean) 등록 (이제부터 수행됨)

- new AnnotationConfigApplicationContext(AppConfig.class)를 작성함으로써 스프링 컨테이너 객체를 만들고, 스프링 컨테이너(엄밀히 말하면 ApplicationContext 도 아니고, AnnotationConfigApplicationContext도 아닌, AnnotatedBeanDefinitionReader)는 AppConfig.class(또는 AppConfig.xml 등)을 인자로 받습니다. 즉, 싱글턴 패턴의 임의의 클래스를 구성 정보로 읽습니다.
- 싱글턴 패턴의 임의의 클래스BeanDefinition(빈 설정 메타정보)에 따라 Spring Bean을 생성하고 Key와 Value의 형태로 컨테이너에 등록합니다. 메서드의 이름을 Spring Bean 이름(Key)으로, 메서드의 반환값을 Spring Bean 객체(Value)로 설정합니다.

3. 런타임 시 Spring Bean간의 의존관계 주입

- 구성 정보를 읽은 컨테이너(AnnotationConfigApplicationContext 등) 객체를 만듭니다. 구성 정보대로 Spring Bean을 컨테이너에 만듭니다. 표현했었던 의존관계대로 컨테이너 내에서 Spring Bean 을 찾아서 각 Spring Bean간의 의존관계를 주입합니다.

 

 

 

자동 의존관계주입 (AutoAppConfig.java)

1. 구성 정보(@Configuration) 작성 (미래형 표현에 주의. 아직 perform되지 않음)

- AutoAppConfig클래스에서 @Configuration 어노테이션을 사용함으로써 자신(AppConfig클래스)이 스프링 컨테이너를 만들 필요한 구성 정보임을 표시합니다. 수동 의존관계주입일 때에는 AppConfig클래스에 각 인터페이스의 생성자가 인자를 받지 않고 구현체 객체를 반환하여 런타임 시에 구성될 의존관계를 표현했던 반면, AutoAppConfig클래스는 그러한 과정을 할 필요가 없습니다. 아직 스프링 컨테이너는 만들어지지 않았습니다. 
- @Configuration 어노테이션은 CGLIB라는 바이트코드 조작 라이브러리를 사용하여 AutoAppConfig 클래스를 상속받은 임의의 다른 클래스를 만들 것입니다. AutoAppConfig 클래스는 싱글턴 패턴의 임의의 클래스만들어질 것입니다. 역시 아직 스프링 컨테이너는 만들어지지 않았고 Spring Bean 또한 만들어지지 않았습니다.
- AutoAppConfig 클래스에서 @ComponentScan 어노테이션을 사용합니다. 스프링이 컨테이너를 만들 때 @Component 어노테이션이 붙은 구현체 클래스들을 구성정보의 내용(Spring Bean)으로 스캔할 것이고 읽을 것입니다. 즉, @Configuration 어노테이션이 붙은 클래스(AutoAppConfig클래스)는 스프링이 컨테이너를 만들 때 읽을 구성 정보이고 @Component 어노테이션이 붙은 클래스들은 그 구성정보 안에 들어갈 내용입니다.

2. 구성 정보의 내용(@Component) 알리기

- Spring Bean으로 등록하길 원하는 구현체 클래스들에게 @Component 어노테이션을 붙입니다. 스프링이 @Component 어노테이션이 붙은 클래스들을 스캔하여 런타임 시에 @Component 어노테이션이 붙은 구현체 클래스들을 Spring Bean으로 등록할 것입니다. 그 때, 클래스의 이름에서 앞글자만 소문자로 바꾼 이름을 Spring Bean 이름(Key)으로, 클래스 이름을 Spring Bean 객체(Value)로 설정될 것입니다. 역시 아직 스프링 컨테이너는 만들어지지 않았고 Spring Bean 또한 만들어지지 않았습니다.  

3. Component간의 자동 의존관계 표현(@Autowired)

- 구현체 클래스에서 구현체 클래스의 생성자에 @Autowired 어노테이션을 사용합니다. 생성자의 파라미터로는 인터페이스 타입이어야 하고, 그 파라미터를 인스턴스 변수에 대입합니다. 물론 이전에 인스턴스 변수를 선언해줬어야 합니다.

- @Autowired 어노테이션는 말 그대로 "스프링에 의해 자동으로 의존관계가 주입된" 것으로 보면 됩니다. 구현체 클래스에서 구현체 클래스의 생성자에 @Autowired 어노테이션을 붙이면, 그 생성자의 파라미터로 들어온 인터페이스 타입 객체에 해당하는 Spring Bean을 컨테이너에서 찾아서 주입시킬 것 입니다. (컨테이너에 해당 Spring Bean이 하나라면 괜찮겠지만 만약 여러 개라면 오류가 날 수 있으니 따로 설정해줘야 합니다.)

- 다시 한번 더 정리하자면, @Component 어노테이션이 붙은 구현체 클래스는 구성정보이고, 그 안의 구현체 클래스의 생성자는 구성정보의 내용입니다.

4. 런타임 시 Spring Bean간의 의존관계 주입 (이제부터 수행됨)

- 구성 정보(AutoAppConfig클래스)를 읽고 컨테이너(AnnotationConfigApplicationContext 등) 객체를 만듭니다. 구성 정보대로 Spring Bean을 컨테이너에 만듭니다. 표현했었던 의존관계대로 컨테이너 내에서 Spring Bean 을 찾아서 각 Spring Bean간의 의존관계를 주입합니다.

 

 

 

다음은 아직 남아있던 궁금증을 ChatGPT에게 물어본 내용입니다.

 

나:

@Bean 하나당 BeanDefinition(빈 설정 메타정보) 하나가 생성될 것 입니다." 라는 말에서 질문이 생겼어. 자동 의존관계주입은 @Bean 이라는 어노테이션을 사용하지 않는데, 그럼 자동 의존관계주입에서 BeanDefinition은 필요없는거야?



GPT:

자동 의존관계주입은 @Bean 어노테이션을 사용하지 않지만, 스프링 컨테이너가 자동으로 빈을 등록하고 관리하기 위해 BeanDefinition을 사용합니다. BeanDefinition은 스프링 컨테이너에 등록되는 빈의 설정 메타정보를 담고 있으며, 이를 바탕으로 스프링 컨테이너가 빈을 생성하고 의존성을 주입합니다.

자동 의존관계주입은 다양한 방법으로 수행될 수 있지만, 대표적으로는 @Autowired 어노테이션을 사용하는 방법이 있습니다. @Autowired 어노테이션은 스프링 컨테이너가 빈을 생성할 때 해당 빈이 필요로 하는 의존성을 자동으로 주입합니다. 이때 주입되는 빈들도 모두 BeanDefinition을 가지고 있으며, 스프링 컨테이너가 이를 활용하여 의존성을 주입합니다.


따라서, 자동 의존관계주입에서도 BeanDefinition은 여전히 필요합니다. 빈 설정 메타정보를 담고 있는 BeanDefinition은 스프링 컨테이너가 빈을 생성하고 관리하는데 핵심적인 역할을 수행합니다.


나:

근데 @Autowired 어노테이션이 붙은 구현체 생성자는 파라미터가 모두 인터페이스 타입인데, 구현체에 대한 의존관계가 어떻게 형성되는거야?

 

GPT:

@Autowired 어노테이션이 붙은 구현체 생성자의 인터페이스 타입 파라미터에 대해 의존관계를 형성하기 위해서는 스프링 컨테이너에 해당 인터페이스를 구현한 빈이 등록되어 있어야 합니다.

예를 들어, 다음과 같은 코드가 있다고 가정해보겠습니다.

public interface MyInterface { // ... } 

public class MyImplementation implements MyInterface { // ... } 

public class MyClass { 
    private MyInterface myInterface; 
    
    @Autowired 
    public MyClass(MyInterface myInterface) {
    	this.myInterface = myInterface; 
    } 
    // ... 
}


위 코드에서 MyClass는 MyInterface 타입의 myInterface 멤버 변수를 가지며, MyInterface 타입의 파라미터를 갖는 생성자가 @Autowired 어노테이션으로 주입됩니다. 이때 스프링 컨테이너는 MyInterface를 구현한 MyImplementation 빈을 등록하고, MyClass의 생성자에 주입합니다.

따라서, @Autowired 어노테이션이 붙은 구현체 생성자의 인터페이스 타입 파라미터에 대한 의존관계는 스프링 컨테이너가 해당 인터페이스를 구현한 빈을 등록하고, 생성자에 주입함으로써 형성됩니다.


나:

근데 MyInterface를 구현한 구현체 클래스가 MyImplementation만 존재하지 않을 수 있잖아. 그럼 어떤 구현체 클래스와 의존관계를 맺어야할지 모른다면 문제인 거 아니야?


GPT:

맞습니다. @Autowired 어노테이션을 사용하면 인터페이스 타입 파라미터에 대한 구현체 클래스와의 의존관계를 명시적으로 지정하지 않아도 되기 때문에, 어떤 구현체 클래스와 의존관계를 맺어야 할지 알 수 없는 상황이 발생할 수 있습니다.

이런 경우, 스프링은 다양한 방식으로 의존관계를 해결할 수 있는데, 그 중에서 가장 일반적인 방법은 @Qualifier 어노테이션을 사용하는 것입니다. @Qualifier 어노테이션을 사용하면 같은 인터페이스를 구현한 여러 클래스 중에서 어떤 클래스와 의존관계를 맺어야 하는지 지정할 수 있습니다.

예를 들어, 위에서 예로 든 코드에서 MyInterface를 구현한 또 다른 클래스 MyAnotherImplementation이 존재한다고 가정해보겠습니다.

public class MyAnotherImplementation implements MyInterface { // ... }


이 경우, MyClass의 생성자에 MyInterface를 구현한 클래스가 둘 이상 존재하기 때문에, @Qualifier 어노테이션을 사용하여 어떤 클래스와 의존관계를 맺어야 하는지 지정해주어야 합니다.

public class MyClass { 
    private MyInterface myInterface; 
    
    @Autowired 
    public MyClass(@Qualifier("myImplementation") MyInterface myInterface) {
        this.myInterface = myInterface; 
    } 
    // ... 
}


위 코드에서 @Qualifier 어노테이션의 값으로 "myImplementation"을 지정하여, MyImplementation 클래스와 의존관계를 맺도록 지정하였습니다. 따라서, 스프링 컨테이너는 MyImplementation 빈을 생성하여 MyClass의 생성자에 주입합니다.

 

Comments