스프링 공부 (인프런 김영한 선생님)/스프링 핵심원리

[스프링 핵심원리] 4. 스프링 전환 / 스프링 컨테이너 기본

ProgYun. 2023. 5. 10. 10:57

스프링을 사용하여, AppConfig.java를 리팩터링합니다

package hello;

import hello.springintroduction.Member.MemberRepository;
import hello.springintroduction.Member.MemberService;
import hello.springintroduction.Member.MemberServiceImpl;
import hello.springintroduction.Member.MemoryMemberRepository;
import hello.springintroduction.discount.DiscountPolicy;
import hello.springintroduction.discount.FixDiscountPolicy;
import hello.springintroduction.discount.RateDiscountPolicy;
import hello.springintroduction.order.OrderService;
import hello.springintroduction.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// 사용영역과 구성 영역으로 구역이 이분할 됨.
// 사용영억(클라이언트 코드), 구성영역 (DIP 코드)

@Configuration
public class AppConfig { // 생성자를 통한 주입
    // 각각의 Impl에 생성자를 구현함으로써 Dependency injection이 가능함.
    // XML로 사용하면 AppConfig.java 조차 필요가 없다!

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

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

    /*public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }*/

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

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


}

AppConfig에 설정을 구성한다는 뜻을 스프링에 전하는 @Configuration Annotation을 클래스 선언 상단에 추가해줍니다

각 메서드에 @Bean Annotation을 추가함으로써 , 스프링 컨테이너에 스프링 빈으로 등록할 수 있습니다.

 

스프링 Bean 등록에 따라, MemberApp, OrderApp 코드를 리팩터링 하겠습니다

 

package hello.springintroduction;

import hello.AppConfig;
import hello.springintroduction.Member.Grade;
import hello.springintroduction.Member.Member;
import hello.springintroduction.Member.MemberService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MemberApp {

    public static void main(String[] args) {
        //AppConfig appConfig = new AppConfig();
        //MemberService memberService = appConfig.memberService();

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);

        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("findMember = " + findMember);
        System.out.println("member = " + member);

    }

}
package hello.springintroduction;

import hello.AppConfig;
import hello.springintroduction.Member.Grade;
import hello.springintroduction.Member.Member;
import hello.springintroduction.Member.MemberService;
import hello.springintroduction.Member.MemberServiceImpl;
import hello.springintroduction.order.Order;
import hello.springintroduction.order.OrderService;
import hello.springintroduction.order.OrderServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class OrderApp {

    public static void main(String[] args) {
        //OrderService orderService = new OrderServiceImpl();

        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        OrderService orderService = ac.getBean("orderService", OrderService.class);

        Long memberId=1L;
        Member member= new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 1000);
        System.out.println("order = " + order);
    }
}

 

- 스프링 컨테이너

ApplicationContext를 스프링 컨테이너라 합니다 (ApplicationContext - 전체 애플리케이션 상의 문맥)

AppConfig를 사용해서 직접 객체를 생성하고 DI를 했던 과거와 달리,

ApplicationContext가 이제 모든 객체를 생성하고 Dependency Injection을 수행합니다.

 

ApplicaitonContext, 즉 스프링 컨테이너는 @Configuration Annotation이 붙은 클래스를 설정(구성) 정보로 사용합니다.

 

여기서 @Bean이라 적인 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록합니다.

반환된 객체 - AppConfig.java에서 각 메서드마다 기술했던 return 된 객체를 의미한다.

 

스프링 컨테이너에 등록된 객체를 스프링 빈이라 명명합니다

@Bean의 경우 기본적으로 메서드의 이름을 빈의 이름으로 따라 가져가는 것을 기본값으로 하나

@Bean() 괄호 안에 옵션으로 name을 인자로 User - Defined 로 설정할 수 있습니다.

 

AppConfig에서 직접 조회한 과거와 달리 이젠 ApplicationContext를 이용하여 스프링 빈을 등록했기 때문에

이제부터는 ApplicationContext 클래스 내 기능인 .getBean()메서드를 이용하여 조회해야합니다.

 


Spring Container와 Spring Bean의 생성 과정

ApplicationContext는 스프링 컨테이너이자, Interface에 해당합니다

다음 코드를 통해, ApplicationContext를 생성하면 스프링 내부에서 다음과 같은 과정으로 컨테이너가 생성됩니다

 

ApplicationContext ac = new AnnotationApplicationContext(AppConfig.class);

 

AppConfig.class -> 내가 이 클래스 안에 설정정보를 구성해두었다는 뜻 (구성 정보를 인자로 넘겨 주어야 한다)

 

전에 언급했던대로 ApplicationContext는 인터페이스이기 때문에 하위에 XML로 구성정보를 넘겨주는 경우 등

다양한 구성정보 형식을 받아서 스프링 컨테이너를 생성할 수 있도록 했다.

 

지금은, .class @Configuration Annotation을 썼다는 전제하에 설명합니다.

 

 

2. 스프링 빈 등록

 

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

 

파라미터로 넘어온 설정 클래스 정보 - AppConfig.class를 의미합니다.

 

이 때 스프링 빈을 등록하는 과정에서 이름이 어떻게 등록되는가에 대해서 알아볼 필요가 있습니다.

- 빈 이름은 기본적으로 메서드의 이름을 사용합니다

- 이전에 후술했던 대로 @Bean()의 ()안에 name 인자를 parameter로 넘기므로써 name을 수동으로 설정할 수 있습니다.

 

단, Bean 이름의 경우 반드시 중복되어서는 안됩니다. 같은 이름을 부여하면 다른 빈이 무시되거나 기존 빈을 덮어버리는데

최근 스프링에서는 무조건 오류 발생으로 변경되었습니다 (애매모호한건 해서는 안된다고 수업중에 언급하고 넘어가셨습니다)

 

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

이전에 AppConfig.class에서 수동으로 DI 해줬던것과 다르게 스프링 컨테이너(ApplicationContext)는 자동으로 의존관계를 주입해줍니다.

 

스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져 있습니다.

빈을 등록하면, 생성자를 호출하면서 의존관계 주입도 한번에 처리됩니다.

스프링 컨테이너는 AppConfig.class에 기술된 설정정보(메서드와 메서드 내의 의존관계)를 파악하고 의존관계를 주입합니다

자바코드를 호출하는것과는 다르게 차이가 있습니다 (싱글턴 컨테이너에서 설명)

Bean을 쭉 생성하고 -> 후에 연결(DI)을 진행합니다.

 


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

 

스프링 컨테이너에 모든 빈들이 정상적으로 등록되었는지 확인하는 방법에 대해 공부합니다

package hello.springintroduction.beanFind;

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

    @Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findApplicationBean(){
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("beanDefinitionName = " + beanDefinitionName +
                    " object = " + bean);
            }

        }
    }

}

- 모든 빈 출력하는 방법

실행하게 되는 경우 스프링에 등록된 모든 빈 정보를 출력할 수 있습니다

.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회할 수 있습니다.

.getBean() : 빈 이름으로 객체 인스턴스를 조회할 수 있습니다

 

위 코드에서는 String 배열로 .getBeanDefinitionNames() 를 통해 스프링에 등록된 모든 빈 이름을 조회하고

for each 문을 통해 모두 출력하고 있습니다.

 

이 때, getBeanDefinition을 통해 등록된 bean에 대한 메타데이터 정보를 가져올 수 있습니다.

 

메타데이터 정보 내에는 그 Spring Bean이 Spring 내부에서 사용하는 빈인지 또는 사용자가 정의한 빈인지 구분하는

Role 정보가 enum 형식으로 담겨있습니다 따라서

 

BeanDefinition.ROLE_APPLICATION : 일반적으로 사용자가 정의한 빈

BeanDefinition.INFRASTRUCTURE : 스프링 내부에서 사용하는 빈

 

으로 구분하여 출력할 수 있습니다


스프링 빈 조회 - 기본

 

다음 두 방법으로 스프링 빈을 조회할 수 있습니다

ApplicationContext 내의 .getBean()을 이용합니다

 

- .getBean(빈 이름, 타입)

- .getBean(타입)

 

이 때 조회 대상이 없는 경우 NoSuchBeanDefinitionException이 발생합니다

 

코드 예제는 다음과 같습니다

package hello.springintroduction.beanFind;

import hello.AppConfig;
import hello.springintroduction.Member.MemberService;
import hello.springintroduction.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("빈 이름으로 조회")
    void findByBeanName(){
        MemberService memberService = ac.getBean("memberService", MemberService.class);


        Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("이름 없이 타입으로만 조회 (인터페이스로 조회)")
    void findByBeanType(){
        MemberService memberService = ac.getBean(MemberService.class);


        Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("이름 없이 타입으로만 조회 (구체 클래스로 조회)")
    void findByBeanTypeImpl(){
        MemberService memberService = ac.getBean(MemberServiceImpl.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);

        // 반환 타입 관계없이 스프링 빈에 등록된 인스턴스 타입으로 결정(실제 구체로 적어도 됨, 근데 안좋다 / OCP, DIP)
        // 역할에 의존하고 구현에 의존하지 말자, 이 코드는 별로 좋은 코드가 아니지만, 살다보면 이상과 현실에 괴리가 있다는걸 느낄때 쓰자
    }



    @Test
    @DisplayName("빈 이름으로 조회 -> 없는 경우")
    void findByBeanNameX(){
        //MemberService memberService = ac.getBean("xxxx", MemberService.class);

        org.junit.jupiter.api.Assertions.assertThrows(NoSuchBeanDefinitionException.class,
            () -> ac.getBean("xxxx", MemberService.class));

        // () -> ac.~ 를 실행했을 때, NoSuchDefinitionException이 터지면 Test Pass
    }



}

 

구체 타입, 즉 ApplicationContext를 사용하지 않고 AnnotationConfigApplicationContext를 사용하게 되면 유연성 저하

다형성을 최대한 활용하여 유연성을 확보하기 위해, ApplicationContext를 참조변수의 타입으로 설정!

 


스프링 빈 조회 - 동일한 타입이 둘 이상인 경우

 

타입으로 조회하는 경우 같은 타입이 둘 이상이면 오류가 발생함 (NoUniqueBeanDefinitionException)

이때는 빈 이름을 지정해야 할 필요가 있습니다.

 

ac.getBeansOfType()를 사용하게 되면 해당 타입의 모든 빈을 조회할 수 있습니다

 

예제 코드는 아래와 같습니다

package hello.springintroduction.beanFind;

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

import hello.AppConfig;
import hello.springintroduction.Member.MemberRepository;
import hello.springintroduction.Member.MemoryMemberRepository;
import hello.springintroduction.discount.DiscountPolicy;
import java.util.Map;
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;

public class ApplicationContextSameBeanFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(
        SameBeanConfig.class
    );

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

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다")
    void findBeanByName(){
        MemoryMemberRepository memberRepository = ac.getBean("memberRepository1",
            MemoryMemberRepository.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);
            System.out.println("beansOfType.get(key) = " + beansOfType.get(key));
        }
        System.out.println("beansOfType = " + beansOfType);
        assertThat(beansOfType.size()).isEqualTo(2);
    }

    @Configuration // static - 클래스 내에서만 사용하는 Config로 Scope 제한
    static class SameBeanConfig {

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

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

    }

}

key : bean의 이름을 가져옴

beansOfType.get(key) : bean 객체의 주소를 가져옴(고유 이름)