IoC(Inversion of Control) 맛보기

Posted by yunki kim on April 20, 2022

  다음과 같은 코드를 보자.

1
2
3
4
class OwnerController {
    private OwnerRepository repository = new OwnerRepository();
}
 
cs

  이 코드는 자신이 사용할 의존성(repository)를 자신이 만들어 사용하고 있다. 하지만 다음과 코드는 자신이 사용할 의존성을 생성자를 통해 받아오고 있다.

1
2
3
4
5
6
7
8
class OwnerController {
    private OwnerRepository repository;
 
    public OwnerController(OwnerRepository repo) {
        this.repository = repo;
    } 
}
 
cs

  OwnerController는 자신의 의존성을 관리하지 않는다. 즉, 제어권이 자신에게 있지 않고 역전되었다. 위 코드처럼 의존성을 주입하는 것을 의존성 주입(DI - Dependency Injection)이라 하며 이 역시 IoC의 하나다.

  스프링에서 DI가 어떻게 사용되는지 간단히 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Controller
class OwnerController {
    ...
 
    @Autowired
    private OwnerRepository owners;
 
    @Autowired
    public OwnerController(OwnerRepository clinicService, VisitRepository visits) {
        this.owners = clinicService;
        this.visits = visits;
    }
 
    ...
}
 
@WebMvcTest(OwnerController.class)
class OwnerControllerTests {
    ...
 
    @MockBean
    private OwnerRepository owners;
 
    @MockBean
    private VisitRepository visits;
 
    ...
}
cs

 

  OqnwerController는 생성자로 의존성을 주입받고 있고 OwnerControllerTests에서 해당 인스턴스들을 빈으로 등록해 의존성을 주입하고 있다. 여기서 빈은 간단히 말해 스프링이 관리하는 객체들이다. @MockBean 어노테이션을 사용하면 테스트 시 해당 타입의 인스턴스를 스프링이 자동으로 만들고 빈으로 등록된다. 빈으로 등록된 객체들은 스프링에 존재하는 IoC 컨테이너가관리한다. 즉, IoC 컨테이너가 빈으로 등록된 객체들을 활용해 필요한 의존성을 주입해 준다.

IoC 컨테이너

  IoC 컨테이너는 프레임워크들이 공통적으로 가지고있는 특징이며 IoC를 적용하기 위해 사용된다. 스프링 프레임워크에서는 ApplicationContext와 BeanFactory가 이 역할을 한다. BeanFactory가 사실상 IoC 컨테이너이고 ApplicationContext가 BeanFactory를 implements하고 있기 때문에 같은 역할을 하고 있다고 볼 수 있다.

  스프링 프레임워크가 제공하는 IoC 컨테이너는 빈을 만들고 엮어주며 제공하는 역할을 한다. 특정 클래스를 IoC 컨테이너에 빈으로 등록하기 위해서는 특정 어노테이션을 사용하거나 특정 인터페이스를 implements 해야 한다. 등록된 빈들을 서로의 의존성을 IoC 컨테이너가 주입해 준다. 즉, 스프링의 IoC 컨테이너는 등록되 있는 빈들 끼리만 자동으로 의존성을 주입해준다.

  위에서 언급했듯 스프링의 IoC 컨테이너는 자동으로 의존성을 주입해 주기 때문에 ApplicationContext를 직접적으로 사용할 일은 많지 않다. 스프링 IoC 컨테이너가 자동으로 의존성을 주입 한다는 것을 다음과 같은 예제에서 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Controller
class OwnerController {
 
    private OwnerRepository owners;
    private ApplicationContext applicationContext;
 
 
    @Autowired
    public OwnerController(OwnerRepository clinicService, ApplicationContext applicationContext) {
        this.owners = clinicService;
        this.applicationContext = applicationContext;
    }
 
    @GetMapping("/bean")
    @ResponseBody
    public String bean() {
        return "bean: " + applicationContext.getBean(OwnerRepository.class+ "\n"
            + "owners: " + this.owners;
 
    }
}
cs

localhost:8080/bean

  두 방식으로 가져온 인스턴스가 같다. 이를 바꿔 말하면 스프링 프레임워크를 빈으로 등록한 객체를 하나의 프로그램 전반에서 하나의 인스턴스로 사용한다는 것을 알 수 있다. 이를 싱글톤 스코프라 한다.

빈(Bean)

  IoC 컨테이너가 관리하는 객체를 빈이라 한다. 객체를 빈으로 등록하는 방식은 크게 두 가지가 있다.

1. Compoenent Scanning

 어노테이션 프로세스 중 스프링 IoC 컨테이너를 만들고 사용하는 여러 가지 인터페이스들이 존재한다. 이런 인터페이스들을 life cycle callback이라 한다. 그리고 이런 life cycle callback 중에서는 @Component 어노테이션을 사용하는 클래스들을 찾아서 인스턴스를 생성하고 빈으로 등록하는 어노테이션 프로세서가 등록되 있다.

  스프링 부트의 경우 이 어노테이션 프로세서를 @SpringBootApplication 내에서 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
...
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    ...
}
cs

  @CompoenentScan은 어느 부분 부터 컴포넌트를 찾을지를 결정한다.@CompoenentScan 어노테이션을 사용한 위치 부터 모든 하위 패키지에 존재하는 모든 클래스를 스캔해 @Component를 사용하는 모든 클래스를 찾아 빈으로 등록한다.

  @Repository, @Service, @Controller, @Configuration 등의 어노테이션은 내부적으로 @Compoenent 어노테이션을 사용하고 있어 이들을 사용한 클래스 역시 빈으로 등록된다.

2. XML이나 자바 설정 파일에 등록

  자바 설정 파일을 이용해 등록하는 예제는 다음과 같다.

1
2
3
4
5
6
7
8
@Configuration
public class SampleConfig {
    
    @Bean
    public SampleController sampleController() {
        return new SampleController();
   } // SampleController를 빈으로 등록
}
cs

  이런식으로 @Configuration과 @Bean을 이용해 등록을 하면 @Compoenent 어노테이션을 별도로 명시하지 않아도 빈으로 등록된다.

의존성 주입

  @AutoWired 어노테이션을 사용하면 의존성을 주입할 수 있다. 생성자를 통해 의존성을 주입하는 방식은 다음과 같다.

1
2
3
4
5
@Autowired
public OwnerController(OwnerRepository clinicService, ApplicationContext applicationContext) {
    this.owners = clinicService;
    this.applicationContext = applicationContext;
}
cs

  생성자를 통해 의존성을 주입할 때 만약 해당 클래스에 생성자가 한개이고 그 생성자로 주입받는 레퍼런스들이 빈으로 등록되 있다면 스프링 4.3 부터는 @Autowired 어노테이션을 생략할 수 있다.

  필드로 의존성을 주입받고 싶다면 필드에 @Autowired 어노테이션을 추가하면 된다.

1
2
3
4
5
6
7
@Controller
class OwnerController {
 
    @Autowired
    private OwnerRepository owners;
}
 
cs

  setter 로도 의존성을 주입할 수 있다. 이 방식으로 사용하면 스프링 IoC 컨테이너가 인스턴스를 생성하고 해당 인스턴스의 setter를 사용해 의존성을 주입한다.

1
2
3
4
5
6
7
8
9
10
11
@Controller
class OwnerController {
 
    private OwnerRepository owners;
 
    @Autowired
    public void setOwners(OwnerRepository owners) {
        this.owners = owners;
    }
}
 
cs

  생성자, 필드, setter를 통한 주입 중 스프링은 생성자를 통한 의존성 주입을 권장하고 있다. 생성자를 이용해 의존성을 주입하며 필요한 의존성 없이 프로그램이 동작하는 경우를 막을 수 있다. 하지만, 이로 인해 순환 참조가 발생할 수 있다. 따라서 순환 참조가 발생할 경우가 있을 때만 필드, setter를 통한 의존성 주입을 사용해야 한다.