본문 바로가기

더 나은 엔지니어가 되기 위해/지금은 안쓰는 자바

[스프링 프레임워크 핵심 기술] IoC 컨테이너 1

인프런에서 백기선님의 스프링 프레임워크 핵심 기술 을 공부하며 개인적으로 정리한 글입니다.

스프링 IoC 컨테이너

IoC (Inversion of Control) 는 의존 관계 주입이라고 하며, 어떤 객체가 사용하는 의존 객체를 직접 만들어 사용하는 것이 아니라, 주입 받아 사용하는 방법을 말한다.

IoC 컨테이너는 스프링에서 쓰이는 여러 객체들을 생성, 관리하는 객체다.
여기서 IoC 컨테이너가 관리하는 객체들을 이라고 한다.

IoC 컨테이너 내부적으로, BeanFactory 객체를 통해, 빈 설정 소스로부터 빈 정의를 읽고, 빈을 구성 및 제공한다.
다음과 같이 객체를 의존성 주입을 통해 받고 싶으면, 해당 객체가 IoC 컨테이너에 등록되어 있어야 한다.

@Autowired
BookRepository bookrepository;

// @Autowired 를 통해 bookrepository 에 객체 주입.
// 사전에 BookRepository 가 IoC 컨테이너에 빈 등록되어 있어야 함.

실제로 우리가 주로 사용하게 될 BeanFactoryApplicationContext 라는 interface 다.

1) 개념

스프링 IoC 컨테이너가 관리하는 객체

  • 의존성 관리가 쉽게 가능하다.
    • Mock 을 이용한 테스트 시에 용이함.
  • 스코프(싱글톤 or 프로토타입) 설정이 쉽게 가능하다.
  • 라이프사이클 인터페이스를 지원한다.
    • 객체 생성 전후 작업을 코딩할 수 있다. (예를 들면, @PostConstruct)

2) 빈 등록(설정) 방법

  • application.xml 을 통해 등록
  • @Configuration 를 적용한 클래스 내에서 @Bean 을 통해 등록
  • @ComponentScan@Component 를 통해 등록

현재 대부분은 마지막을 사용하므로, 구체적인 코드를 다루는 것은 생략.

@Autowired

1) required = true

@Autowired 는 객체의 타입에 해당하는 빈을 찾아 주입한다.

@Autowired
BookRepository bookrepository;

이 때 @Autowiredrequired 값은 true 가 기본 값으로, 주입받으려는 해당 객체가 빈 목록에 없으면 애플리케이션 구동에 실패한다.
만약 다음과 같이 required=false 로 주면, 객체 주입에 실패하더라도 애플리케이션을 구동시킨다.

@Autowired(required = false)
BookRepository bookrepository;

2) 위치

@Autowired 가 사용될 수 있는 위치는 다음과 같다.

  • 생성자
  • Setter
  • 필드

생성자나 Setter 는 파라미터로 넘어오는 객체에 의존성 주입을 한다.

3) 주입 객체가 여러 개인 경우

만약 주입 받으려는 객체의 정의가 여러 개인 경우는 어떻게 할까.
예를 들면 다음과 같은 상황이다.

@Repository
public class A_BookRepository implements BookRepository {
}
@Repository
public class B_BookRepository implements BookRepository {
}
@Service
public class BookService {

  @Autowired
  BookRpeository bookRepository;
}

@AutowiredBookRepository 를 주입받으려는데, 해당 BookRepository 를 구현한 클래스가 2개가 있다. 따라서 결과적으로 이 코드는 에러를 일으키며, 원인은 두 클래스 중 어떤 클래스를 주입시켜야하는지 정해지지 않았기 때문이다.

이를 해결하는 방법으론 다음과 같은 방법이 있다.

3.1) @Primary

다음과 같이 @Primary 를 표시함으로써, 여러 객체 중 어떤 것을 주입받을지 설정할 수 있다.

@Repository @Primary
public class A_BookRepository implements BookRepository {
}

3.2) @Qualifier

Qualifier("사용할 빈 클래스의 이름") 을 통해 지정해줄 수도 있다.
이 때, 클래스 이름의 첫 글자는 소문자가 되어야한다.

@Autowired @Qualifier("a_BookRepository")
BookRpeository bookRepository;

3.3) 여러 개의 빈 다 받기

다음과 같이 List 를 통해 받으면 해당 빈의 여러 클래스들을 다 받을 수 있다.

@Autowired
List<BookRepository> bookrepositories;

BeanPostProcessor

빈의 초기화 전후에 테스크를 수행하는 클래스를 정의한 인터페이스다.
BeanFactory 라이프 사이클 내에 포함되어 있다.

AutowiredAnnotationBeanPostProcessor 라는 클래스가 BeanPostProcessor 를 구현하고 있으며, 즉 빈의 초기화 작업 때, Autowired 와 관련된 작업들을 이 클래스가 수행하게 된다.

이와 연관된 어노테이션으로 @PostConstruct 가 있는데, 예를 들면 다음과 같다.

@Service
public class BookService {

  @Autowired
  BookRepository bookRepository;

  @PostConstruct
  public void setup() {
    // BookService 초기화(생성자 호출) 이후에 작업할 내용.
    // 생성자 호출 이후 이므로, bookRepository 는 이미 주입 되어있음.
  }
}

빈의 스코프

1) 싱글톤과 프로토타입

다음과 같이 일반적인 형태로 빈 정의를 하면 해당 빈은 싱글톤으로 생성된다.

@Component
public class Single {
}

@Scope("prototype") 을 붙여주면 해당 빈은 생성될 때마다 매번 다른 인스턴스를 생성한다.

@Component @Scope("prototype")
public class Proto {
}

실제로 생성된 객체를 확인해보면,

@Component
public class AppRunner implements ApplicationRunner {

  @Autowired
  ApplicationContext ctx;

  @Override
  public void run(ApplicationArguments args) throws Exception {

    System.out.println("Single")
    System.out.println(ctx.getBean(Single.class));
    System.out.println(ctx.getBean(Single.class));
    System.out.println(ctx.getBean(Single.class));

    System.out.println("Prototype")
    System.out.println(ctx.getBean(Proto.class));
    System.out.println(ctx.getBean(Proto.class));
    System.out.println(ctx.getBean(Proto.class));
  }
}

다음과 같이 객체 아이디가 동일하거나 다른 것을 확인할 수 있다.

Single
me.heumsi.Single@47371a45
me.heumsi.Single@47371a45
me.heumsi.Single@47371a45
Prototype
me.heumsi.Single@435bc213
me.heumsi.Single@321ab523
me.heumsi.Single@15445a46

2) 싱글톤 내 프로토타입 문제

다음과 같이, 싱글톤 빈 내에 프로토 타입 빈을 주입받는 상황이라 해보자.

@Component
public class Single {

  @Autowired
  Proto proto;

  public Proto getProto() {
    return proto;
  }
}

이럴 경우 Single 을 빈으로 등록할 때, Proto 도 같이 인스턴스화 되어, Autowired 로 주입받는 Signle.getProto() 를 하면 항상 같은 Proto 인스턴스가 나오게 된다.

@Component
public class AppRunner implements ApplicationRunner {

  @Autowired
  Single single;

  @Override
  public void run(ApplicationArguments args) throws Exception {
    System.out.println(single.getProto());
    System.out.println(single.getProto());
    System.out.println(single.getProto());
  }
}
me.heumsi.Single@47371a45
me.heumsi.Single@47371a45
me.heumsi.Single@47371a45

getProto() 를 할 때마다 매번 다른 Proto 인스턴스가 나오게 하려면 어떻게 해야할까? 즉, 싱글톤 내에서 프로토타입 빈을 주입받으려면 어떻게 해야할까?

3) proxyMode 사용

프로토타입 빈 클래스의 @Scope 에서 proxyMode = ScopedProxyMode.TARGET_CLASS 를 파라미터로 넘겨준다.

@Component @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Proto {
}

* 객체의 프록시를 떠서 사용하는 일종의 프록시 패턴임.

이후 싱글톤 빈 클래스를 다음과 같이 수정한다.

@Component
public class Single {

  @Autowired
  private ObjectProvider<Proto> proto;

  public Proto getProto() {
    return proto.getIfAvailable();
  }
}

이제 동일하게 ApplicationRunner 를 실행하면 다음과 같이 다른 인스턴스를 내뱉는다.

me.heumsi.Single@5312ba12
me.heumsi.Single@fe123a3a
me.heumsi.Single@1254f123