[스프링 IoC 컨테이너와 빈]_01

3 minute read

스프링 IoC 컨테이너와 빈

Inversion of Control

의존 관계 주입(Dependency Injection)이라고도 하며, 어떤 객체가 사용하는 의존 객체를 직접 만들어 사용하는게 아니라, 주입 받아 사용하는 방법을 말 함.

스프링 IoC 컨테이너

● BeanFactory
● 애플리케이션 컴포넌트의 중앙 저장소.
● 빈 설정 소스로 부터 빈 정의를 읽어들이고, 빈을 구성하고 제공한다.

● 스프링 IoC 컨테이너가 관리 하는 객체.
● 장점
	○ 의존성 관리
	○ 스코프
		■ 싱글톤: 하나만 만들어서 사용 (default)
		■ 프로포토 타입: 매번 다른 객체를 만들어 사용
	○ 라이프사이클 인터페이스

Inversion of Control

직접 만들어 사용하는 예

BookRepository repository = new BookRepository();
BookService service = new BookService(repository);

IoC를 사용한 예1) @Autowired 어노테이션 사용

@Autowired
BookRepository repository;

BookService service = new BookService(repository);

IoC를 사용한 예2) 생성자 주입 (1번 예시보다 권장되는 방식)

BookRepository repository;

public BookService(BookRepository repository) {
  this.repository = repositoy;
}

IoC 컨테이너를 사용하는 이유?

여러 개발자들이 스프링 커뮤니티에서 논의해서 만들어낸 여러가지 DI 방법과 Best Practices들과 노하우가 쌓여있는 프레임워크이기 때문이다.

컨테이너 안에 들어있는 객체들을 이라고 한다.

컨테이너라고 부르는 이유는 IoC 기능을 제공하는 빈들을 담고 있기 때문에 컨테이너라고 하는 것이다. 우리는 그런 들을 컨테이너로부터 가져와서 사용할 수 있는 것이다.

스프링 초기에는 XML로 설정하는 것이 대세였지만, 구글 주스(Google Guice)1가 선보인 어노테이션기반의 DI를 지원하기 시작했다. 그래서 오늘 날에는 @Service 를 사용해서 포조 객체(일반적인 객체)를 빈으로 등록할 수 있고, @Autowired로 빈에 등록이 되어있는 객체를 손쉽게 주입을 받아서 사용할 수 있는 것이다.

BeanFactory

문서

스프링의 가장 최상위에 있는 인터페이스는 BeanFactory라는 인터페이스이다. IoC의 핵심적인 클래스이다.

Bean factory 구현은 가능한 표준 Bean라이프 사이클 인터페이스를 지원해야한다.

전체 초기화 방법과 표준 순서

  1. BeanNameAware’s setBeanName
  2. BeanClassLoaderAware’s setBeanClassLoader
  3. BeanFactoryAware’s setBeanFactory
  4. EnvironmentAware’s setEnvironment
  5. EmbeddedValueResolverAware’s setEmbeddedValueResolver
  6. ResourceLoaderAware’s setResourceLoader (only applicable when running in an application context)
  7. ApplicationEventPublisherAware’s setApplicationEventPublisher (only applicable when running in an application context)
  8. MessageSourceAware’s setMessageSource (only applicable when running in an application context)
  9. ApplicationContextAware’s setApplicationContext (only applicable when running in an application context)
  10. ServletContextAware’s setServletContext (only applicable when running in a web application context)
  11. postProcessBeforeInitialization methods of BeanPostProcessors
  12. InitializingBean’s afterPropertiesSet
  13. a custom init-method definition
  14. postProcessAfterInitialization methods of BeanPostProcessors

ApplicationContext

문서

  • 가장 많이 사용하게될 빈 팩토리
  • BeanFactory를 상속받은 인터페이스
  • BeanFactory 가 가진 기능에 추가적으로 ApplicationEventPublisher, EnvironmentCapable 등과 같이 다양한 기능을 가지고 있다.

이러한 라이프 사이클로 스프링이 더 여러가지 기능을 제공해줄 수 있는 것이다. (컨테이너 안의 빈들을 가공하는 것이 가능하기 때문에)

특정 클래스를 왜 빈으로 등록해서 IoC 컨테이너가 관리하게 할까?

  1. 의존성 주입때문에

    의존성 주입을 받으려면 빈으로 들어가 있어야 받을 수 있다.

  2. 빈의 scope 때문에

    싱글톤으로 객체를 만들어서 관리하고 싶을 때

    스프링 IoC 컨테이너에 등록되는 빈들은 기본적으로 (별도의 어노테이션을 지정하지 않는다면) 싱글톤 스코프로 빈이 등록된다.

    • 싱글톤 패턴 : 하나의 애플리케이션에서 하나의 인스턴스만 생성되는 것을 보장하고, 생성된 객체를 어디에서든지 참조할 수 있도록 하는 패턴
    • 싱글톤으로 사용하고 싶지 않다면? 프로토 타입이 있다.
    • 프로토 타입 : 매번 다른 객체를 사용하는 것

    따라서 애플리케이션 전반에서 우리가 스프링 IoC 컨테이너로 부터 받아서 사용한다면 그 인스턴스들은 항상 같은 객체이다.

    메모리 면에서도 효율적이고, 미리 만들어 놓은 하나의 객체를 가져와 사용하기 때문에 런타임 시 성능 최적화에도 유리하다. (매번 만들어 사용하는 프로토 타입에 비해서)

    특히, DB와 일을 하는 xxxRepository 와 같은 객체는 만드는데 비용이 비쌀 텐데, 이런 것들을 싱글톤으로 쉽게 사용한다면 큰 장점이 될 수 있다.

  3. 라이프사이클 인터페이스를 지원해준다.

    스프링 IoC 컨테이너에 등록된 빈에 국한된 이야기이다.

    어떤 빈이 만들어 졌을 때, 추가적인 작업을 하고 싶다. 여러가지 다양한 라이프 사이클 인터페이스를 사용해서 부가적인 작업을 할 수 있고, 스프링(부트) 자체에서 이런 라이프사이클 콜백을 이용해서 부가적인 기능을 만들어 낼 수 있다.

    예시 ) @PostConstruct 로 해당 빈이 생성될 때, 실행할 작업을 추가할 수 있다.

    라이프사이클 인터페이스

우리가 직접 IoC 기능을 하는 코드를 작성할 수도 있지만, 위의 장점들 때문에 빈으로 등록해서 스프링이 제공해주는 IoC 컨테이너 기능을 사용하는 이유이다.

의존성 문제

다음 BookService를 테스트하는 코드를 보자

public class BookServiceTest {
  @Test
  public void save() {
    Book book = new Book();
    
    BookRepository bookRepository = new BookRepository();
    BookService bookService = new BookService(bookRepository);
    
    Book result = bookService.save(book);
    
    assertThat(book.getCreated()).isNotNull();
    assertThat(book.getBookStatus()).isEqualTo(BookStatus.DRAFT);
    assertThat(result).isNotNull();
  }
}

현재 BookRepository의 save 메서드는 null 만 리턴하고 있기 때문에 무조건 테스트에 실패할 수 밖에 없다. 하지만 성공하는 테스트 코드를 만들고 싶다면?

BookService가 BookRepository에 의존성을 가지고 있어서, BookRepository를 구현하지 않고서는 BookService만을 테스트할 수 없는 상황이다.

의존성을 가진 BookService의 단위테스트를 만들기 힘들다.

현재 생성자를 통해서 빈주입을 받고있다.

@Service
public class BookService {
  BookRepository repository;

  public BookService(BookRepository repository) {
    this.repository = repositoy;
  }
}

하지만 이 코드를 다음과 같이 직접 생성해서 넣어준다면 의존성을 바꿔줄 수 없는 코드이므로 더 테스트하기 힘들어진다.

@Service
public class BookService {
  BookRepository repository = new BookRepository();
}

하지만 우리는 의존성 주입을 받을 수 있도록 BookService 코드를 작성했기 때문에 테스트 시에 가짜 객체(Mock객체)를 만들어서 테스트할 수 있는 것이다.

public class BookServiceTest {
  
  @Mock
  BookRepository bookRepository;
  
  @Test
  public void save() {
    Book book = new Book();
    
    //save 메서드를 호출할 때, 매개변수로 넘긴 book 인스턴스를 그대로 리턴하게 한다.
    // 즉, 동일한 book 인스턴스를 반환하게 된다.
    when(bookReposiroty.save(book)).thenReturn(book);
    BookService bookService = new BookService(bookRepository);
    
    //result가 null이 아니므로 테스트가 정상적으로 통과하게 된다.
    Book result = bookService.save(book); 
    
    assertThat(book.getCreated()).isNotNull();
    assertThat(book.getBookStatus()).isEqualTo(BookStatus.DRAFT);
    assertThat(result).isNotNull();
  }
}

다음 시간에, 스프링 부트가 없을 때, 어떤식으로 XML 설정과 자바 설정을 사용할 수 있었는지 확인해보자

  1. 스프링이 해주는 역할 중에 하나인 의존성 주입을 해주는 프레임 워크