저번 시간에 애플리케이션 전체의 구성하는 AppConfig가 어떻게 제어의 역전과 의존관계 주입을 할 수 있는지
그리고 스프링을 이용한 스프링 컨테이너와 스프링빈으로 AppConfig를 어떻게 활용할 수 있는지 알아보았다.
이번 시간에는 스프링 컨테이너와 스프링 빈에 대해서 조금 더 알아보도록 하자!
1. 스프링 컨테이너
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
이런 코드를 통해서 우리는 OrderApp과 MemberApp에 스프링 컨테이너를 활용했었다.
이 중 ApplicationContext를 우리는 스프링 컨테이너라고 한다.
그런데 ApplicationContext와 AnnotaionConfigApplicationContext는 유사해보이는데 뭐가 다른 것일까?
ApplicationContext는 '인터페이스'이다.
그리고 AnnotaionConfigApplicationContext가 구현체인 것이다.
스프링 컨테이너는 XML 기반으로도 만들 수 있고, 에노테이션 기반의 자바 설정 클래스로도 만들 수 있다.
위 코드는 자바 설정 클래스를 기반으로 스프링 컨테이너를 만든 방법이다.
2. 스프링 컨테이너의 생성 과정
스프링 컨테이너를 생성할 때 구성정보를 지정해줘야 한다.
AppConfig.class가 그 역할을 수행한다.
그럼 AppConfig는 @Configuration 이 붙었은 클래스이다. 해당 클래스에서 @Bean 이 붙은 메서드들을 스프링 머테이너에 등록하기 시작한다.
스프링 빈 저장소에는 (빈 이름, 빈 객체) 이런 식으로 스프링 빈이 저정된다.
package hello.core_review;
import hello.core_review.Discount.DiscountPolicy;
import hello.core_review.Discount.FixDiscountPolicy;
import hello.core_review.member.MemberRepository;
import hello.core_review.member.MemberService;
import hello.core_review.member.MemberServiceImpl;
import hello.core_review.member.MemoryMemberRepository;
import hello.core_review.order.OrderService;
import hello.core_review.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 FixDiscountPolicy();
}
}
AppConfig의 내용은 위와 같다.
그렇다면
스프링 컨테이너에는
이와 같이 스프링 빈이 저장되어 있을 것이다.
스프링 컨테이너는 파라미터러 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록한다.
빈 이름은 메서드 이름을 사용하는게 기본적이지만, 이름을 직접 부여할 수도 있다.
ex) @Bean(name = "memberService2")
그리고 스프링 컨테이너는 설정 정보를 참고해서 의존 관계를 주입한다.
단순히 자바 코드를 호출하는 것과는 차이가 있다.
정말 등록됐는지 확인하는 방법은 Junit을 통해 테스트 해보면 된다.
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);
}
}
먼저 AppConfig에 등록된 모든 빈을 출력해보겠다.
.getBeanDefinitionNames() 를 통해서 스프링에 등록된 모든 빈 이름을 조회할 수 있다.
그리고 이 이름을 이용해서 ac.getBean()을 이용해 등록된 모든 빈을 출력한다.
그런데 출력해보면 내가 등록한 빈만 존재하는게 아니라 스프링 내부에서 사용하는 빈까지 모두 출력되는 모습을 볼 수 있다.
@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("name = " + beanDefinitionName + " Object = " + bean);
}
}
}
이럴 때는 .getBeanDefinition을 사용해서 BeanDefinition을 추출하고.
이 추출한 BeanDefinition이 ROLE_APPLICATION인지 확인해주면 된다. 이는 사용자가 정의한 애플리케이션 빈인지 확인하는 것이다.
참고로 ROLE_INFRASTRUCTURE는 스프링 내부에서 사용하는 빈을 확인하는 것이다.
3. 스프링 빈 조회
스프링 빈을 조회하는 기본적인 방법으로는 인자로
- 빈 이름, 타입을 주는 방법 -> ac.getBean(빈이름, 타입)
- 타입을 주는 방법 -> ac.getBean(타입)
이렇게 두 가지가 있다. 만약 조회 대상 스프링 빈이 없다면 예외가 발생한다.
타입으로 조회하는 방법은 추상 타입이 아니라 구체 타입으로 조회하는 방법도 존재하지만 유연성이 떨어지는 단점이 있다.
package hello.core_review.beanfind;
import hello.core_review.AppConfig;
import hello.core_review.member.MemberService;
import hello.core_review.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 ApplicationConfigApplicationContext {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("빈 이름으로 조회")
void findBeanByName() {
MemberService memberService = ac.getBean("memberService", MemberService.class);
Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("빈 이름 없이 타입만으로 조회")
void findBeanByType() {
MemberService memberService = ac.getBean( 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);
}
@Test
@DisplayName("빈 이름으로 조회 X")
void findBeanByNameX() {
// ac.getBean("xxxxx", MemberService.class);
org.junit.jupiter.api.Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean("xxxxx", MemberServiceImpl.class));
}
}
그런데 타입으로만 빈을 조회할 때 같은 타입인 스프링 빈이 두개 이상 존재한다면 문제가 생긴다.
이럴때는 빈 이름을 함께 지정해주어야 한다.
또한 특정 타입의 빈을 모두 조회하고 싶다면 .getBeanOfType()를 이용하고, 인자로 타입을 넣어주면 된다.
그럼 Map자료구조 형태로 반환된다.
package hello.core_review.beanfind;
import hello.core_review.member.MemberRepository;
import hello.core_review.member.MemoryMemberRepository;
import org.junit.jupiter.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.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
public class ApplicationContextSameBeanContext {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
@Test
@DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생")
void findBeanByTypeDuplicate() {
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);
}
@Configuration
static class SameBeanConfig {
@Bean
public MemberRepository memberRepository1() {
return new MemoryMemberRepository();
}
@Bean
public MemberRepository memberRepository2() {
return new MemoryMemberRepository();
}
}
}
스프링 빈 조회시 상속 관계는 어떻게 될까?
구현 객체들은 모두 interface를 상속하고 있다.
스프링 빈 조회시 부모 타입으로 조회하면 자식 타입도 함께 조회된다.
그런데 부모타입으로 조회시 자식 타입이 둘 이상 있으면 중복 오류가 발생한다.
그럴 때는 빈 이름을 함께 지정해주어야 한다.
Object 클래스는 모든 타입의 부모임으로 해당되는 모든 자식 속성이 출력된다.
package hello.core_review.beanfind;
import hello.core_review.Discount.DiscountPolicy;
import hello.core_review.Discount.FixDiscountPolicy;
import hello.core_review.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.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.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
public class ApplicationContextExtentsFindTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
@Test
@DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면 중복 오류 발생")
void findBeanByParentTypeDuplicate() {
assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(DiscountPolicy.class));
}
@Test
@DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면 빈 이름 지정")
void findBeanByParentTypeBeanName() {
DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy1", DiscountPolicy.class);
assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
}
@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("ket = " + key + " value = " + beansOfType.get(key));
}
}
@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));
}
}
@Configuration
static class TestConfig {
@Bean
public DiscountPolicy rateDiscountPolicy1() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy rateDiscountPolicy2() {
return new FixDiscountPolicy();
}
}
}
4. BeanFactory와 ApplicationContext
BeanFactory는 스프링 컨테이너의 최상위 인터페이스이다. 스프링 빈을 관리하고 조히하는 역할을 담당한다.
getBean() 메서드를 제공하는 인터페이스이다.
ApplicationContext 는 BeanFactory를 상속받아 제공한다.
ApplicationContext는 BeanFactory 이외의 다른 인터페이스도 상속 받기 때문에 애플리케이션을 개발할 때 필요한 부가 기능들을 지원해줄 수 있다.
이렇게 다양한 인터페이스들을 상속받는다.
BeanFactory나 ApplicationContext를 스프링 컨테이너라고 부른다.
이전에 우리는 스프링 컨테이너를 설계하는 방식이 애노테이션 기반 코드 설정 말고도 XML설정을 사용하는 방법이 있다고 말하였다.
이렇게 ApplicatioinContext를 상속받는 클래스들이 존재한다. 우리는 그 중 애노테이셔 기반 코드를 이용한 AnnotationConfigApplicationContext를 이용해왔다.
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_review.member.MemberServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository" />
</bean>
<bean id="memberRepository"
class="hello.core_review.member.MemoryMemberRepository" />
<bean id="orderService" class="hello.core_review.order.OrderServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository" />
<constructor-arg name="discountPolicy" ref="discountPolicy" />
</bean>
<bean id="discountPolicy" class="hello.core_review.Discount.RateDiscountPolicy" />
</beans>
package hello.core_review.xml;
import hello.core_review.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
public class XmlAppContext {
@Test
void xmlAppContext() {
ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
MemberService memberService = ac.getBean("memberService", MemberService.class);
Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
}
}
5. 스프링 빈 설정 메타 정보 - BeanDefinition
스프링은 BeanDefinition이라는 추상화를 통해서 이렇게 다양한 설정 형식을 지원한다.
이 또한 역할과 구현을 나눈 것으로 이해하면 된다.
스프링 컨테이너는 BeanDefinition을 의존하기 때문에 이를 빈 설정 메타 정보라고 한다.
스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다
더 크게 보면
이렇게 생각할 수 있다. 결국은 BeanDefinition이라는 빈 메타정보가 생성되고 이를 스프링 컨테이너가 읽는다.
BeanDefinition은
- BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
- factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
- factoryMethodName: 빈을 생성할 팩토리 메서드 지정, 예) memberService
- Scope: 싱글톤(기본값)
- lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연 처리 하는지 여부
- InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
- DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
- Constructor arguments, Properties: 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리 역할의 빈을 사용 하면 없음)
이런 정보들을 포함하고 있다.
'Spring' 카테고리의 다른 글
[Spring 핵심 원리 이해] 6. 의존관계 자동 주입 (1) | 2025.01.03 |
---|---|
[Spring 핵심 원리 이해] 5. 컴포넌트 스캔 (0) | 2024.12.28 |
[Spring 핵심 원리 이해] 4. 싱글톤 컨테이너 (0) | 2024.12.27 |
[Spring 핵심 원리 이해] 2. 객체 지향 원리 적용 (2) | 2024.12.18 |
[Spring 핵심 원리 이해] 1. 예제 만들기 (1) | 2024.12.07 |