Spring

스프링 IoC 컨테이너와 Bean ( 1 )

들어가기에 앞서 본 포스팅은 백기선님의 인프런 강의를 기반으로 작성된 포스팅임을 알려드립니다.

처음 스프링을 배우는 입장이라 정확하지 않은 정보가 있을 수 있습니다.
댓글을 통해 알려주신다면 최대한 빨리 피드백하도록 하겠습니다!

 

IoC ( Inversion of Control ) / DI ( Depencency Injection )

 

IoC ( 제어의 역전 ) 라는 말 자체는 개념적인 성격을 지닙니다.

 

 

 

 

public class BookService {

    private BookRepository bookRepository = new BookRepository();

    public Book save(Book book){
        book.setCreated(new Date());
        book.setBookStatus(BookStatus.DRAFT);
        return bookRepository.save(book);
    }
}

 

위 코드의 경우 IoC가 적용되지 않은 상태입니다.

 

BookService 클래스 내부의 BookRepository 객체를 직접 생성하여 사용하는 모습을 확인할 수 있습니다.

 

이렇게 생성된 bookRepository객체는 BookService를 생성할 때 마다 새롭게 생성되므로, 항상 동일한 객체임을 보장할 수 없습니다.

 

Repository가 DB에 직접 데이터를 주고받는 역할을 수행하는 것을 생각해봤을 때, 아무래도 위험하게 느껴집니다.

 

 

 

 

 

 

BookService bookService = new BookService();
BookService bookService1 = new BookService();

assertThat(bookService.bookRepository).isEqualTo(bookService1.bookRepository);

실제로 테스트를 작성하여 돌려보면 두 객체 속의 bookRepository가 서로 다른 객체임을 확인할 수 있습니다.

 

 

 

이렇게 항상 같은 객체여야하는 참조변수가 서로 다르게 나타나는 경우를 해결하기 위해 도입된 개념이 IoC입니다.

외부에서 객체를 주입 받음으로써 항상 같은 객체임을 보장받는 것이죠.

 

주 목적은 하나의 객체를 모든 영역에서 재사용하기 위함이기 때문에 어딘가에 해당 객체들을 모아두고 이를 관리해주는 공간이 필요하다는 생각이 듭니다.

 

 

 

 

 

 

 

의존성을 주입하는 작업은 굳이 스프링을 사용하지 않더라도 가능합니다.

    public BookRepository bookRepository;

    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

 

이렇게 생성자를 통하여 외부에서 객체를 받아서, 내 필드의 참조변수에 넣어주는 작업을 의존성 주입이라고 부릅니다.

 

 

 

BookRepository bookRepository = new BookRepository();
BookService bookService = new BookService(bookRepository);
BookService bookService1 = new BookService(bookRepository);

assertThat(bookService.bookRepository).isEqualTo(bookService1.bookRepository);

 

테스트 코드를 작성하고 확인해보면 두 객체가 서로 같은 객체임을 알 수 있습니다.

 

 

 

 

 

 

IoC 컨테이너와 빈

 

앞서 하나의 객체를 모든 영역에서 재사용하기 때문에 그러한 객체를 모아둘 공간이 필요하다. 라고 말한게 기억나시나요?

 

바로 스프링에서 이 객체들을 모아두는 공간을 IoC 컨테이너라고 부릅니다.

스프링에서 Bean이라고 불리는, 특정 표식이 달린 객체들을 IoC 컨테이너라는 공간에 모아두고 관리하는거죠.

 

 

 

 

 

 

 

 


 

왜 쓸까?

위에서 생성자를 통한 의존성 주입을 하는 코드를 살펴보았습니다.

 

이렇게 개발자가 직접 코드를 통해서 의존성을 주입하고, 사용할 수 있는데 왜 IoC 컨테이너와 Bean을 사용하는 걸까요?

 

그 이유는 편의성과 안정성이라고 생각합니다.

 

 

이후에 살펴볼테지만 스프링의 기능을 통하여 객체를 관리하면 애노테이션으로 정말 편리하게 객체들을 관리할 수 있습니다.

 

게다가 다수의 개발자들이 그간 쌓여온 지식들을 모아서 Spring이라는 프레임워크의 한 기능으로 구현되어 있기 때문에 직접 코드를 작성하면서 실수가 생길 수 있다는 것에 비하여 안정성이 뛰어나다고 생각됩니다.

 

 

이 외에도 IoC 컨테이너에 등록된 Bean들은 싱글톤 스코프로 등록되기 때문에 항상 동일한 객체임을 보장받습니다.

 

 


 

 

 

 

이제 IoC컨테이너의 내부에 Bean라는 이름의 관리받는 객체가 들어있다는 사실을 알게 되었습니다.

이어서 IoC컨테이너 내부에 있는 Bean을 꺼내기 위해서 어떻게 해야하는지를 알아봅시다.

 

 

 

 

 

 

IoC컨테이너에 접근하기 위한 최상위 클래스는 BeanFactory 입니다.

해당 클래스에서는 구현체들이 따라야할 라이프사이클을 명시하고 있습니다.

 

 

 

실제로 Bean을 확인하기 위해 사용되는 인터페이스는 ApplicationContext이며, BeanFactory를 상속하고 있습니다.

 

지금은 기능이 확장된 BeanFactory 정도로 생각하면 되겠습니다.

 

 

 

 

 

 

 

 

Bean 등록 방법

 

앞에서 특정한 표식이 붙은 객체를 Bean이라고 부른다 배웠습니다.  

이번엔 이 특정한 표식을 붙히는 방법들에 대해 알아보려고 합니다.

 

고전적인 방법부터 모두 알아볼 생각이니 원하시는 내용이 아니면 건너뛰시면 될 것 같습니다.

 

 

 

xml 파일을 이용한 방법 ( 사용할 일 적음 )

 

public class BookService {

    public BookRepository bookRepository;

    public void setBookRepository(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    public Book save(Book book){
        book.setCreated(new Date());
        book.setBookStatus(BookStatus.DRAFT);
        return bookRepository.save(book);
    }
}
public class BookRepository {

    public Book save(Book book){
        return null;
    }
}

 

resource 디렉토리 밑에  application.xml 파일을 만들어 bean을 설정하는 방법입니다.

 

 

초기 화면

 

 

<bean id="bookService"
        class="me.ddings.springhw.service.BookService"/>

<bean id="bookRepository"
      class="me.ddings.springhw.repository.BookRepository"/>

기본적은 bean의 등록은 다음과 같이 id와 FQCN을 입력하는 것으로 할 수 있으며, 추가적으로 scopeautowired등의 설정을 더 해주는 것이 가능합니다.

 

여기까지 하면 Bean을 등록만 한 상태이고, property 속성을 통해 name과 참조할 bean을 지정해줘야 합니다.

 

 

 

 

<bean id="bookService"
      class="me.ddings.springhw.service.BookService">
    <property name="bookRepository" ref="bookRepository"/>
</bean>

이렇게 하면 BookService의 setbookRepository 를 실행할 때, 의존성이 주입됩니다.

 

 

 

 

 

 

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("apllication.xml");
String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
System.out.println(Arrays.toString(beanDefinitionNames));
BookService bookService = (BookService) applicationContext.getBean("bookService");

System.out.println(bookService.bookRepository);

 

ApplicationContext를 이용하여 Bean을 확인해보면 재대로 주입된 것을 확인할 수 있습니다.

 

 

 

 

 

 

 

 

Component Scan을 이용한 방법 ( xml ) 

 

 

xml 파일내에 component-scan을 이용하여 bean을 탐색 - 등록 - 주입하는 방법입니다.

 

component-scan이란 @component라는 애노테이션이 붙은 클래스들을 찾아서 해당 클래스의 객체를 bean으로 등록하는 방법을 말합니다.

 

 

@Component

 

Component (Spring Framework 5.3.5 API)

Indicates that an annotated class is a "component". Such classes are considered as candidates for auto-detection when using annotation-based configuration and classpath scanning. Other class-level annotations may be considered as identifying a component as

docs.spring.io

 

 

 

 

 

이런식으로 Component의 하위 애노테이션들을 찾아서 Bean으로 등록하고, Autowired가 붙은 생성자나 필드, 메소드를 찾아서 IoC컨테이너의 Bean을 주입하는 방식으로 작동합니다.

 

 

 

 

ApplicationContext를 사용하여 Bean들을 확인해보면 제대로 등록되어있는 것을 확인할 수 있습니다.

 

 

 

 

 

 

자바 설정파일을 이용한 방법

 

@Configuration
public class ApplicationConfig {

    @Bean
    public BookRepository bookRepository(){
        return new BookRepository();
    }

    @Bean
    public BookService bookService(){
        return new BookService(bookRepository());
    }
    
}

 

자바 설정파일을 이용하여 Bean을 등록하는 방법입니다.

 

 

 

 

 

public static void main(String[] args) {
    ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
    String[] beanDefinitionNames = context.getBeanDefinitionNames();
    System.out.println(Arrays.toString(beanDefinitionNames));

}

마찬가지로 bean을 확인해보면 등록되어 있는 것을 확인할 수 있습니다.

 

 

 

 

ComponentScan을 이용한 방법 ( xml 아님 )

 

 

스프링 부트기준으로 프로젝트를 생성하면 만들어지는 @SpringBootApplication의 경우 이 방법을 사용하고 있습니다.

 

xml파일에서와 마찬가지도 @Component가 붙은 클래스들을 모두 Bean으로 등록해주며, 기본적으로 생성되는 @SpringBootApplication가 붙은 클래스의 하위클래스들을 탐색하여 Bean으로 등록합니다. 

 

 

 

 

 

 

 

 

@Autowired

생성자로 Bean을 주입받는다. 

 

@Autowired는 IoC컨테이너에 등록된 Bean을 꺼내오는 애노테이션입니다.

 

이 애노테이션을 달 수 있는 위치는 생성자, 메소드, 파라미터, 필드가 있으며 런타임 시 까지 유지되는 애노테이션입니다.

 

 

 

 

 

 


@Autowiredrequired 속성을 통해 해당 의존성을 반드시 주입받아야하는가? 를 설정할 수 있습니다.

 

이 옵션을 false로 지정하면, 생성자가 아닌 필드 or 메소드를 통합 주입의 경우 해당 의존성이 IoC컨테이너에 없더라도 객체를 생성할 수 있습니다.

 

더보기
//@Repository
public class BookRepository {

    public Book save(Book book){
        return null;
    }
}
@Service
public class BookService {

    private BookRepository bookRepository;

//    @Autowired(required = false)
//    public BookService(BookRepository bookRepository) {
//        this.bookRepository = bookRepository;
//    }

    @Autowired(required = false)
    public void setBookRepository(BookRepository bookRepository){
        this.bookRepository = bookRepository;
    }

    public Book save(Book book){
        book.setCreated(new Date());
        book.setBookStatus(BookStatus.DRAFT);
        return bookRepository.save(book);
    }
}
@Component
public class TestRunner implements ApplicationRunner {

    @Autowired
    BookService bookService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println(bookService);
    }
}

 

위 코드에서 setter에 @Autowired(required = false) 를 통해 의존성을 받은다음, Runner에서 디버그를 돌려보면 다음과 같은 결과를 얻을 수 있습니다.

 

 

 

bookService Bean이 IoC컨테이너에 등록되어 있다는 사실 + bookRepository가 컨테이너에 없더라도 bookService의 객체는 생성된다는 사실을 알 수 있죠.

 

만약 생성자를 통한 주입을 했다면, 스프링이 BookService 객체 생성 시, 에러가 발생합니다.

 

 

 

 

 

 

 

발생할 수 있는 상황

 

 

이번엔 @Autowired를 통해 의존성을 주입받을 시에 발생할 수 있는 상황에 대해 살펴보겠습니다.

 

 

 

  1. 해당 타입의 빈이 하나만 존재하는 경우
  2. 해당 타입의 빈이 여러개가 존재하는 경우
  3. 해당 타입의 빈이 없는 경우

 

 

이렇게 세 가지 경우에 대해 생각해 볼 수 있습니다.

3번의 경우 빈이 존재하지 않는데, 주입받으려고 하므로 당연하게 에러가 발생합니다.

 

 

 

 

 

 

 

해당 타입의 빈이 하나만 존재하는 경우

 

 

@Repository
public class BookRepository {

    public Book save(Book book){
        return null;
    }
}
@Service
public class BookService {

    private BookRepository bookRepository;

    @Autowired
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    public Book save(Book book){
        book.setCreated(new Date());
        book.setBookStatus(BookStatus.DRAFT);
        return bookRepository.save(book);
    }
}
@Component
public class TestRunner implements ApplicationRunner {

    @Autowired
    ApplicationContext ctx;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        String[] beanDefinitionNames = ctx.getBeanDefinitionNames();

        Arrays.stream(beanDefinitionNames).forEach(System.out::println);
    }
}

 

 

정상적인 상황이기 때문에 문제없이 동작합니다.

 

 

 

 

IoC 컨테이너에도 들어있는 것을 확인할 수 있죠.

 

 

 

 

 

 

해당 타입의 빈이 여러 개인 경우

 

 

public interface BookRepository {

    public default Book save(Book book){
        return null;
    }
}
@Repository
public class BookRepositoryMK1 implements BookRepository{
}
@Repository
public class BookRepositoryMK2 implements BookRepository{
}

 

해당 타입의 빈이 두 개 이상인 경우에는 에러가 발생합니다.

 

 

 

이 경우, 둘 중에서 원하는 녀석을 골라서 주입받는게 가능하며 @Primary와 @Qualifier를 이용한 방법이 존재합니다.

 

 

 

 

 

@Primary를 사용한 방법은 사용하길 원하는 클래스에 애노테이션을 달아주는 방법입니다. 

 

@Repository @Primary
public class BookRepositoryMK2 implements BookRepository{
}

이렇게 하면 해당 클래스타입의 Bean을 주입받을 수 있습니다.

 

 

 

 

 

@Qualifier를 사용한 방법은 생성자를 통한 주입에는 사용할 수 없습니다.

 

필드 주입

 

다음과 같이 해당 클래스의 이름을 value값으로 전달해줘서 해당 타입의 빈이 주입되도록 할 수 있습니다.

 

사용자의 오타로 인한 에러발생확률이 존재하므로 타입 안정성이 떨어지며, 생성자에 사용할 수 없다는 단점이 존재합니다.

 

 

여기서 가만살펴보면 클래스의 이름은 BookRepositoryMK2 이지만 @Qualifier에 적어주는 이름은 앞 글자가 소문자인, bookRepositoryMK2 인 것을 확인할 수 있습니다.

그 이유는 스프링에 의해서 자동으로 생성되는 Bean들은 맨 앞글자가 소문자인 상태로 id가 등록되기 때문입니다.

이는 자바의 코딩 규약에서 클래스의 경우 맨 앞글자를 대문자로 지정하는 것으로 되어있기 떄문이라고 생각되네요.

 

 

 

 

@Primary와 @Qualifier를 동시에 사용했을 땐 어느게 적용될까요?

 

@Repository @Primary
public class BookRepositoryMK1 implements BookRepository{
}
@Autowired @Qualifier("bookRepositoryMK2")
private BookRepository bookRepository;

 

 

 

두 가지를 모두 사용했을 때는 @Qualifier쪽이 적용되는 것을 확인할 수 있습니다.

아마 사용자가 직접 입력하기 때문에 우선순위가 더 높지않나 생각되네요.

 

 

 

 

 

 

 

이 외에 원하는 Bean을 주입받는 다른 방법이 또 존재합니다.

 

@Service
public class BookService {

    private BookRepository bookRepositoryMK2;

    @Autowired
    public BookService(BookRepository bookRepositoryMK2) {
        this.bookRepositoryMK2 = bookRepositoryMK2;
    }

    public Book save(Book book){
        book.setCreated(new Date());
        book.setBookStatus(BookStatus.DRAFT);
        return bookRepositoryMK2.save(book);
    }
}

 

바로 해당 Bean의 id에 해당하는 이름으로 필드명을 짓는 방법입니다.

의존성 주입시에 타입체크, 이름체크를 둘 다 하기때문에 이런 방식으로 주입받는 것도 가능합니다.

 

 

 

 

 

간략하게 동작원리를 살펴보자.


ApplicationContext를 이용하여 등록된 모든 빈들을 출력해보면, 직접 등록한 Bean말고도 다른 수많은 Bean들이 출력되는 것을 확인할 수 있다.

이 중에 AutowiredAnnoationProcessor 라는 녀석이, @Autowired를 통한 의존성 주입에 사용된다.




이는 BeanFactory의 lifecycle의 표시된 부분에서 사용되는 라이프사이클 메소드이다.

AutowiredAnnoationProcessor 는 BeanPostProcessor의 구현체이며, Bean 초기화 이전에 BeanFactory가 이를 찾아서 다른 Bean들에게 적용하는 식으로 동작한다.