스프링 빈 기능
Spring

스프링 빈 기능

반응형

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

1.스프링 컨테이너 생성

목표

  • Spring의 @Bean 기능을 사용하여 스프링 컨테이너에 등록하는 연습을 해보자
  • Spring의 ac.getBean(),  ac.getBeanDefinitionNames(), ac.getBeanDefinitionName() 기능을 사용해보자
  • Configuration을 사용해 Contatiner에 Bean을 저장하는 방법을 수행해보자
//AppConfig.class

import bluish_Community_Projcet.bluish.discount.DiscountPolicy;
import bluish_Community_Projcet.bluish.discount.RateDiscountPolicy;
import bluish_Community_Projcet.bluish.member.*;
import bluish_Community_Projcet.bluish.order.OrderService;
import bluish_Community_Projcet.bluish.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy(){
        return new RateDiscountPolicy();
    }
}
//Test/discount/RateDiscountPolicy

import bluish_Community_Projcet.bluish.AppConfig;
import bluish_Community_Projcet.bluish.member.Grade;
import bluish_Community_Projcet.bluish.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

class RateDiscountPolicyTest {
    DiscountPolicy discountPolicy;

    @BeforeEach
    public void beforeEach(){
        AppConfig appconfig = new AppConfig();
        discountPolicy = appconfig.discountPolicy();
    }

    @Test
    @DisplayName("청년주택 실 거주자용 쿠폰 할인 테스트")
    void resident(){
        Member member = new Member(1L, "memberA", Grade.RESIDENT);
        int discount = discountPolicy.discount(member, 120000);
        Assertions.assertThat(discount).isEqualTo(12000);
    }

    @Test
    @DisplayName("청년주택 비 거주자용 쿠폰 할인 테스트")
    void basic(){
        Member member = new Member(1L, "memberA", Grade.BASIC);
        int discount = discountPolicy.discount(member, 120000);
        Assertions.assertThat(discount).isEqualTo(0);
    }
}

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

  • AnnotationConfigApplicationContext에서 Spring Bean을 탐색할 때 사용하는 메소드
/**
 * ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회
 * ac.getBeanDefinition(beanDefinitionName)은 beanDefinitionName의 빈을 찾는다.
 * ac.getBean(빈 이름) : 빈 이름으로 빈 객체(인스턴스)를 조회한다.
 */

ROLE_APPLICATION : 사용자가 정의한 빈(내부를 뜯어본 결과 0로 설정된 값)
ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈(내부를 뜯어본 결과 1로 설정된 값)
//test/blush/beanfind/ApplicationContextInfoTest
package bluish_Community_Projcet.bluish.beanfind;

import bluish_Community_Projcet.bluish.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

    @Test
    @DisplayName("모든 빈 조회하기")
    public void findAllBean() throws Exception {
        //given
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        //when
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);
            //then
            System.out.println("beanDefinitionName = " + beanDefinitionName + " object = " + bean);
        }
    }

    @Test
    @DisplayName("사용자추가 어플리케이션 빈 조회하기")
    public void findApplicationBean() throws Exception {
        //given
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        //when
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + " object= " + bean);
            }
        }
        //then
    }
}

3.스프링 빈 조회 - 기본

스프링 컨테이너에서 스프링 빈을 찾아야 할 때

//스프링 컨테이너에서 스프링 빈을 찾는 기본적인 조회 방법
1.ac.getBean(빈 이름, 타입)
2.ac.getBean(타입) //중복 이름이 있을 경우 문제 발생

예시

package bluish_Community_Projcet.bluish.beanfind;

import bluish_Community_Projcet.bluish.AppConfig;
import bluish_Community_Projcet.bluish.member.MemberService;
import bluish_Community_Projcet.bluish.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;

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

    @Test
    @DisplayName("빈 이름으로 조회하기")
    public void findBeanByName() throws Exception{
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("이름 없이 타입으로만 조회하기")
    public void findBeanByType() throws Exception{
        MemberService memberService = ac.getBean(MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("구체 타입으로 조회하기")
    public void findCertainType() throws Exception{
        //given
        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    //NoSuchBeanDefinitionException이 발생하여야 테스트 성공
    @Test
    @DisplayName("없는 빈 이름으로 조회하여 오류 유도하기")
    public void findNoBeanName() throws Exception{
        //given
        org.junit.jupiter.api.Assertions.assertThrows(NoSuchBeanDefinitionException.class,
                () -> ac.getBean("Nothing name", MemberService.class));
    }
}
  • AppConfig에서 등록한 @Bean들의 Return 값이 ac.getBean()을 사용해서 구하는것을 볼 수 있다.

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

특정 타입의 빈을 모두 조회하는 방법

  • ac.getBeansOfType을 사용하면 모든 빈을 받을 수 있다. 반환 값을 Map<string, Objcet>로 반환된다
  • 예시
Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);

💡 assertThrow를 사용해서 예외처리 비교할때 org.junit.jupiter.api.Assertions의 Aseertions를 사용해야 한다!

사용 예시

void findBeanByTypeDuplicate(){
        assertThrows(NoUniqueBeanDefinitionException.class, ()->ac.getBean(MemberRepository.class));
    }

연습코드

//test/beanfind/ApplicationContextSameBeeanFindTest
package bluish_Community_Projcet.bluish.beanfind;

import bluish_Community_Projcet.bluish.member.MemberRepository;
import bluish_Community_Projcet.bluish.member.MemberService;
import bluish_Community_Projcet.bluish.member.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
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.junit.jupiter.api.Assertions.*;

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

    @Test
    @DisplayName("타입으로 조회시 같은 값이 둘 있는 경우 오류 발생 유도 테스트")
    public void findBeanByTypeDuplicate() throws Exception{
        assertThrows(NoUniqueBeanDefinitionException.class, ()->ac.getBean(MemberRepository.class));
    }

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있을 경우 명시적 이름을 지정하여 조회 가능하다")
    public void findBeanByName() throws Exception{
        MemberRepository memberRepository1 = ac.getBean("memberRepository1", MemberRepository.class);
        Assertions.assertThat(memberRepository1).isInstanceOf(MemberRepository.class);
    }

    @Test
    @DisplayName("특정 타입으로 모두 조회하기")
    public void finAllBeanByName() throws Exception{
        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);
        Assertions.assertThat(beansOfType.size()).isEqualTo(2);
    }

    @Configuration
    static class sameBeanTest{
        @Bean
        public MemberRepository memberRepository1(){
            return new MemoryMemberRepository();
        }
        @Bean
        public MemberRepository memberRepository2(){
            return new MemoryMemberRepository();
        }
    }
}

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

  • bean은 상속 관계에 있어서 자신이 상속 받은 bean또한 포함해서 줄줄이 나오는 형식이다
///test/beanfind/ApllicationContextExtendsFindTest

package bluish_Community_Projcet.bluish.beanfind;

import bluish_Community_Projcet.bluish.discount.DiscountPolicy;
import bluish_Community_Projcet.bluish.discount.FixDiscountPolicy;
import bluish_Community_Projcet.bluish.discount.RateDiscountPolicy;
import org.junit.jupiter.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 org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

import static org.assertj.core.api.Assertions.*;

public class ApplicationContextExtendsFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

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

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

    @Test
    @DisplayName("특정 하위 타입으로 조회")
    public void findBeanbySubType(){
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
    }
    @Test
    @DisplayName("부모 타입으로 모두 조회하기")
    public void findAllBeanByParentType(){
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        for (String s : beansOfType.keySet()) {
            System.out.println("key = " + s + " value = " + beansOfType.get(s));
        }
        System.out.println("beansOfType = " + beansOfType);
    }

    @Test
    @DisplayName("최상단 부모 Object로 Bean을 모두 뽑아오기(Bean의 상속된 모든것을 연속적으로 뽑아오는것을 유추 가능)")
    public void findAllBeanObjectType(){
        Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
        for (String s : beansOfType.keySet()) {
            System.out.println("key = " + s + " value = " + beansOfType.get(s));
        }
        System.out.println("beansOfType = " + beansOfType);
    }
    
    @Configuration
    static class TestConfig{
        @Bean
        public DiscountPolicy rateDiscountPolicy(){
            return new RateDiscountPolicy();
        }
        @Bean
        public DiscountPolicy fixDiscountPolicy(){
            return new FixDiscountPolicy();
        }
    }
}

6.BeanFactory와 ApplicationContext

스프링 컨테이너에서 ApplicationContext와 BeanFactory

  • 스프링 컨테이너를 사용하기 위해서는 ApplicationContext 인터페이스를 많이 사용한다.
  • 실제로 ApplicationContext 인터페이스를 많이 사용하지만 ApplicationContext는 최상위 인터페이스가 아니다!
  • 최상위 인터페이스는 BeanFactory이다.
  • BeanFactory는 bean을 관리하고 조회하는 역할을 한다. 대표적으로 getBean()을 제공한다.
  • 그럼에도 ApplicationContext를 새로만들어서 BeanFactory를 상속해서 재정의한 이유는 웹 개발에서 필요한 여러 기능을 제공하기 위해 추가적으로 다른 요소를 상속받아 기능을 추가한 인터페이스이기 때문이다. 대표적으로는 아래의 4가지 추가 상속 사항이 있다.
    • MeesageSource
      • 메시지 소스를 통한 국제화 기능
      • 예를 들자면 한국에서 접속하면 한국어가 나올수 있게, 영어권에서 접속하면 영어가 나올수 있게 하는 기능을 의미
    • EnvironmentCapable
      • 환경변수 관리
      • 로컬 개발, 운영 등을 구분해서 처리한다.
    • ApplicationEventPublisher
      • 이벤트를 발행하고 구독하는 모델을 편리하게 지원
    • ResourceLoader
      • 편리한 리소스 조회(파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회)

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

💡 XML 솔직히 안 써서 복습할 필요 없을듯. 레거시 프로젝트 할 일이 생길 때 그 때만보자

  • 스프링에서 레거시 프로젝트를 진행할 경우 XML 등록이 필요할 때 사용하자
  • ID는 메소드명
  • class는 반환할 객체의 위치
  • name과 ref는 생성자를 의미한다.
  • appConfig.java→ appConfig.xml을 비교하면서 보자
//appConfig.java
package hello.core;

import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemoryMemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public DiscountPolicy discountPolicy(){
        return new FixDiscountPolicy();
    }
}
//appConfig.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>

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

  • 스프링에 bean을 등록하는 방법은 크게 xml을 이용해 직접 bean을 등록하는 방법과 팩토리 메소드를 사용하는 방법이다. 팩토리 메소드 방법은 우리가 보통 @Bean 어노테이션을 활용해 등록하는 방법을 의미한다.
  • 보통은 팩토리 메소드 패턴을 사용한다.
  • 그러나 가끔 오픈소스 컨트리뷰션등 강연이 진행될때 BeanDefiname을 직접 꺼내서 코딩하거나.. 가끔 등장할 때가 있으니 알아두자

스프링의 다향한 설정 형식을 지원하는 논리

  • 스프링이 다양한 설정형식 (XML, class ...)등을 지원할 수 있는것은 추상화를 통해 구현되어있기 때문이다.
  • 그림을 보면 BeanDefinition을 상속해서 정의를 하기 때문에 실제로 스프링 컨테이너는 class, xml인지 뭔지 알지도 못하고 오로지 BeanDefinition으로만 해석한다.
  • BeanDefinition을 빈 설정 메타정보라 하며 @Bean을 사용해서 등록하면 하나의 정보가 등록된다.

동작원리는 아래와 같다.

  • 각각의 Reader들이 설정정보(like AppConfi.class, AppConfig.xml)를 해석한뒤 BeanDefiniton 정보를 만든다.
  • BeanDefinition에는 아래와 같은 정보를 얻을 수 있다.
    • BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
    • factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
    • factoryMethodName: 빈을 생성할 팩토리 메서드 지정, 예) memberService
    • Scope: 싱글톤(기본값)
    • lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연처리 하는지 여부InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
    • DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
    • Constructor arguments, Properties: 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
  • 이 중에서 factorBeanName, Method정보를 가지고 팩토리패턴을 이뤄낸다.
  • 팩토리 패턴 정보를 사용하면 정보를 확인할 때 class 이름이 null로 되있는것을 볼 수 있다. 예제를 통해 확인해보자
package hello.core.beandefinition;

import hello.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class BeanDefinitionTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 설정 메타정보 확인")
    void findApplicationBean(){
        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);
            }
        }
    }
}

실행결과

static class passiveBeanInfo{
        String bean;
        String scope;
        boolean ab;
        boolean lazyInit;
        Integer autowireMode;
        Integer dependencyCheck;
        boolean autowireCandidate;
        boolean primary;
        String factoryBeanName;
        String factoryMethodName;
        String initMethodName;
        String destroyMethodName;
        String pos; //Config 위치
    }
  • 조회 시 위와 같은 정보를 얻을 수 있다.
  • Bean을 자료형으로 등록할 경우도 있으니 필요할 때 찾아서 사용하자
반응형

'Spring' 카테고리의 다른 글

Spring 메시지, 국제화  (0) 2022.06.27
Spring의 Validation이란  (0) 2022.06.27
Spring MVC 구조 및 구현  (0) 2022.04.16
Spring의 종류와 장점  (0) 2022.04.12
좋은 객체 지향 설계의 5가지 원칙(SOLID)  (0) 2021.09.30