본문 바로가기
Spring

[Spring] 스프링 컨테이너와 스프링 빈

by soro.k 2022. 9. 14.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

본 포스팅은 <스프링 핵심 원리 - 기본편> 강의 내용을 복습하기 위해 간략히 작성된 글이므로 전체 내용은 위 강의에서 확인 가능합니다. 

 

1. 스프링이란

2. 객체지향의 5가지 원칙(SOLID), 관심사의 분리 

3. 제어의 역전(IoC)과 의존관계 주입(DI)

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

5. 싱글톤 패턴과 싱글톤 컨테이너

6. 컴포넌트 스캔

7. @Autowired 필드명 매칭, @Qualifier, @Primary

8. 빈 생명주기 콜백

9. 빈 스코프


스프링 컨테이너 생성

스프링 컨테이너는 설정 정보를 참고해서 스프링 빈을 등록하고 의존관계를 설정해줍니다.

 

먼저 스프링 컨테이너를 생성하는 코드를 작성해 보겠습니다.

ApplicationContext ac 
	= new AnnotationConfigApplicationContext(AppConfig.class);

스프링 컨테이너는 XML 기반이나 애노테이션 기반 자바 설정 클래스로 만들 수 있는데요. 여기에서는 ApplicationContext(인터페이스)의 구현체로 AnnotationConfigApplicationContext(XX.class) 즉, 애노테이션 기반 자바 설정 클래스를 선택해서 스프링 컨테이너를 생성했습니다.

 

XML 기반 사용 코드

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/resource/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>

 

 

스프링 컨테이너를 생성하는 코드를 작성했으니 동작 과정에 대해 살펴보겠습니다.

스프링 컨테이너 동작 과정

 

1. 스프링 컨테이너를 생성할 때는 구성 정보를 지정해줘야 하는데 여기서 아래 AppConfig.class를 지정해줬습니다.

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

 

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

- memberService

- memberRepository

- orderService

- discountPolicy

 

3. 그리고 아래의 그림과 같이 의존관계 설정을 완료합니다.

의존관계 설정

 

BeanFactory와 ApplicationContext

BeanFactory스프링 컨테이너의 최상위 인터페이스스프링 빈을 관리하고 조회합니다. ApplicationContext는 이 BeanFactory의 기능을 모두 상속받아서 제공을 하는데요. ApplicationContext에는 빈을 관리하고 조회하는 기능 외에도 여러 부가 기능에 있습니다. 그래서 우리가 직접 BeanFactory를 사용할 일은 거의 없습니다.

 

 

ApplicationContext만이 가진 부가 기능은 다음과 같습니다.

  • MessageSource(메시지소스를 활용한 국제화 기능)
  • EnvironmentCapable(환경변수) : 로컬, 개발, 운영등을 구분해서 처리
  • ApplicationEventPublisher(애플리케이션 이벤트) : 이벤트를 발행하고, 구독하는 모델을 편리하게 지원
  • ResourceLoader(편리한 리소스 조회) : 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

 

 

스프링 빈 조회

스프링 빈이란 개념을 많이 들어봤을 텐데 이렇게 스프링 컨테이너에 등록된 객체스프링 빈이라고 합니다. 스프링 빈을 찾으려면 ac.getBean()메서드를 사용하면 됩니다.

 

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

 

테스트 코드

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 findBeanByName() {
		MemberService memberService = 
			ac.getBean("memberService", memberServiceImpl.class);
		assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
	}
}

인터페이스에만 의존하는 규칙을 지키기 위해서는 구체 타입으로 조회하지 않는 것이 좋습니다.

 

 

만약 빈이 없다면 ‘NoSuchBeanDefinitionException’ 에러를 만날 수 있습니다. 이럴 때는 테스트 코드를 어떻게 작성해야 할까요?

@Test
@DisplayName("빈 이름으로 조회X")
void findBeanByName() {
	MemberService xxxx = 
		ac.getBean("xxxx", memberService.class);
	assertThrows(NoSuchBeanDefinitionException.class, 
		() -> ac.getBean("xxxx", memberService.class));
}

assertThrows를 이용해서 오른쪽 로직을 실행했을 때 ‘NoSuchBeanDefinitionException’ 에러가 떠야 테스트가 성공할 수 있게 만든 코드입니다.

 

 

스프링 빈에서 중요한 것은 중복되지 않게 해야 한다는 것입니다. 빈이 2개 이상으로 찾아지면 ‘NoUniqueBeanDefinitionException’에러를 만날 수 있는데 아래와 같이 테스트 코드를 작성할 수 있습니다.

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

 

참고로 BeanDefinition이란 빈 설정 메타정보라고 하는데 @Bean, <bean>당 각각 하나씩 메타 정보를 생성됩니다. 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성하는 것이죠.

 

 

그런데 이렇게 스프링 빈이 2개 이상이어서 에러가 날 때는 어떻게 해결해야 할까요?

 

[Spring] @Autowired 필드명 매칭, @Qualifier, @Primar

스프링 핵심 원리 - 기본편 - 인프런 | 강의 스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런... www.inflearn.

justsora.tistory.com

 

 

만약 조회한 빈이 모두 필요할 때는 어떻게 해야 할까요?

예를 들어서 할인 서비스를 제공하는데 클라이언트가 할인의 종류를 선택할 수 있다고 가정해 봅시다. 스프링을 사용하면 전략 패턴을 간단하게 구현할 수 있습니다.

public class AllBeanTest {
    @Test
    void findAllBean() {
        ApplicationContext ac = 
            new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
        DiscountService discountService = ac.getBean(DiscountService.class);
        Member member = new Member(1L, "userA", Grade.VIP);
        int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
        assertThat(discountService).isInstanceOf(DiscountService.class);
        assertThat(discountPrice).isEqualTo(1000);
    }

    static class DiscountSerivice {
        private final Map<String, DiscountPolicy> policyMap;
        private final List<DiscountPolicy> policies;
        
        public DiscountService(Map<String, DiscountPolicy> policyMap,
        List<DiscountPolicy> policies) {
            this.policyMap = policyMap;
            this.policies = policies;
            System.out.println("policyMap = " + policyMap);
            System.out.println("policies = " + policies);
    }
    
    public int discount(Member member, int price, String discountCode) {
        DiscountPolicy discountPolicy = policyMap.get(discountCode);
        System.out.println("discountCode = " + discountCode);
        System.out.println("discountPolicy = " + discountPolicy);
        return discountPolicy.discount(member, price);
    }

    }
}

DiscountService는 Map으로 DiscountPolicy를 주입받는데 이때 두 개의 구현 객체를 주입받습니다. discount() 메서드에 “fixDiscountPolicy”가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아서 실행하는 것이죠.