Java Spring Core

Spring Convertion

Spring을 활용하기 위해 Annotaion을 설정해줘야한다.

AppConfig

package hello.core;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;

@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        /*return new FixDiscountPolicy();*/
        return new RateDiscountPolicy();
    }

}

@Configuration을 설정해줌으로써 해당 부분이 환경 설정하는 부분임을 알린다. @Bean을 통해 해당 객제들을 Spring Container에 Spring Bean으로 등록한다.

DI가 필요한 부분에는 Spring Container에서 찾아온다.

MemberApp

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

OrderApp

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

여기서 사용되는 ApplicationContext는 스프링 컨테이너이고, 인자로 AppConfig.class을 전달함으로써 AppConfig에 설정한 대로 Spring Container을 구성하겠다는 의미이다.

getBean 메소드를 이용해서 Spring Container에 등록된 Spring Bean을 입력받게 된다.

이때, AppConfig에 적힌 메소드 이름들이 각각 Spring Bean의 이름으로 default으로 설정되게 된다.

스프링 컨테이너에 등록된 스프링빈을 검색하는 법에 대해 알아보자

getBean()

Application Context

AnnotationConfigApplicationContext ac=new AnnotationConfigApplicationContext(AppConfig.class);

우선 Application Context 객체를 생성해준다.

getBean(“bean name”,type)

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

}

@Test
@DisplayName("구체 타입으로 조회")
void findBeanByName2(){
    MemberService memberService = ac.getBean("memberService", MemberServiceImpl.class);
    Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);

}

getBean 메소드를 이용해, 타입에 대해서 Spring Bean를 검색할 수 있다.

getBean(type)

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

}

타입만을 인자로 전달해서 bean을 검색할 수 도 있다.

getBean(“bean name”,type) nonexisting_type

@Test
@DisplayName("없는 빈 조회")
void findBeanByNameX(){
    assertThrows(NoSuchBeanDefinitionException.class ,
            () -> ac.getBean("XXXXX",MemberService.class));
}

만약 없는 타입에 대해 Spring Bean을 검색하려고 하면 NoSuchBeanDefinitionException 에러가 발생하게 된다.

Searching Spring Beans for Duplicate Types

만약 중복되는 타입에 대해 Spring Bean을 검색하려고 하면 어떻게 될까?

TestConfig

@Configuration
    static class TestConfig {

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

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

    }

위와 같이 MemoryMemberRepository 타입이 2개가 존재하게 된다.

AnnotationConfigApplicationContext ac=new AnnotationConfigApplicationContext(TestConfig.class);

우선 해당 Config 파일을 이용해서 Spring Container을 구성해준다.

getBean(type)


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

다음과 같이 중복된 타입에 대해서는 명확한 Spring Bean을 얻어낼 수 가 없어 NoUniqueBeanDefinitionException 에러가 발생한다. 이는 타입에 대해서 Spring Bean을 찾으려고 해서 그렇다 –> 이를 해결하기 위해 아래 처럼 Bean name까지 명시해준다.

getBean(“beanname”,type)

    @Test
    @DisplayName("같은 타입이 둘 이상인 경우, 빈 이름으로 조회")
    void findBeanByName(){
        MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);
        assertThat(memberRepository).isInstanceOf(MemberRepository.class);
    }

bean name의 경우 모든 Spring Bean 마다 고유한 값으로 겹치는 Spring Bean이 검색되지 않는다.

getBeansOfType(type)

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

getBeansOfType을 이용하게 되면 해당 타입을 가지고 있는 모든 빈에 대한 조회가 가능하다. 해당 메소드의 반환형은 Map<String,Type> 형태이다.

Spring Bean Inheritance

bean_extends_structure 위와 같이 상속관계를 가지는 Spring Bean에 대해 조회를 해보게 되면 자식 관계에 있는 Spring Bean 모두가 조회 된다. 예를 들어 그림에서 2번 Bean에 대한 조회를 해보면, 2를 포함해서 자식 관계에 있는 4,5번도 같이 조회된다.

Spring Bean에서 상속관계라고 하면 의존관계라고 생각하면 된다.

TestConfig

@Configuration
static class TestConfig{
    @Bean
    public DiscountPolicy fixDiscountPolicy(){
        return new FixDiscountPolicy();
    }
    @Bean
    public DiscountPolicy rateDisCountPolicy(){
        return new RateDiscountPolicy();
    }
}

위와 같이 Spring Container을 구성하게 되면 Discount Policy 아래에는 FixDiscountPolicy, RateDiscountPolicy가 존재하게 된다.

Spring Container

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

getBean(type)

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

위와 같이 자식 관계가 존재하는 Type에 대해 조회를 해보면 NoUniqueBeanDefinitionException 에러가 발생하는데, 이는 위에서 다뤘듯, 중복된 타입이 존재할 때 발생하는 에러이다.

getBean(“beanname”,type) or getBean(specific_type)

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

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

beanname을 명시해서 Spring Bean을 조회하거나, 구체 타입을 명시해서 얻어낸다.

getBeansOfType(type)

@Test
@DisplayName("부모 타입으로 모두 조회하기")
void findAllBeansByParentType(){
    Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
    for (String key : beansOfType.keySet()) {
        System.out.println("key = " + key + " value= " + beansOfType.get(key));
    }
}

위와 같이 getBeansOfType 메소드를 이용하면 DiscountPolicy 아래에 있는 모든 타입에 대해 조회를 할 수 있다.

getBeansOfType(Object.class)

@Test
@DisplayName("부모 타입으로 모두 조회하기 - Object")
void findAllBeansByParentType(){
    Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(Object.class);
    for (String key : beansOfType.keySet()) {
        System.out.println("key = " + key + " value= " + beansOfType.get(key));
    }
}

위와 같이 Object class에 대한 Spring Bean을 조회해보면 모든 Spring Bean이 출력되는 것을 확인할 수 있다. Object 클래스는 모든 클래스가 상속하는 최상위 클래스이므로 그 아래에 있는 모든 클래스들이 출력되게 된다.

Spring Container

Spring Container는 AppConfig 파일을 읽어서 spring_container

위와 같이 Config 파일을 토대로 각각의 메소들을 Spring Bean으로 등록하게 된다. 그래서 우리는 getBean, getBeansOfType,등의 메소드를 이용해서 Spring Bean에 대한 조회가 가능하다.

Spring Container은 아래와 같은 구조로 이루어져있다.

bean_factory_structure

BeanFactory가 최상위 클래스에 존재하는데, 우리가 대부분 사용한 getBean, getBeansOfType은 BeanFactory가 제공하는 메소드들이다. 그러면 왜 우리는 Application Context을 사용하는 것일까?

Application Context

application_context_structure Application Context는 Bean Factory 기능이외에도 Spring Application 구동에 필요한 여러 기능들을 제공해준다.

Interfaces Description
MessageSource 메시지소스를 활용한 국제화 기능, 예를 들어서 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력
EnvironmentCapable 로컬, 개발, 운영등을 구분해서 처리
ApplicationEventPublisher 이벤트를 발행하고 구독하는 모델을 편리하게 지원
ResourceLoader 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

이러한 부가기능들을 제공하므로 우리는 ApplicationContext을 스프링 컨테이너로 이용한다.

AnnotationConfigApplicationContext

ApplicationContext을 자세히 보면 interface로 되어 있는 것을 확인 할 수 있다. 이또한, 역할과 구현의 분리한 것을 알 수 있다. ApplicationContext을 받을 수 있는 구현 클래스들은 아래와 같다.

annotaion_config_structure

스프링 컨테이너인 Application Context는 java 기반의 config file(Annotation)으로 부터 스프링 컨테이너를 생성할 수 있지만, xml 기반의 config file을 이용할 수 있다.

이 처럼 다양한 기반의 Config file 구성을 위해 Application Context을 interface로 구현한 것이다.

GenericXmlApplicationContext

//ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");

이 처럼 xml 기반의 Config 파일을 이용하고자하면 위와 같이 구현 클래스만 변경해주면 된다.

appConfig.xml ```xml

xml을 이용하는 방식은 Spring Annotaion을 이용하는 방식과 유사한 코드 구조를 가지고 있다.

## Bean Definition

Spring 에서는 어떻게 다양한 형식(java,xml) 기반으로 작성된 config file을 읽어서 Spring Bean으로 등록하는 것일까?

![bean_definition](/assets/images/jsf/bean_definition_structure.png)

이는 Spring Container가 Spring Bean을 생성할 때, Bean Definition 이라는 정보를 이용해서 Spring Bean을 만들기 때문이다. 즉, Spring Container에서는 이 Bean Definition이 Java 기반으로 형성된거지, xml 기반으로 형성된거지 상관없이 오직 Bean Definition을 이용해서 Spring Bean을 만들게 된다. --> 여기서도 역할/구현 분리의 개념이 포함된 것이다.

두번째 그림을 보면 Application Context을 구현하는 구체 클래스들이 각각 설정파일을 읽고 Bean Definition을 만들기 위해 BeanDefinitionReader라는 것을 이용하는 것을 확인 할 수 있다.즉 각각의 구체 클래스들은 개별적으로 Reader을 활용해서 Bean Definition을 만들고, Spring Container은 이 Bean Defintion Spring Bean을 만들기만 하면 되는 것이다.

*Spring Bean 1개에 대해서는 Bean Definition 1개가 존재하며, Bean Definition은 Bean에 대한 메타 정보라고 이해하면 된다.*

Bean Definition 내에 들어있는 정보는 아래와 같다

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

>Bean Definition Test

```java
AnnotationConfigApplicationContext ac=new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("Application 빈 출력하기")
void findApplicationBean() {
    String[] beanDefinitionNames = ac.getBeanDefinitionNames();
    for (String beanDefinitionName : beanDefinitionNames) {
        /*
        BeanDefinition.ROLE_APPLICATION == 사용자 정의 bean만 조회
        BeanDefinition.ROLE_INFRASTRUCTURE == 스프링 내부에 사용되는 bean 조회
        */
        BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
        if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name = " + beanDefinitionName + "BeanDefinition= " + beanDefinition);
        }
    }
}
method description
getBeanDefinitionNames Spring Container에 등록된 bean definition name list 검색
getBeanDefinition 특정 bean definition에 대한 조회

References

link: inflearn

link:springcore

link:spring_framework

댓글남기기