BackEnd/Spring

[Spring] 스프링 컨테이너와 스프링 빈

샤아이인 2022. 2. 1.

내돈내고 내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

 

1. 스프링 컨테이너 생성

스프링 컨테이너가 생성되는 과정과 코드를 살펴보자!

 

다음 코드를 먼저 살펴보자.

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
 

애노테이션 기반의 구성 정보를 담고 있는 AppConfig.class가 있다. 이를 사용하여 스프링 컨테이너를 생성할 수 있다.

위의 ApplicationContext는 인터페이스에 해당한다.

따라서 이를 구현한 여러 구현체들이 있다. 그중 우리는 애노테이션을 활용하는 구현체를 선택한 것이다.

 

이에 관해서 따로 내가 정리해둔 글이 있다. 꼭 읽어보길 권장한다.

이렇게 생성된 applicationContext가 스프링 컨테이너라고 할 수 있다.

더 정확히 하려면 BeanFactory와 ApplicationContext를 구분해서 이야기해야 한다.

일반적으로 BeanFactory를 직접 사용하는 경우는 거의 없다. 따라서 보통 ApplicationContext를 스프링 컨테이너라 부른다.

 

구성 정보를 기반으로 스프링 컨테이너가 생성되면 다음 사진과 같이 생성된다.

 

1) 스프링 빈 등록

출처 - 인프런 스프링 (김영한) 강의

스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록한 것이다.

 

기본적으로 Bean의 이름은 메서드 이름과 동일하게 되는데, 속성 값으로 @Bean(name="원하는 이름") 을 주면 직접 지정할 수 있다.

또한 모든 빈은 항상 다른 이름을 가져야 한다.

 

2) 의존관계 설정

스프링 컨테이너는 설정 정보를 참고하여 의존관계 주입(DI)를 진행한다.

출처 - 인프런 스프링 (김영한) 강의

위의 글을 보면 스프링은 빈을 생성하고, 의존관계를 주입하는 단계를 나누어서 2단계로 설명하였다.

하지만 현실은 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존관계 주입도 한번에 처리된다.

위의 글에서는 이해를 돕기 위해 개념적으로 단계를 나누어 설명한 것이다.

 

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

이번에는 직전에 작성한 코드들이 스프링 컨테이너에 다 빈으로 등록되는지 확인해보는 시간이었다.

이를 test코드로 작성해 보자!

public 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("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);
            }
        }
    }
}
 

모든 빈 출력하기 부분에서는 내가 생성한 빈 뿐만 아니라 스프링이 내부적으로 생성하는 빈까지 모두 확인해볼 수 있었다.

애플리케이션 빈 출력하기는 내가 생성한 빈들만 확인할 수 있었다.

 

우선 ac.getBeanDefinitionNames() 으로 스프링에 등록된 모든 빈의 이름을 리스트 형식으로 받아온다.

 

이후 for-each문을 돌면서 ac.getBeanDefinition(beanDefinitionName) 을 통하여 Bean에 대한 meta data 정보들을 반환한다.

코드에서는 스프링이 내부에서 사용하는 빈을 getRole()로 구분하기 위해 사용함.

 

최종적으로 getBean()으로 빈 객체를 구해온다.

 

이후 출력하여 확인해보면 다음과 같다.

인텔리제이 단축키

iter + 엔터 치면 바로 forEach문 생성해 준다.

soutm 는 함수명 출력문, soutv는 직전 변수명 출력문 생성

 

3. 스프링 빈 조회 - 기본

이번시간에는 getBean을 통하여 빈을 가져오는 여러 test를 진행하였다. 다음 코드를 살펴보자.

package hello.core.beanfind;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ApplicationContextBasicFindTest {

    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 findBeanByType2() {
        MemberService memberService = ac.getBean(MemberServiceImpl.class); // 구체타입 적용
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("빈 이름으로 조회X")
    void findBeanByNameX() {
        //ac.getBean("xxxxx", MemberService.class);
        assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean("xxxxx", MemberService.class));
    }
}
 

3번째의 구체 타입으로 조회는 별로 좋지 못한 코드라고 하셨다.

왜냐하면 인터페이스 가 아닌 구상클래스에 의존하고 있기 때문이다.

 

4번째의 조회가 안되는 test또한 재미있는 부분이었다.

우선 원하는 예외를 던지는지 확인하기 위해서는 assertThrows를 사용한다. 인자는 2개를 넘겨준다.

첫 인자는 발생하긴 원하는 예외 class이며, 두번째 인자는 해당 로직을 람다로 넘길 수 있다.

 

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

우선 코드부터 본 후 설명하겠습니다~

package hello.core.beanfind;

import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ApplicationContextSameBeanFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
    
    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다") 
    void findBeanByTypeDuplicate() {
        //DiscountPolicy bean = ac.getBean(MemberRepository.class);
        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();
        }
    }
}
 

1번 테스트는 타입으로 빈을 검색했기 때문에 오류가 발생하였습니다. MemberRepository 타입이 2개나 등록되어 있기 때문이죠!

 

이를 해결해 주기 위해서는 이름도 함께 인자로 넘겨주면 해결이 가능하게 됩니다. 2번 테스트에서 확인할 수 있습니다.

 

마지막으로 특정 타입에 해당되는 모든 빈들을 가져오고 싶다면 ac.getBeansOfType(MemberRepository.class) 과같이 사용하면 됩니다.

반환값으로는 Map을 얻게 됩니다!. 따라서 key값들을 keySet()으로 얻어와 반복문을 돌면서 출력할수도 있죠!

3번 테스트에서 확인해볼 수 있었습니다.

 

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

getBean()을 통해 빈을 조회할때 부모 타입으로 조회하면 자식까지 한번에 조회된다.

그래서 모든 자바 객체의 최고 부모인 Object 타입으로 조회하면, 모든 스프링 빈을 조회한다. 다음 그림을 살펴보자.

출처 - 인프런 스프링 (김영한) 강의

2번으로 검색하면 2번을 상속한 4, 5 번까지 조회할 수 있다.

 

이번시간 또한 테스트 코드를 작성하면서 공부하였다. 다음 코드를 살펴보자.

package hello.core.beanfind;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

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

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 중복 오류가 발생한다")
    void findBeanByParentTypeDuplicate() {
        //DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
        assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(DiscountPolicy.class));
    }

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 빈 이름을 지정하면 된다")
    void findBeanByParentTypeBeanName() {
        DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
        assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("특정 하위 타입으로 조회")
    void findBeanBySubType() {
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기")
    void findAllBeanByParentType() {
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        assertThat(beansOfType.size()).isEqualTo(2);
        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();
        }
    }
}
 

 

6. BeanFactory와 ApplicationContext

BeanFactory와 ApplicationContext의 전체적인 상속관계는 다음과 같다.

● BeanFactory

스프링 컨테이너의 최상위 인터페이스다.

스프링 빈을 관리하고 조회하는 역할을 담당한며, getBean() 을 메서드를 제공한다!

지금까지 우리가 사용했던 대부분의 기능은 BeanFactory가 제공하는 기능이라고 하셨다.

 

● ApplicationContext

BeanFactory 기능을 모두 상속받아서 제공해준다. 이뿐만 아니라 추가적인 기능들 까지 더 제공해준다.

우선 다음 그림을 살펴봅시다~

출처 - 인프런 스프링 (김영한) 강의

1) 메시지소스를 활용한 국제화 기능

예를 들어서 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력하는 국제화가 추가 기능을 제공한다!

 

2) 환경변수

로컬, 개발, 운영등을 구분해서 처리할 수 있다 하셨다.

 

3) 애플리케이션 이벤트

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

 

4) 편리한 리소스 조회

파일, 클래스패스, 외부 등 에서 리소스를 편리하게 조회할 수 있다!

 

정리해보면, ApplicationContext는 BeanFactory를 상속받았기 때문에 빈 관리기능에 더불어 + 추가적인 부가 기능들을 제공하는 것 이다. 따라서 BeanFactory를 직접 사용하기보다는 거의 ApplicationContext를 사용하게 된다고 하셨다~

 

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

위에서도 봤듯 스프링은 빈을 등록하기 위한 여러가지 방식을 제공한다. 애노테이션 방식, XML 방식 등등 다양한 설정 형식을 지원한다.

어떻게 다른 설정정보를 갖고 동일한 빈들을 생성하게 되는 것 일까?

 

그 중심에는 BeanDefinition 이라는 추상화가 있다!

 

역할 과 구현을 개념적으로 나눈 것 이다!

XML 이든, 애노테이션 방식이든, 읽어서 BeanDefinition을 만들면 된다. 스프링 입장에서는 오직 이 메타 정보만 알면 되는 것 이다!

출처 - 인프런 스프링 (김영한) 강의

AnnotationConfigApplicationContext 는 AnnotatedBeanDefinitionReader 를 사용해서 AppConfig.class 를 읽고 BeanDefinition 을 생성한다.

 

GenericXmlApplicationContext 는 XmlBeanDefinitionReader 를 사용해서 appConfig.xml 설정 정보를 읽고 BeanDefinition 을 생성한다.

 

스프링에서 빈을 등록하는 방식에는 크게 2가지가 있다.

 

1) 직접 스프링 빈을 등록하는 방식 (XML 방식)

위의 테스트 결과를 보면 class 정보가 스프링에게 직접적으로 들어나있으며, 펙토리 메서드는 사용하지 않았다.

위 방식은 스프링 빈을 해당 경로에 가서 직접적으로 생성하는 방식이다.

따라서 class 경로가 적혀 있으며, factoryBeanName 과 factoryMethodName은 null이 되게 된다.

 

2) 우회 하여 등록하는 방식 (애노테이션 방식이 팩토리 메서드 방식이다, 패턴 말하는거 아님!!)

우회하여 등록한 방식은 class에 대한 정보가 spring에게 없다.

factoryBeanName(appConfig) 에 있는 factoryMethodName(memberService)를 통해 bean이 생성된다.

이는 memberService라는 factoryMethod를 사용하여 생성했음을 알 수 있다!


BeanDefinition 정보

BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)

factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig

factoryMethodName: 빈을 생성할 팩토리 메서드 지정, 예) memberService

Scope: 싱글톤(기본값)

lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연처리 하는지 여부

InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명

DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명

Constructor arguments, Properties: 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)

댓글