본문 바로가기
Spring/스프링 핵심 원리

인프런 스프링 기본 강의 정리 #4

by 예린lynn 2023. 11. 23.
728x90

 1. 스프링 컨테이너 생성

//스프링 컨테이너 생성
ApplicationContext applicationContext =
                   new AnnotationConfigApplicationContext(AppConfig.class);

ApplicationContext 를 스프링 컨테이너이자 인터페이스이다.

그리고 new AnnotationConfigApplicationContext(AppConfig.class)는 ApplicationContext의 구현체이다.

 

스프링 컨테이너의 생성 과정을 알아보도록 하자.

1) 스프링 컨테이너 생성

스프링 컨테이너를 생성하면 그 안에 스프링 빈 저장소가 생성된다.

이때 구성 정보(AppConfing.class)를 지정해줘야 한다.

 

2) 스프린 빈 등록

@Bean이 붙은 코드의 내용을 호출한다.

 

빈 이름은 @Bean이 붙은 메소드의 메소드명이다.

빈 이름을 직접 부여할 수도 있다 -> @Bean(name="...")

이때 빈 이름은 중복되면 안 되고, 항상 다른 이름을 부여해야 한다.

 

3) 스프링 빈 의존관계 설정 - 준비

 

4) 스프링 빈 의존관계 설정 - 완료

스프링 컨테이너에 빈이 등록되면 등록된 빈들 사이의 의존관계가 설정된다. 이를 의존관계 주입(DI)라고 한다.

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

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

스프링 컨테이너에 실제 스프링 빈들이 잘 등록되었는지 확인할 수 있다.

src/test에 beanfind 패키지를 생성하고, 그 아래 ApplicationContextInfoTest 클래스를 생성한다.

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()를 통해 등록된 모든 빈들의 이름을 가져온다.
  • 가져온 빈의 이름을 반복문으로 순회하면서, ac.getBean(beanDefinitionName)을 통해 해당 빈의 객체를 가져와 빈의 이름과 객체를 출력한다.
  • 각 빈에 대한 BeanDefinition을 가져와서 해당 빈의 RoleROLE_APPLICATION인지 확인한다.
  • ROLE_APPLICATION은 직접 등록한 애플리케이션 빈을 나타내며, 이를 만족하는 빈들의 이름과 객체를 출력한다.

3. 스프링 빈 조회 - 기본

스프링 컨테이너에서 빈을 찾기 위해 src/test/beanfind에 ApplicationContextBasicFindTest 클래스를 생성한다.

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

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName() {
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        //memberService가 MemberServiceImpl에 있으면 성공
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

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

    // 구체 타입으로 조회 시 변경시 유연성이 떨어지는 단점이 있음
    @Test
    @DisplayName("구체 타입으로 조회")
    void findBeanByName2() {
        //MemberService라는 이름의 빈을 구체 타입인 MemberServiceImpl로 조회
        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

실패 시의 테스트도 생성해보자.

//실패 테스트
    @Test
    @DisplayName("빈 이름으로 조회X")
    void findBeanByNameX() {
    
        //ac.getBean("xxxxx", MemberService.class); -> xxxx는 없는 이름
        //오른쪽에 있는 로직이 성공하면, 왼쪽에 있는 예외가 발생
        
        Assertions.assertThrows(NoSuchBeanDefinitionException.class, () ->
                ac.getBean("xxxxx", MemberService.class));
    }

 

assertThrows()는 테스트 코드에서 예외 발생을 검증하기 위해 사용되는 메서드이다. 이 메서드는 특정 코드 실행 시 예외가 발생하는지를 확인하고, 해당 예외가 기대한 예외인지를 검증한다.

assertThrows(ExpectedExceptionClass.class, () -> {
    // 예외가 발생할 것으로 예상되는 코드 블록
    // 해당 코드 블록을 실행할 때 예외가 발생해야 테스트가 성공
});

ExpectedExceptionClass는 기대하는 예외 클래스를 나타내며, 코드 블록 내부에는 특정 동작이나 메서드 호출을 포함한 코드가 위치한다. 이 코드 블록이 실행될 때 예외가 발생하면 테스트는 성공하고, 발생한 예외가 기대한 예외와 일치하지 않으면 테스트는 실패한다.

 

전체 테스트 코드는 다음과 같다.

import hello.core.AppConfig;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;

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 static org.assertj.core.api.Assertions.*;

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 findBeanByName2() {
 MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
 assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
 }
 
 @Test
 @DisplayName("빈 이름으로 조회X")
 void findBeanByNameX() {
 //ac.getBean("xxxxx", MemberService.class);
 Assertions.assertThrows(NoSuchBeanDefinitionException.class, () ->
ac.getBean("xxxxx", MemberService.class));
 }
}

cf) 클래스 목록 확인 : Ctrl+E 키

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

타입으로 조회시 같은 타입이 둘 이상이면 오류가 발생한다.

public class ApplicationContextSameBeanFindTest {

    AnnotationConfigApplicationContext ac=new AnnotationConfigApplicationContext(SameBeanConfig.class);

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

//static이면 이 클래스에서만 적용된다
    @Configuration
    static class SameBeanConfig{
        @Bean
        public MemberRepository memberRepository1(){
            return new MemoryMemberRepository();
        }
        
        @Bean
        public MemberRepository memberRepository2(){
            return new MemoryMemberRepository();
        }
    }

위와 같은 오류는 빈 이름을 지정해서 해결할 수 있다.

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다")
    void findBeanByTypeDuplicate() {
        //MemberRepository 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);
 }
  • ac.getBeansOfType(MemberRepository.class)를 사용하여 MemberRepository 타입의 모든 빈을 맵 형태로 가져온다.
  • Map<String, MemberRepository> beansOfType에는 키는 빈의 이름이 되고, 값은 해당 빈의 객체가 된다.
  • 가져온 빈들을 순회하면서 각 빈의 이름과 객체를 출력한다.
  • 마지막으로, assertThat(beansOfType.size()).isEqualTo(2)를 사용하여 조회된 MemberRepository 타입의 빈이 2개인지 검증한다.

이 테스트는 MemberRepository 타입의 모든 빈을 스프링 애플리케이션 컨텍스트에서 조회하고, 그 개수가 예상한 것과 일치하는지를 확인한다. 이를 통해 해당 타입의 빈들이 모두 올바르게 등록되고 관리되고 있는지를 검증할 수 있다.

 

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

부모 타입으로 조회하면, 자식 타입도 함께 조회한다.

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

 

위 내용을 보여주는 테스트 코드를 생성해보자.

src/test/beanfind에 ApplicationContextExtendsFindTest 클래스를 생성한다.

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);
    }
    
    @Configuration
    static class TestConfig {
    
        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }
        
        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
    }

자식이 둘 이상 있으면 오류가 발생하기 때문에 빈 이름으로 지정해준다.

 

상속 관계에 따라 특정 하위 타입 혹은 부모 타입으로 조회할 수 있다.

    @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));
        }
    }

 

Object 타입을 이용하여 모든 스프링 빈을 조회할 수 있다.

    @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));
        }
    }

 

6. BeanFactory와 ApplicationContext

-BeanFactory

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

ApplicationContext가 제공하는 부가기능

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

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

스프링 컨테이너는 다양한 형식의 설정 정보를 받아들인다. ex) 자바 코드, XML, Groovy

지금까지 사용했던 AppConfig.class는 자바 코드를 이용한 것이다.

 

-XML 설정 사용

최근에는 스프링 부트를 많이 사용하면서 XML기반의 설정은 잘 사용하지 않는 추세이다. 

그러나 아직 많은 프로젝트들이 XML로 되어 있고, XML을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 있다.

 

XML 설정 파일을 만들기 위해 src/test에 xml 패키지를 생성하고, 그 아래에 XmlAppContext 클래스를 생성한다.

import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import static org.assertj.core.api.Assertions.*;

public class XmlAppContext {

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

 

다음으로 src/main/resources에 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>

appConfig.xml 스프링 설정 정보와 자바 코드로 된 AppConfig.java 설정 정보를 비교해보면, 거의 비슷하다.

 

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

스프링은 BeanDefinition이라는 추상화를 이용해서 다양한 설정 형식을 지원한다.

 즉, 역할과 구현을 개념적으로 나눈 것으로 XML, 자바코드를 읽어 BeanDefinition을 만들면 된다.

또한 스프링 컨테이너는 BeanDefinition만 알면 된다.

BeanDefinition을 빈 설정 메타정보라고 하는데, @Bean과 <bean> 당 각각 하나씩 메타 정보가 생성된다.

 

AnnotationConfigApplicationContext AnnotatedBeanDefinitionReader를 사용해서 

AppConfig.class를 읽고 BeanDefinition을 생성한다.

 

코드를 통해 BeanDefinition에 대해 자세히 알아보도록 하자.

먼저 src/test/java에 beandefinition 패키지를 생성하고, 그 안에 BeanDefinitionTest 클래스를 생성한다.

import hello.core1.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);
            }
        }
    }
}
728x90