4. 스프링 컨테이너와 스프링 빈

jinhxxxxkim·2023년 1월 27일
0

Spring핵심

목록 보기
4/9
post-thumbnail

4. 스프링 컨테이너와 스프링 빈

1. 스프링 컨테이너의 생성

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
  • ApplicationContext를 스프링 컨테이너라 하며, 인터페이스다.
    • 다형성 적용을 위해 인터페이스로 되어있다.
  • ApplicationContext의 구현체는 여러가지가 있다.(XML기반, 어노테이션 기반)
  • AppConfig를 사용했던 방식이 어노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든 것이다.

스프링 컨테이너의 생성 과정

  1. 스프링 컨테이너의 생성
    • new AnnotationConfigApplicationContext(AppConfig.class)를 통해 스프링 컨테이너를 생성한다.
    • 스프링 컨테이너 생성 시 구성정보를 지정해주어야한다.
    • 위 그림에서는 AppConfig.class를 구성정보로 지정하였다.
  1. 스프링 빈 등록
    • 스프링 컨테이너(ApplicationContext)는 생성자의 파라미터로 넘어온 설정 클래스의 정보를 사용하여 스프링 빈을 등록한다.
    • 빈의 이름은 기본적으로 메소드의 이름이며 직접 부여할 수 있다
    • 빈의 이름이 key, 빈 객체가 value의 형태로 저장된다.
    • 빈의 이름은 항상 다른 이름을 부여해야한다.
  1. 스프링 빈 의존관계 설정
    • 스프링 컨테이너는 설정정보를 참고하여 의존관계를 주입한다.
    • 자바 코드로 스프링 빈을 등록하면(위와 같이), 생성자를 호출하면서 의존관계 주입도 한번에 처리된다

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

모든 빈 조회하기

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

String[] beanDefinitionNames = ac.getBeanDefinitionNames();
Object bean = ac.getBean(beanDefinitionName);
  • ac.getBeanDefinitionNames()을 통해 스프링에 등록된 모든 빈 이름을 문자열 배열로 반환한다.
  • ac.getBean()에 매개변수로 전달된 빈 이름으로 빈 객체(인스턴스)를 조회한다.

애플리케이션 빈 조회하기

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

for (String beanDefinitionName : beanDefinitionNames) {
	BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
    beanDefinition.getRole()
}

// BeanDefinition.ROLE_APPLICATION
// BeanDefinition.ROLE_INFRASTRUCTURE
  • ac.getBeanDefinition(beanDefinitionName)을 통해 매개변수로 전달된 빈에 대한 메타데이터 정보들을 가져온다.
  • beanDefinition.getRole()을 통해 빈의 role정보를 가져온다
  • BeanDefinition.ROLE_APPLICATION: 직접 등록한 빈
  • BeanDefinition.ROLE_INFRASTRUCTURE: 스프링이 내부적으로 확장하기 위한 기반 빈

3. 스프링 빈 조회 - 기본

스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회 방법

  • ac.getBean(빈이름, 타입)
  • ac.getBean(타입)
  • 타입은 컨테이너 내에서 빈의 타입으로, 빈을 등록하기 위해 인스턴스를 생성하는 메소드의 반환형으로 이해하였다.
  • 조회 대상에 스프링 빈이 없다면 예외 발생
    • NoSuchBeanDefinitionException: No bean named 'xxxxx' available

공통 코드

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

1. 빈 이름으로 조회

MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
  • ac.getBean(빈이름, 타입)의 형태로 빈을 조회한다.
  • 빈 이름은 "memberService"이며, 타입은 MemberService.class로 구현체가 아닌, 인터페이스를 전달하였다.
  • 조회 결과를 instanceOf 메소드를 사용하여 MemberServiceImpl클래스가 맞는지 확인한다.
  • 아니라면 에러가 발생한다.
DiscountPolicy discountPolicy = ac.getBean("discountPolicy", DiscountPolicy.class);
assertThat(discountPolicy).isInstanceOf(FixDiscountPolicy.class);
  • 위의 코드는 에러가 발생하는 코드다.
name = discountPolicy object = hello.core.discount.RateDiscountPolicy@2c7b5824
  • 현재 스프링에 등록된 discountPolicyRateDiscountPolicy다.
  • 그러나 isInstanceOf(FixDiscountPolicy.class)로 조회할 경우, 다음과 같은 오류가 발생한다.
java.lang.AssertionError: 

Expecting actual:		hello.core.discount.RateDiscountPolicy@3b7ff809
to be an instance of:	hello.core.discount.FixDiscountPolicy
but was instance of:	hello.core.discount.RateDiscountPolicy
  • 실제 discountPolicyRateDiscountPolicy이며, RateDiscountPolicy의 인스턴스여야 하지만, FixDiscountPolicy의 인스턴스라 예상하여 오류가 발생하였다.

2. 빈 이름없이 타입으로만 조회

MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
  • ac.getBean(타입)의 형태로 빈을 조회한다.
  • 빈의 이름을 전달하지 않아도 조회할 수 있다.

3. 구체 타입으로 조회

MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
  • ac.getBean(빈이름, 타입)의 형태로 조회를 하나, 타입에 인터페이스가 아닌, 구현체가 전달된다.
  • 허나, 이는 DIP를 준수하기에 바람직하지 않다.
  • 구체 타입으로 조회하면 변경시 유연성이 떨어진다.
  • 추상화(역할)에 의존하도록 설계하는 것이 바람직하다.

4. 예외 터트리기

import static org.junit.jupiter.api.Assertions.assertThrows;

assertThrows(NoSuchBeanDefinitionException.class,
                ()->ac.getBean("xxxxx", MemberService.class));
  • 로직(람다식) 실행 시 NoSuchBeanDefinitionException 예외가 발생하여야 Test가 통과다.

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

ac.getBean(타입)의 형태로 빈을 조회할 경우, 같은 타입의 스프링 빈이 둘 이상일 경우 오류가 발생한다. 따라서 같은 타입의 스프링 빈이 둘 이상일 경우 빈의 이릅을 지정하여 ac.getBean(빈이름, 타입)의 형태로 조회를 한다.

공통 코드

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
@Configuration
static class SameBeanConfig{
	@Bean
    public MemberRepository memberRepository1(){
    	return new MemoryMemberRepository();
	}

    @Bean
    public MemberRepository memberRepository2(){
    	return new MemoryMemberRepository();
	}
}
  • Test에만 사용하는 Configuration 클래스다.
  • 중복 오류를 검증하기 위해 동일한 타입의 빈을 정의하였다.

1. 중복 오류 발생

// error
MemberRepository bean = ac.getBean(MemberRepository.class)
  • 현재 스프링에 등록되어있는 빈은 2개이며, 2개의 빈 모두 타입이 MemberRepository이다.
  • 따라서 위의 코드와 같이 호출할 경우, NoUniqueBeanDefinitionException 에러가 발생한다.
expected single matching bean but found 2: memberRepository1,memberRepository2
  • 1개의 빈이 매칭될 것으로 예상하였지만, 두개의 빈이 조회되었다고 뜬다.
import static org.junit.jupiter.api.Assertions.assertThrows;

assertThrows(NoUniqueBeanDefinitionException.class,
                () -> ac.getBean(MemberRepository.class));
  • Test 코드는 junit의 assertThrows를 사용하여 진행한다.

2. 빈 이름 지정

MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);
assertThat(memberRepository).isInstanceOf(MemberRepository.class);
  • 중복 오류가 발생하였을 경우, 빈의 이름을 지정하여 조회하면 해결할 수 있다.

3. 특정 타입을 모두 조회하기

Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
  • ac.getBeansOfType()를 사용하여 스프링 컨테이너 내에 매개변수로 전달된 타입의 모든 빈을 Map의 형태로 반환한다.
  • Map의 key는 빈의 이름(메소드의 이름)이며, value는 생성된 인스턴스이다.

5. 스프링 빈 조회 - 상속 관계

  • 부모타입으로 조회하면, 자식 타입도 함께 조회된다.
  • 따라서 Object타입으로 조회한다면, 모든 스프링 빈을 조회한다.

공통코드

@Configuration
static class TestConfig {
	@Bean
    public DiscountPolicy rateDiscountPolicy() {
    	return new RateDiscountPolicy();
	}

    @Bean
    public DiscountPolicy fixDiscountPolicy() {
    	return new FixDiscountPolicy();
	}
}

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
  • Test를 위해 같은 부모(DiscountPolicy)를 갖는 빈 2개를 생성한 후, 해당 클래스를 컨테이너의 구성정보로 사용한다.

1. 부모타입으로 조회

// error
DiscountPolicy bean = ac.getBean(DiscountPolicy.class);

assertThrows(NoUniqueBeanDefinitionException.class,
                () -> ac.getBean(DiscountPolicy.class));
  • 부모(DiscountPolicy) 클래스로 조회할 경우, 2개 이상의 자식이 있으므로, NoUniqueBeanDefinitionException이 발생한다.
  • 따라서 assertThrows를 사용하여 Test를 실행한다.
DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
  • 자식이 둘 이상 있을 경우, 빈 이름을 지정하여 조회한다.
RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
  • 특정 하위 타입으로 조회할 수 있으나, 좋지 못한 방법이다.

2. 부모 타입으로 모두 조회하기

Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
  • 특정 타입을 모두 조회할 경우와 같이, ac.getBeansOfType()메소드를 사용하여, 모든 빈을 조회한다.
Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
  • 위와 같이 Object 타입으로 조회할 경우, 모든 스프링 빈이 조회된다.

6. BeanFactory와 ApplicationContext

BeanFactory

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

ApplicationContext

  • BeanFactory의 기능을 모두 상속받아서 제공한다.
  • 애플리케이션 개발 시, 빈의 관리 및 조회 이외의 수 많은 부가기능이 필요하다.

ApplicationContext이 제공하는 부가기능

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
		MessageSource, ApplicationEventPublisher, ResourcePatternResolver {...}
  • 위의 코드는 ApplicationContext의 선언 코드다.
  • BeanFactory만 상속 받는 것이 아닌, 여러가지 부가기능을 위한 인터페이스를 상속받는 것을 알 수 있다.
  • 인터페이스 분리 원칙 ISP(Interface segregation principle)의 한 예

MessageSource

  • 메시지소스를 활용한 국제화 기능을 사용할 수 있다.
    • 예) 영미권에서는 영어로, 한국에서는 한글로 출력한다.

EnvironmentCapable(환경변수)

  • 로컬(내PC), 개발(Test Server), 운영(Production)등을 구분해서 처리
  • 각 환경별로 어떤 DB에 연결해야될지에 대한 정보를 처리

ApplicationEventPublisher

  • 이벤트를 발행하고 구독하는 모델을 편리하게 지원

ResourceLoader

  • 편리한 리소스 조회
  • 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

7. 다양한 설정 형식 지원 - 자바 코드, XML

  • 스프링 컨테이너는 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어 있다.
    • 자바 코드, XML, Groovy etc.

어노테이션 기반 자바코드 설정

  • AnnotationConfigApplicationContext클래스를 사용하여, 자바코드로 된 설정정보를 넘기면 된다.

XML 설정

  • GenericXmlApplicationContext를 사용하면서 xml 설정 파일을 넘기면 된다
ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
  • 위 코드에서 appConfig.xml이라는 xml파일 형식의 설정파일을 컨테이너에 전달하고있다.
  • xml파일의 형태는 자바코드 설정파일과 상당히 유사하다.
<bean id="memberService" class="hello.core.member.MemberServiceImpl">
	<constructor-arg name="memberRepository" ref="memberRepository"/>
 </bean>

<bean id="memberRepository" class="hello.core.member.MemoryMemberRepository"/>

<bean id="orderService" class="hello.core.order.OrderServiceImpl">
	<constructor-arg name="memberRepository" ref="memberRepository"/>
    <constructor-arg name="discountPolicy" ref="discountPolicy"/>
</bean>

<bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy"/>
  • <bean>이라는 태그를 사용하며, @Bean어노테이션과 같은 역할을 한다.
  • id라는 속성으로 빈에 이름을 부여한다.
  • ref라는 속성으로, 해당 빈에대한 구현의 위치를 명시한다.
  • <constructor-arg>라는 태그를 사용하여 해당 빈의 생성자에 배개변수를 전달한다.

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

  • BeanDefinition을 빈 설정 메타정보라 한다
    • @Bean , <bean> 당 각각 하나씩 메타 정보가 생성된다
  • 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다.
  • 스프링이 다양한 설정 형식을 지원 가능하게 한다.
  • 스프링 컨테이너는 BeanDefinition에만 의존한다.
  • 즉 역할과 구현을 분리시킨 것이다.
  • AnnotationConfigApplicationContextAnnotatedBeanDefinitionReader 를 사용해서 AppConfig.class 를 읽고 BeanDefinition 을 생성한다.
  • GenericXmlApplicationContextXmlBeanDefinitionReader 를 사용해서 appConfig.xml를 읽고 BeanDefinition 을 생성한다.
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
	BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

	if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
    	System.out.println("beanDefinitionName = " + beanDefinitionName + " beanDefinition" + beanDefinition);
	}
}
  • ac.getBeanDefinitionNames()을 통해 스프링에 등록된 모든 빈 이름을 문자열 배열로 반환한다.
  • ac.getBeanDefinition()을 통해 BeanDefinition을 가져온다.

BeanDefinition 정보

Root bean: class [null]; 
scope=; 
abstract=false; 
lazyInit=null; 
autowireMode=3; 
dependencyCheck=0; 
autowireCandidate=true; 
primary=false; 
factoryBeanName=appConfig; 
factoryMethodName=memberService; 
initMethodName=null; 
destroyMethodName=(inferred); 
defined in hello.core.AppConfig
  • BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
  • factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
  • factoryMethodName: 빈을 생성할 팩토리 메서드 지정, 예) memberService
  • Scope: 싱글톤(기본값)
  • lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연처리 하는지 여부
  • InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
  • DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
  • Constructor arguments, Properties: 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음

팩토리 메소드

<bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy"/>
  • XML을 사용할 경우, 직접 스프링 빈을 등록하는 방식이다.
  • XML을 사용할 경우, BeanDefinition의 factoryBeanName, factoryMethodName의 값이 null이 된다.
@Bean
public MemberService memberService(){...}
  • 자바 코드를 사용할 경우, 팩토리 메소드를 사용하여 우회하여 스프링 빈을 등록하는 방식이다.
  • AppConfig가 제공하는 memberService 팩토리 메소드를 사용하여 스프링 빈을 등록

출처: 인프런 스프링 핵심 원리 - 기본편 (김영한)
인프런 스프링 핵심 원리

0개의 댓글