Spring Bean


Spring Bean 이란 IoC Container 내부에 들어있는 객체로 필요할 때 꺼내서 사용할 수 있다. @Bean 이나 xml 설정을 통해 일반 객체를 Bean 으로 등록할 수 있다. 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다. 따라서 같은 스프링 빈이면 모두 같은 인스턴스다.

Spring Bean 생성 과정


Spring Bean 의 생명주기

  1. 객체 생성
  2. 의존 설정
  3. 초기화
  4. 사용
  5. 소멸

Bean 은 IoC Container 에 의해 생명주기를 관리하며 초기화 방법은 @PostConstruct 를 Bean 소멸에서는 @PreDestroy 를 사용한다. 생성한 Bean 을 등록할 때는 @ComponentScan 을 이용하거나 @Configuration@Bean 을 사용하여 Bean 설정 파일에 직접 Bean 을 등록할 수 있다.

Bean 등록 방법


@Component

@Controller
public class MemberController {
 
    private final MemberService memberService;
 
    @Autowired
    public MemberController(MemberService memberService) {
 
         this.memberService = memberService;
    }
}

Bean 으로 등록하고자 하는 클래스 선언부 상단에 @Component 애너테이션을 추가하여 Bean 으로 등록할 수 있다. 자주 사용되는 @Controller @Service @Repository 애너테이션은 모두 @Component 애너테이션이 포함되어 있다. 스프링은 @ComponentScan 을 통해 @Component 애너테이션이 선언된 모든 클래스를 자동으로 스캔한 뒤 Singleton Bean 으로 등록한다.

@Bean

@Configuration
public class SpringConfig {
 
    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }
 
    @Bean
    public MemberRepository memberRepository() {
      return new MemoryMemberRepository();
    }
}

설정 클래스로 사용할 클래스를 생성하고 선언부 상단에 @Configuration 애너테이션을 추가하여 Bean 설정을 위한 클래스임을 명시한다. Bean 으로 등록할 클래스를 반환하는 메서드를 만들고 해당 메서드 상단에 @Bean 애너테이션을 추가하여 Singleton Bean 을 등록할 수 있다. 이 때 생성되는 Bean 의 이름은 기본적으로 메서드명으로 지정되며, @Bean(name = "name") 을 통해 직접 이름을 지정해줄 수 도 있다.

Bean Scope


Bean Scope 는 Bean 이 존재할 수 있는 범위를 뜻하며 Singleton Prototype Request Session Application 등이 존재한다.

Singleton

  • Singleton 은 기본 Scope 로 IoC 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 Scope 다.

Prototype

  • Prototype 은 Bean 의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 Scope 다.

Request

  • Request 는 웹 요청이 들어오고 나갈때 까지 유지하는 Scope 다.

Session

  • Session 은 웹 세션 생성부터 종료까지 유지하는 Scope 다.

Application

  • Application 은 WebServletContext 와 같은 범위로 유지하는 Scope 다.

IoC


Inversion of Control, IoC 는 제어의 역전으로 프로그램의 흐름을 직접 제어하는 것이 아닌 외부에서 관리하는 것이다. 즉, 코드의 최종 호출은 개발자가 제어하는 것이 아닌 프레임워크 내부에서 결정된 대로 이루어지는 것을 의미한다. 객체의 의존성을 역전시키면 객체 간의 결합도를 줄이고 유연한 코드를 작성할 수 있어 가독성 및 유지 보수가 편해진다는 장점이 있다. 스프링은 모든 객체를 스프링이 실행될 때 만들어놓고 필요한 곳에 주입시켜 특정 객체를 의존하는 객체에서 의존 객체를 사용할 수 있도록 한다.

IoC 종류


DL

저장소에 저장되어 있는 Bean 에 접근하기 위해 컨테이너가 제공하는 API 를 통해 Bean 을 Lookup 하는 것이다. DL 은 컨테이너 종속이 증가하기 때문에 주로 DI 를 사용한다.

DI

빈 설정 정보를 바탕으로 각 클래스간의 의존관계를 컨테이너가 자동으로 연결해준다.

IoC Container


인스턴스 생성부터 소멸까지 인스턴스의 생명주기를 관리해주기 때문에 개발자는 비즈니스 로직에만 집중할 수 있다.

  • 객체의 생성을 책임지고 의존성을 관리
  • POJO 의 생성, 초기화, 서비스, 소멸에 대한 권란을 가짐
  • 개발자들이 직접 POJO 를 생성할 수 있지만 컨테이너에게 맡김

IoC Container 의 역할

ApplicationContext 는 IoC Container 이며 Bean 을 인스턴스화하고 구성하고 조립하는 역할을 한다. 애플리케이션 실행 시점에 Bean 오브젝트를 인스턴스화하고 DI 한 뒤 최초로 애플리케이션을 가동할 Bean 하나를 제공한다.

IoC Container 의 종류

ApplicationContext 는 BeanFactory 의 서브 인터페이스

BeanFactory

  • 컨테이너에서 Bean 을 생성하고 DI 하는 기능을 제공
  • Bean 등록, 생성, 조회, 반환을 관리
  • 팩토리 디자인 패턴을 구현한 것으로 빈을 생성하고 분배하는 책임을 가짐
  • Bean 을 조회할 수 있는 getBean() 메서드가 정의되어 있음

ApplicationContext

  • BeanFactory 의 기능에 더해 스프링의 각종 부가 기능을 추가로 제공
  • 국제화가 지원되는 텍스트 메시지를 관리
  • 이미지같은 파일 자원을 로드할 수 있는 포괄적인 방법 제공
  • 리스너로 등록된 빈에게 이벤트 발생을 알려줌

스프링 컨테이너 생성


ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
  • ApplicationContext 를 스프링 컨테이너라고 한다.
  • ApplicationContext 는 인터페이스이다.
  • 스프링 컨테이너는 XML 기반으로 만들 수 있고, 어노테이션 기반의 Java 설정 클래스로 만들 수 있다.
  • AppConfig 를 사용한 방식이 어노테이션 기반의 Java 설정 클래스로 스프링 컨테이너를 만든 것이다.

컨테이너에 등록된 모든 빈 조회


@Test
@DisplayName("모든 빈 출력")
void findAllBean() {
    String[] beanDefinitionNames = ac.getBeanDefinitionNames();
    for (String beanDefinitionName : beanDefinitionNames) {
        Object bean = ac.getBean(beanDefinitionName);
        System.out.println("name = " + beanDefinitionName + " object = " + bean);
    }
}
@Test
@DisplayName("애플리케이션 빈 출력")
void findApplicationBean() {
    String[] beanDefinitionNames = ac.getBeanDefinitionNames();
    for (String beanDefinitionName : beanDefinitionNames) {
        BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
        // Role ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
        // Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
        if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name = " + beanDefinitionName + " object = " + bean);
        }
    }
}

스프링 빈 조회 - 기본


  • Spring Container 에서 Spring Bean 을 찾는 가장 기본적인 조회 방법
    • ac.getBean(빈이름, 타입);
    • ac.getBean(타입);
  • 조회 대상인 Spring Bean 이 없으면 NoSuchBeanDefinitionException 발생
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("빈 이름으로 조회")
void findBeanByName() {
    MemberService memberService = ac.getBean("memberService", MemberService.class);
    assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("빈 타입으로 조회")
void findBeanByType() {
    MemberService memberService = ac.getBean(MemberService.class);
    assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("구체 타입으로 조회")
void findBeanByExactType() {
    MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
    assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("없는 빈 이름으로 조회")
void findNotExistingBeanByName() {
    assertThatThrownBy(() -> ac.getBean("xxxx", MemberService.class))
            .isInstanceOf(NoSuchBeanDefinitionException.class);
}

스프링 빈 조회 - 동일한 타입이 둘 이상


  • 타입으로 조회 시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생한다. 이때는 빈 이름을 지정.
  • ac.getBeansOfType() 을 사용하면 해당 타입의 모든 빈을 조회할 수 있다.
public class ApplicationContextSameBeanFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
    @Test
    @DisplayName("타입으로 조회 시 동일한 타입이 둘 이상 있으면 예외 발생")
    void findBeanByTypeDuplicate() {
        assertThatThrownBy(() -> ac.getBean(MemberRepository.class))
                .isInstanceOf(NoUniqueBeanDefinitionException.class);
    }
    @Test
    @DisplayName("타입으로 조회 시 동일한 타입이 둘 이상 있으면 빈 이름 지정")
    void findBeanByName() {
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
        assertThat(memberRepository).isInstanceOf(MemberRepository.class);
    }
    @Test
    @DisplayName("특정 타입 모두 조회하기")
    void findAllBeanByType() {
        Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
        System.out.println("beansOfType = " + beansOfType);
        assertThat(beansOfType.size()).isEqualTo(2);
    }
    @Configuration
    static class SameBeanConfig {
        @Bean
        public MemberRepository memberRepository() {
            return new MemoryMemberRepository();
        }
        @Bean
        public MemberRepository memberRepository2() {
            return new MemoryMemberRepository();
        }
    }
}

스프링 빈 조회 - 상속 관계


  • 부모 타입으로 조회하면, 자식 타입도 함께 조회한다.
  • 그래서 모든 자바 객체의 최고 부모인 Object 타입으로 조회하면 모든 스프링 빈을 조회한다.
public class ApplicationContextExtendsFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
    @Test
    @DisplayName("부모 타입으로 조회 시 자식이 둘 이상 있으면 예외 발생")
    void findBeanByParentTypeDuplicate() {
        assertThatThrownBy(() -> ac.getBean(DiscountPolicy.class))
                .isInstanceOf(NoUniqueBeanDefinitionException.class);
    }
    @Test
    @DisplayName("부모 타입으로 조회 시 자식이 둘 이상 있으면 빈 이름 지정")
    void findBeanByParentTypeBeanName() {
        DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
        assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
    }
    @Test
    @DisplayName("특정 하위 타입으로 조회")
    void findBeanBySubType() {
        DiscountPolicy rateDiscountPolicy = ac.getBean(RateDiscountPolicy.class);
        assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
    }
    @Test
    @DisplayName("부모 타입으로 모두 조회")
    void findAllByParentType() {
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        assertThat(beansOfType.size()).isEqualTo(2);
    }
    @Test
    @DisplayName("부모 타입으로 모두 조회 - Object")
    void findAllByObjectType() {
        Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
    }
    @Configuration
    static class TestConfig {
        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }
        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
    }
}

BeanFactory 와 ApplicationContext


BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스
  • 스프링 빈을 관리하고 조회하는 역할을 담당
  • getBean() 을 제공

ApplicationContext

  • BeanFactory 의 기능을 모두 상속받아서 제공
  • 애플리케이션 개발할 때는 BeanFactory 에서 제공하는 기능 외에도 많은 부가기능이 필요

ApplicationContext 가 제공하는 부가기능

  • 메시지소스를 활용한 국제화 기능
    • 한국에서 들어오면 한국어, 영어권에서 들어오면 영어로 출력
  • 환경변수
    • 로컬, 개발, 운영등을 구분해서 처리
  • 애플리케이션 이벤트
    • 이벤트를 발행하고 구독하는 모델을 편리하게 지원
  • 편리한 리소스 조회
    • 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

정리

  • ApplicationContext 는 BeanFactory 의 기능을 상속받는다.
  • ApplicationContext 는 빈 관리 기능 + 편리한 부가 기능을 제공한다.
  • BeanFactory 를 직접 사용할 일은 거의 없고, ApplicationContext 를 사용한다.
  • BeanFactory 나 ApplicationContext 를 모두 스프링 컨테이너라고 한다.

스프링 빈 설정 메타 정보 - BeanDefinition


  • 스프링은 어떻게 이런 다양한 설정 형식을 지원하는가?
  • 그 중심에는 BeanDefinition 이라는 추상화가 있다.
    • XML, 자바 코드 등이 BeanDefinition 으로 만들어지면 스프링 컨테이너는 BeanDefinition 만 알면된다.
  • BeanDefinition 을 메타정보라 한다.
    • @Bean <bean> 당 각각 하나씩 메타 정보가 생성된다.
  • 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다.
  • AnnotationConfigApplicationContextAnnotatedBeanDefinitionReader 를 사용해서 AppConfig.class 를 읽고 BeanDefinition 을 생성한다.

BeanDefinition

  • BeanClassName
  • factoryBeanName
  • factoryMethodName
  • Scope
  • lazyInit
  • InitMethodName
  • DestroyMethodName
  • Constructor arguments, Properties

정리

  • BeanDefinition 을 직접 생성해서 스프링 컨테이너에 등록할 수 도 있다.
  • 하지만 실무에서 BeanDefinition 을 직접 정의하거나 사용할 일은 거의 없다.
  • BeanDefinition 에 대해서 너무 깊이있게 이해하기 보단, 스프링이 다양한 형태의 설정 정보를 BeanDefinition 으로 추상화해서 사용한다는 것 정도만 이해하면 된다.
  • 가끔 스프링 코드나 스프링 관련 오픈 소스의 코드를 볼 때, BeanDefinition 이라는 것이 보일 때가 있는데, 이때 이런 메커니즘을 떠올리면 된다.