본문 바로가기

STUDY/스프링 철저 입문

1-1) 스프링 의존성 주입(DI), 제어의 역전(IoC) : @Bean, @Component, @Autowired, @Primary, @Qualifier

- 의존성 주입(Dependency Injection, DI), 제어의 역전(Inversion of Control, IoC) 이란

 

큰 규모의 프로젝트를 다수와 협업하여 개발하다보면 여러 문제 상황에 직면하게 된다. 규모가 커짐에 따라, 컴포넌트의 수가 많아지고, 컴포넌트들 간의 결합 역시 복잡해지게 된다. 예를 들어서 다음과 같은 코드를 보자.

 

class UserService {
    private Encoder encoder;
    ...

    public UserService(Encoder encoder){
        this.encoder = encoder;
    }
    ...
}

 

...
Encoder encoder = new MyEncoder();
UserService dao = new UserService(encoder);
...

 

User관련 기능을 하는 UserService가 있다. 여기서 UserService는 유저정보를 암호화 하기위해 Encoder를 가지고 있고, UserService를 만들때 Encoder도 함께 만들어서 전달해 주었다. 그런데 프로젝트 중간에 MyEncoder가 아니라 다른 Encoder를 사용하게 변경된다면 어떨까?

 

...
Encoder encoder = new AnotherEncoder();
UserService svc = new UserService(encoder);
...

 

다음과 같이 AnotherEncoder으로 코드를 바꾸어 주면 될까? 하지만 UserService와 같이 Encoder를 사용하는 컴포넌트가 한두개가 아니라면? 그것들을 모두 바꾸기에는 많은 시간과 노력이 필요할 것이다. 이와같은 문제의 근본적인 원인은 컴포넌트가 그 안에 필요한 다른 컴포넌트에 의존성이 생기고, 이를 개발자가 직접 관리한다는 것이다.

 

이와같이 컴포넌트가 동작하는데 필요한 컴포넌트를 생성하여 전달해주는 것을 의존성 주입(Dependency Injection)이라고 한다. 그리고 스프링에서는 의존성 주입을 개발자가 아닌 DI컨터이너가 대신 해주고, 이를 제어의 역전(Inversion of Control)이 이루어졌다고 한다. 이것이 DI와 IoC의 개념이다.

 

 

- 컴포넌트 등록하고 사용하기 : @Bean, @Component, @Autoweird

 

이제 개발자는 스프링에서 DI컨테이너 역할을 하는 ApplicationContext를 만들고, 여기에 관리될 컴포넌트들을 등록하기만 하면 된다. 이를 구현하는 방법은 크게 2가지가 있다. 

  • 자바 기반 설정 방식
  • Annotaion 기반 설정 방식

먼저 자바 기반 설정 방식의 구현 코드를 보면 다음과 같다.

 

@Configuration
public class AppContextConfig {
    @Bean
    public Encoder encoder(){
        return new AnotherEncoder();
    }
    ...
}

 

...
ApplicationContext context = new AnnotationConfigApplicationContext(AppContextConfig.class);
Encoder encoder = context.getBean(Encoder.class);
UserService svc = new UserService(encoder);
...

 

DI컨테이너의 설정인 AppContextConfig 클래스를 만들고 Encoder 타입의 빈을 등록하였다. 그리고 Encoder가 필요할 때, AppicationContext의 getBean() 메소드를 사용하여 불러온다.

 

다음으로 Annotation 기반 설정 방식의 구현 코드를 보면 다음과 같다.

 

@Configuration
@ComponentScan({"Component"})
public class AppContextConfig {
    ...
}

 

package Component;
...
@Component("encoder")
public class MyEncoder implements Encoder{
	...
}

 

...
ApplicationContext context = new AnnotationConfigApplicationContext(AppContextConfig.class);
Encoder encoder = context.getBean("encoder");
UserService svc = new UserService(encoder);
...

 

 

AppContext 클래스를 만들고 @ComponentScan을 사용해 Component 패키지에서 @Component가 붙어있는 MyEncoder를 "encoder"라는 이름으로 등록한다. 그리고 Encoder가 필요할 때, AppicationContext의 getBean() 메소드를 사용하여 불러온다.

 

지금까지 컴포넌트를 등록하고 getBean() 메소드를 사용하여 의존성을 주입하는 방법을 알아보았다. 하지만 DI 컨테이너는 이름 그대로 의존성 주입(DI)을 관리해주는 객체이다. 의존성을 주입하는 방법에는 크게 3가지 방법이 있다.

  • 필드 의존성 주입
  • 생성자 의존성 주입
  • 세터 의존성 주입

먼저 필드 의존성 주입의 코드를 보면 다음과 같다.

 

class UserService {
    @Autowired
    private Encoder encoder;
    ...

    public UserService(Encoder encoder){
        this.encoder = encoder;
    }
    ...
}

 

UserService의 encoder 필드에 @Aurowired 어노테이션을 사용하였다. DI 컨테이너는 UserServiceO를 생성하고, 등록된 Encoder 컴포넌트를 찾아서 의존성을 주입해준다. 필드 의존성 주입은 직관적이고 사용법이 간단하여 자주 활용되는 방식이다.

 

먼저 생성자 의존성 주입의 코드를 보면 다음과 같다.

 

class UserService {
    private Encoder encoder;
    ...
	
    @Autowired
    public UserService(Encoder encoder){
        this.encoder = encoder;
    }
    ...
}

 

UserService의 생성자에 @Aurowired 어노테이션을 사용하였다. DI 컨테이너는 UserService를 생성할 때, 등록된 Encoder 컴포넌트를 찾아서 생성자의 파라미터로 주입해준다. 생성자 의존성 주입은 대상 필드가 final로 선언되어 있어도 사용할 수 있고, 순환 의존성이 발생하면 에러를 발생시켜준다.

 

세터 의존성 주입의 코드를 보면 다음과 같다.

 

class UserService {
    private Encoder encoder;
    ...
	
    public UserService(Encoder encoder){
        this.encoder = encoder;
    }
    ...
    
    @Autowired
    public setEncoder(Encoder encoder){
        this.encoder = encoder;
    }
}

 

 

UserService의 세터에 @Aurowired 어노테이션을 사용하였다. DI 컨테이너는 UserService를 생성하고, 등록된 Encoder 컴포넌트를 찾아서 의존성을 주입해준다.

 

 

- 중복된 타입의 컴포넌트 관리 : @Primary, @Qualifier

 

만약 등록된 Encoder 컴포넌트가 2개 이상일 경우, @Primary나 @Qualifier 어노테이션을 사용하여 주입할 컴포넌트를 특정할 수 있다.

 

@Configuration
public class AppContext {
    @Bean("MyEncoder")
    @Primary
    public Encoder encoder(){
        return new MyEncoder();
    }
    @Bean("AnotherEncoder")
    public Encoder encoder(){
        return new AnotherEncoder();
    }
    ...
}

 

class UserService {
    @Autowired
    private Encoder encoder;
    ...
	
    public UserService(Encoder encoder){
        this.encoder = encoder;
    }
    ...
}

 

위의 코드는 2개의 Encoder 컴포넌트 중 @Primary인 MyEncoder 컴포넌트가 주입된다.

 

class UserService {
    @Autowired
    @Qualifier("AnotherEncoder")
    private Encoder encoder;
    ...
	
    public UserService(Encoder encoder){
        this.encoder = encoder;
    }
    ...
}

 

위의 코드는 2개의 Encoder 컴포넌트 중 "AnotherEncoder"인 AnotherEncoder컴포넌트가 주입된다.

 

이와 같은 방식으로 의존성을 관리하면, 하나의 컴포넌트를 변경하기 위해 여러줄의 코드를 수정하지 않아도 된다.  코드의 유연성, 유지보수 측면에서 DI 컨테이너와 IoC 아주 중요한 개념이다.

728x90