반응형
스프링 컨테이너와 스프링 빈
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
- 편리한 리소스 조회(파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회)
- MeesageSource
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 |