관리 메뉴

나구리의 개발공부기록

스프링 컨테이너와 스프링 빈, 스프링 컨테이너 생성, 컨테이너에 등록된 모든 빈 조회, 스프링 빈 조회(기본/동일한 타입이 둘 이상/상속관계), BeanFactory와 ApplicationContext, 다양한 설정 형식 지원(자바코드/XML), 스프링 빈 설정 메타 정보 - BeanDefinition 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 핵심원리 - 기본편

스프링 컨테이너와 스프링 빈, 스프링 컨테이너 생성, 컨테이너에 등록된 모든 빈 조회, 스프링 빈 조회(기본/동일한 타입이 둘 이상/상속관계), BeanFactory와 ApplicationContext, 다양한 설정 형식 지원(자바코드/XML), 스프링 빈 설정 메타 정보 - BeanDefinition

소소한나구리 2024. 1. 29. 17:41

  출처 : 인프런 - 스프링 핵심원리 - 기본편(유료) / 김영한님  
  유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용  

https://inf.run/kCYMv


1. 스프링 컨테이너 생성

1) 스프링 컨테이너 생성 코드

ApplicationContext applicationContext =
                     new AnnotationConfigApplicationContext(AppConfig.class);
  • ApplicationContext를 스프링 컨테이너라고하며 인터페이스임
  • 스프링컨테이너는 XML기반과 애노테이션 기반의 자바 설정 클래스(기존의 AppConfig 클래스)로 만들 수 있음 
  • new AnnotationConfigApplicationContext(AppConfig.class);는 ApplicationContext 인터페이스의 구현체

** 참고

  • 스프링 컨테이너를 부를 때 BeanFactory, ApplicationContext로 구분해서 부름(BeanFactory가 최상위 인터페이스)
  • BeanFactory를 직접 사용하는 경우는 거의 없어서 일반적으로 ApplicationContext를 스프링 컨테이너라고 함

2) 스프링 컨테이너 생성 과정

(1) 스프링 컨테이너 생성과 스프링빈 등록

  • new AnnotationConfigApplicationContext(AppConfig.class) -> AppConfig.class를 구성정보로 등록하여 스프링 컨테이너를 생성
  • 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈으로 등록
  • 빈이름은 메서드이름을 사용하며 직접 부여 할 수도 있음

** 주의

  • 빈 이름은 항상 다른 이름을 부여 해야 함
  • 같은 빈을 부여하면 다른 빈이 무시되거나 기존 빈을 덮어버리거나 설정에 따라 오류가 발생함
@Bean(name="memberService2") // 애노테이션에 빈이름을 직접 부여
// 같은 이름이 부여 되면 다른 빈이 무시되거나 기존 빈을 덮어버리는 등의 오류가 발생하니 주의!

좌) 스프링 컨테이너 생성 / 우) 스프링 컨테이너 등록

(2) 스프링 빈 의존관계 설정준비 및 완료

  • 설정 정보를 참고하여 의존관계를 주입(DI)
  • 단순히 자바 코드를 호출하는 것 같지만 차이가 있음(싱글톤 컨테이너에서 설명)
  • 생성자를 호출하면서 의존관계 주입이 한번에 처리

좌) 스프링 빈 의존관계 설정 준비 / 우) 완료


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

1) 테스트 코드 작성

(1) 모든 빈 출력하기

  • test에 beanfind패키지를 생성 후 스프링에 등록된 모든 빈 정보를 출력하는 테스트 코드를 작성
  • ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회
  • ac.getBean() : 빈 이름으로 빈 객체(인스턴스)를 조회
package hello.core.beanfind;

class ApplicationContextInfoTest {
    AnnotationConfigApplicationContext ac
                   = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈을 출력")
    void findAllBean() {
        // 스트링 배열로 빈이 정의된 이름을 조회
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("beanDefinitionName = " + beanDefinitionName +
                                           " Object = " + bean);
        }
    }

 

(2) 애플리케이션 빈 출력하기

  • 위처럼 출력하면 스프링이 내부에서 사용되는 빈도 함께 출력 되는데, 내가 등록한 빈만 확인할 수도 있음
  • 스프링 내부에서 사용하는 빈은 getRole()로 구분할 수 있음
  • ROLE_APPLICATION: 일반적으로 사용자가 정의한 빈
  • ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
    @Test
    @DisplayName("애플리케이션 빈 출력")
    void findApplicationBean() {
     
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            //ROLE_APPLICATION : 일반적으로 사용자가 정의한 빈
            //ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈
            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("beanDefinitionName = " + beanDefinitionName + 
                                                         " Object = " + bean);
            }
        }
    }
}

3. 스프링빈 조회

1) 스프링빈 조회 - 기본

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

  • ac.getBean(빈이름, 타입)
  • ac.getBean(타입)
  • 조회 대상 스프링 빈이 없으면 NoSuchBeanDefinitionException 예외 발생 
  • 구체타입으로 조회도 가능하지만 변경시 유연성이 떨어짐(나중에 필요할 때 사용)
package hello.core.beanfind;

class ApplicationContextBasicFindTest {
    AnnotationConfigApplicationContext ac
                   = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName() {
        MemberService memberService = ac.getBean("memberService", MemberService.class);

        // org.assertJ 검증, A.isInstanceOf(B) A가 B의 인스턴스면 true
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("이름 없이 타입으로만 조회")
    void findBeanByType() {
        MemberService memberService = ac.getBean(MemberService.class);
        // 검증 코드 생략
    }

    @Test
    @DisplayName("구체 타입으로 조회")
    void findBeanByName2() {
        MemberServiceImpl memberService =
                          ac.getBean("memberService", MemberServiceImpl.class);
        // 검증 코드 생략
    }

    @Test  // 실패상황 테스트
    @DisplayName("빈 이름으로 조회X")
    void findBeanByNameX() {
        // NoSuchBeanDefinitionException 예외 발생
//        MemberService xxxxx = ac.getBean("xxxxx", MemberService.class);

        // NoSuchBeanDefinitionException이 터져야 true가 되도록 코딩
        assertThrows(NoSuchBeanDefinitionException.class, 
                () -> ac.getBean("xxxxx", MemberService.class));
    }
}

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

(1) 타입이 둘 이상일 때 조회 방법

  • 타입으로 조회 시 같은 타입의 스프링 빈이 둘 이상이면 오류 발생하기 때문에 빈 이름을 지정하여 조회해야 함
  • ac.getBeansOfType()을 사용하면 해당 타입의 모든 빈을 조회 가능
package hello.core.beanfind;

public class ApplicationContextSameBeanFindTest {
    AnnotationConfigApplicationContext ac 
                   = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면 중복 오류 발생")
    void findBeanByTypeDuplicate() {
//        NoUniqueBeanDefinitionException 발생
//        MemberRepository bean = ac.getBean(MemberRepository.class);

        // NoUniqueBeanDefinitionException 에러가 발생하면 true가 되도록 코딩
        assertThrows(NoUniqueBeanDefinitionException.class, () ->
                                    ac.getBean(MemberRepository.class));
    }
    
    @Test
    @DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면 빈 이름을 지정")
    void findBeanByName() {
        MemberRepository memberRepository = ac.getBean("memberRepository1", 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 memberRepository1() {
            return new MemoryMemberRepository();
        }
        
        @Bean
        public MemberRepository memberRepository2() {
            return new MemoryMemberRepository();
        }
    }
}

3) 스프링빈 조회 - 상속 관계

(1) 상속 관계일 때 조회 방법

  • 부모 타입으로 조회하면 자식 타입도 함께 조회 됨
  • Object타입(모든 자바 객체의 최고 조상)으로 조회하면 모든 스프링 빈을 조회

package hello.core.beanfind;

public class ApplicationContextExtendsFindTest {
    AnnotationConfigApplicationContext ac
                    = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 타입으로 조회 시 자식이 둘 이상 있으면 중복 오류가 발생")
    void findBeanByParentTypeDuplicate() {
//        DiscountPolicy bean = ac.getBean(DiscountPolicy.class);;
//        NoUniqueBeanDefinitionException발생
        assertThrows(NoUniqueBeanDefinitionException.class,
                           () -> ac.getBean(DiscountPolicy.class));
    }
    @Test
    @DisplayName("부모 타입으로 모두 조회")
    void findAllBeanByParentType() {
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
    }
	// 직접 등록한 것 외의 것들이 모두 검색 됨
    @Test
    @DisplayName("Object타입으로 모두 조회")
    void findAllBeanByObjectType() {
        Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
    }
    @Configuration
    static class TestConfig {
        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }
        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
    }
}

4. BeanFactory와 ApplicationContext

1) BeanFactory

(1) 설명

  • 스프링 컨테이너의 최상위 인터페이스
  • 스프링 빈을 관리하고 조회하는 역할을 하며 getBean()을 제공
  • 지금까지 사용한 대부분의 기능은 BeanFactory가 제공하는 기능

2) ApplicationContext

(1) 설명

  • BeanFactory기능을 모두 상속 받아서 제공
  • 애플리케이션을 개발할 때는 Bean을 관리하고 조회하는 기능은 물론 수 많은 부가기능이 필요한데 이런 부가 기능을 지원함

(2) ApplicationContext가 제공하는 부가기능

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

3) 정리

  • ApplicationContext는 BeanFactory의 기능을 상속 받으며 빈 관리기능 + 편리한 부가 기능을 제공
  • BeanFactory보다는 부가기능이 포함된 ApplicationContext를 사용
  • BeanFactory나 ApplicationContext를 스프링 컨테이너라 함

5. 다양한 설정 형식 지원(자바코드 / XML)

  • 스프링 컨테이너는 다양한 형식의 설정 정보를 받아들일 수 있게 설계 되어 있음
  • 자바코드, XML, Groovy 등등

1) 애노테이션 기반 자바 코드 설정

  • 지금까지 해왔던 방식으로 AnnotationConfigApplicationContext클래스를 사용하여 자바 코드로된 설정 정보를 넘기면 됨
  • new AnnotationConfigApplicationContext(AppConfig.class)

2) XML 설정

  • 최근에는 스프링부트를 많이 사용하면서 잘 사용하지 않지만 아직 많은 레거시 프로젝트들이 XML로 되어있음
  • XML을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점이 있기에 한번쯤 배워두는 것도 괜찮음

(1) XmlAppConfig 사용 자바 코드

  • GenericXmlApplicationContext를 사용하면서 xml 설정 파일을 넘기면 됨
  • xml기반의 appConfig.xml스프링 설정 정보와 자바 코드로 된 AppConfig.java 설정 정보를 비교해보면 거의 비슷함.
package hello.core.xml;

public class XmlAppContext {
    @Test
    void xmlAppContext() {
        GenericXmlApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

 

(2) xml기반의 스프링 빈 설정 정보

  • xml로 정의한 스프링 빈 설정
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <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"/>
</beans>

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

1) BeanDefinition

(1) 동작 방식

  • 스프링은 BeanDefinition(인터페이스)이라는 추상화가 있기 때문에 다양한 설정 형식이 지원 됨
  • 즉, 역할과 구현을 개념적으로 나눈 것으로 스프링 컨테이너는 BeanDefinition만 알면 되고 구현체는 뭘로 되어있는지는 몰라도됨
  • BeanDefinition을 빈 설정 메타정보라고 하며 @Bean, <bean>당 각각 하나씩 메타 정보가 생성되고 이것을 기반으로 스프링 컨테이너가 스프링 빈을 생성함

 

(2) 코드 레벨로 다이어그램 구현

  • AnnotationConfigApplicationContext는 AnnotatedBeanDefinitionReader를 사용해서 AppConfig.class를 읽고 BeanDefinition을 생성
  • xml도 동일한 방식으로 동작하며 새로운 형식의 설정 정보로도 BeanDefinition을 생성할 수 있음

2) BeanDefinition 살펴보기

(1) BeanDefinition정보

  • BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
  • factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
  • factoryMethodName: 빈을 생성할 팩토리 메서드 지정, ) memberService
  • Scope: 싱글톤(기본값)
  • lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연 처리 하는지 여부
  • InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
  • DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
  • Constructor arguments, Properties: 의존관계 주입에서 사용 (자바 설정 처럼 팩토리 역할의 빈을 사용 하면 없음)
package hello.core.beandefinition;

public class BeanDefinitionTest {
    AnnotationConfigApplicationContext ac 
                    = new AnnotationConfigApplicationContext(AppConfig.class);
//    GenericXmlApplicationContext ac 
//                  = new GenericXmlApplicationContext("appConfig.xml");

    @Test
    @DisplayName("빈 설정 메타정보 확인")
    void findApplicationBean() {
        // .GetBeanDefinitionNames()로 빈 메타정보의 이름을 String[]로 가져오기
        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);
            }
        }
    }
}

 

(2) 정리

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