이 글은 김영한님의
스프링 핵심 원리 - 기본편
강의를 수강하고 정리한 내용입니다.
강의 보러가기
//스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
ApplicationContext
를 스프링 컨테이너라고 한다.
ApplicationContext
는 인터페이스이며 위 코드에선 AnnotationConfigApplicationContext
를 구현체로 선택했다.
어노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든 것이다.
참고: 더 정확히는 스프링 컨테이너를 부를 때
BeanFactory
,ApplicationContext
로 구분해서 이야기한다.
BeanFactory
를 직접 사용하는 경우는 거의 없으므로 일반적으로ApplicationContext
를 스프링 컨테이너라 한다.
1. 스프링 컨테이너 생성
new AnnotationConfigApplicationContext(AppConfig.class)
스프링 컨테이너가 생성된다.
스프링 컨테이너 안에는 스프링 빈 저장소라는 것이 있다.
위 코드처럼 스프링 컨테이너를 생성할 때는 구성 정보를 지정해주어야 한다.
2. 스프링 빈 등록
구성 정보로 넘겨준 AppConfig.class에 등록된 Bean을 모두 호출해서
메소드명을 빈 이름, 반환 되는 객체를 빈 객체로 등록한다.
@Bean(name = "memberService2")
❗주의: 빈 이름은 항상 다른 이름을 부여해야 한다.❗
같은 이름을 부여하면 다른 빈이 무시되거나 기존 빈을 덮어버리거나 설정에 따라 오류가 발생한다.
-> 그냥 빈 이름은 항상 다르게 부여하자
3. 스프링 빈 의존관계 설정
스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입(DI)한다.
memberService
는 memberRepository
메소드를 호출하므로 memberRepository
에 의존하고 있다.
orderService
는 memberRepository
와 discountPolicy
를 호출하므로 둘 다 의존하고 있다.
그냥 단순히 자바 코드를 호출하는 것처럼 보이지만, 차이가 있다.
-> 싱글톤 컨테이너에서 설명
- 위 예제는 스프링이
memberService
를 빈으로 등록할 때memberService
내부에서memberRepository
를 호출한다.
- 빈 생성 과정과 의존관계 주입이 한꺼번에 이루어지게 된다.
- 하지만 스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나뉘어져 있다.
이 부분은 뒤에 싱글톤 컨테이너에서 다시 설명한다.
이제 스프링 컨테이너에 스프링 빈들이 잘 등록되어 있는지 직접 조회해 보자.
@Test
@DisplayName("모든 빈 출력하기")
void findAllBean() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name = " + beanDefinitionName + ", object = " + bean);
}
}
스프링에 등록된 모든 빈을 전부 출력한다.
ac.getBeanDefinitionNames()
: 스프링에 등록된 모든 빈 이름을 조회한다.ac.getBean()
: 빈 이름으로 빈 객체(인스턴스)를 조회한다.
내가 등록한 빈뿐만 아니라 스프링 내부에서 사용하는 빈까지 모두 출력이 되었다.
@Test
@DisplayName("애플리케이션 빈 출력하기")
void findApplicationBean() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
// Role ROLE_APPLICATION : 직접 등록한 애플리케이션 빈
// Role ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈
if (beanDefinition.getRole() == beanDefinition.ROLE_APPLICATION) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name = " + beanDefinitionName + ", object = " + bean);
}
}
}
내가 등록한 빈만 출력한다.
스프링이 내부에서 사용하는 빈은 getRole()
로 구분이 가능하다.
ROLE_APPLICATION
: 일반적으로 사용자가 정의한 빈ROLE_INFRASTRUCTURE
: 스프링이 내부에서 사용하는 빈💡 구성 정보로 넘겨준
AppConfig
도 스프링 빈에 자동으로 등록된다. 💡
스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회 방법
@Test
@DisplayName("빈 이름으로 조회")
void findBeanByName() {
MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("구체 타입으로 조회")
void findBeanByName2() {
MemberService memberService = ac.getBean("memberService", MemberServiceImpl.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("이름 없이 타입으로만 조회")
void findBeanByType() {
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
// 실패 테스트
@Test
@DisplayName("빈 이름으로 조회X")
void findBeanByNameX() {
// ac.getBean("xxxxx", MemberService.class);
assertThrows(NoSuchBeanDefinitionException.class,
() -> ac.getBean("xxxxx", MemberService.class));
}
ac.getBean(빈이름, 타입)
ac.getBean(타입)
조회 대상 스프링 빈이 없으면 예외 발생
NoSuchBeanDefinitionException: No bean named 'xxxxx' available
- 구체 타입으로 조회하면 변경 시 유연성이 떨어지기 때문에 인터페이스 타입으로 조회하는 것이 좋다.
- 항상 역할과 구현을 구분해야하며 역할에 의존하도록 설계하자.
타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생한다.
ac.getBeansOfType()
을 사용하면 해당 타입의 모든 빈을 조회할 수 있다.
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
@Test
@DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다.")
void findBeanByTypeDuplicate() {
// MemberRepository bean = ac.getBean(MemberRepository.class);
assertThrows(NoUniqueBeanDefinitionException.class,
() -> ac.getBean(MemberRepository.class));
}
@Configuration
static class SameBeanConfig {
@Bean
public MemberRepository memberRepository1() {
return new MemoryMemberRepository();
}
@Bean
public MemberRepository memberRepository2() {
return new MemoryMemberRepository();
}
}
스프링 입장에서 MemberRepository
타입으로 등록된 빈이 두 개가 있는데 둘 중에 어떤 것을 반환해주어야 할지 모른다.
NoUniqueBeanDefinitionException
에러 발생getBean()
메소드에 타입뿐만 아니라 빈 이름까지 넘겨주어 해결한다.
특정 타입의 빈 하나만 조회하는게 아니라 모두 조회하고 싶은데..
그럴땐 getBeansOfType()
을 사용하면 된다. (반환 타입은 Map)
@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();
System.out.println("beansOfType = " + beansOfType);
assertThat(beansOfType.size()).isEqualTo(2);
}
출력 결과
key = memberRepository1 value = hello.core.member.MemoryMemberRepository@3c9bfddc
key = memberRepository2 value = hello.core.member.MemoryMemberRepository@1a9c38eb
beansOfType = {memberRepository1=hello.core.member.MemoryMemberRepository@3c9bfddc, memberRepository2=hello.core.member.MemoryMemberRepository@1a9c38eb}
특정 타입 빈을 모두 조회하는 방법은 나중에
@Autowired
같이 자동으로 의존관계를 주입할 때 이런 기능이 적용되기 때문에 설명한 것
이 부분이 중요하다!
부모 타입으로 빈을 조회하면, 자식 타입도 함께 조회한다.
그래서 모든 자바 객체의 최고 부모인 Object
타입으로 조회시, 모든 스프링 빈을 조회한다.
부모 타입 조회 예시 테스트 코드
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
@Test
@DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 중복 오류가 발생한다.")
void findBeanByParentTypeDuplicate() {
// DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
assertThrows(NoUniqueBeanDefinitionException.class,
() -> ac.getBean(DiscountPolicy.class));
}
@Test
@DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 빈 이름을 지정하면 된다.")
void findBeanByParentTypeBeanName() {
DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", 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 findByBeanByParentType() {
Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " value=" + beansOfType.get(key));
}
assertThat(beansOfType.size()).isEqualTo(2);
}
@Configuration
static class TestConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
여기까지 조회하는 기본적인 방법들을 모두 살펴보았다.
사실 몇 가지 기능이 더 있지만 이정도만 알아두면 된다.
- 실제로
ApplicationContext
에서 빈을 조회하는 일은 많지 않다.- 나중에 자동 의존관계 주입을 이해하려면 이정도만 알아두면 된다.
스프링 컨테이너의 최상위 인터페이스다.
스프링 빈을 관리하고 조회하는 역할을 담당한다.
getBean()
을 제공한다.
지금까지 이 글에서 사용했던 대부분의 기능은 BeanFactory
가 제공하는 기능이다.
BeanFactory
기능을 모두 상속받아서 제공한다.
애플리케이션을 개발할 때는 빈을 관리하고 조회하는 기능은 물론이고, 수 많은 부가기능이 필요하다.
메시지소스를 활용한 국제화 기능
한국에서 들어오면 한국어로, 영어권에서 들어오면 영어 등 알맞은 언어로 출력
환경변수
개발할 때는 크게 로컬, 개발, 운영 환경으로 나뉜다.
로컬, 개발, 운영 등을 구분해서 처리
애플리케이션 이벤트
편리한 리소스 조회
ApplicationContext
는 BeanFactory
의 기능을 상속받는다.
ApplicationContext
는 빈 관리기능 + 편리한 부가 기능을 제공한다.
BeanFactory
를 직접 사용할 일은 거의 없다. 부가기능이 포함된 ApplicationContext
를 사용한다.
BeanFactory
나 ApplicationContext
를 스프링 컨테이너라 한다.
지금까지는 자바 코드로 설정을 했는데 스프링 컨테이너는 다양한 형식의 설정 정보를 받을 수 있도록 유연하게 설계되어 있다.
지금까지 사용했던 것이다.
AnnotationConfigApplicationContext
클래스를 사용하면서 자바 코드로된 설정 정보를 넘기면 된다.
최근에는 스프링 부트를 많이 사용하면서 XML 기반의 설정은 잘 사용하지 않는다.
많은 레거시 프로젝트들이 XML로 되어 있고, 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점도 있으므로 한번쯤 배워두는 것도 괜찮다.
GenericXmlApplicationContext
를 사용하면서 xml 설정 파일을 넘기면 된다.
테스트 코드
@Test
@DisplayName("XML 기반 간단한 빈 조회")
void xmlAppContext() {
ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
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="orderService" class="hello.core.order.OrderServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository"/>
<constructor-arg name="discountPolicy" ref="discountPolicy"/>
</bean>
<bean id="memberRepository" class="hello.core.member.MemoryMemberRepository"/>
<bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy"/>
</beans>
AppConfig
클래스와 거의 같은 설정이다.
xml
도 지원하고Java
도 지원하고..
위에xml
기반 예제 코드를 보면 스프링 컨테이너 구현체만 다를뿐, 나머지 코드는 똑같네?
❓ 스프링은 어떻게 이런 다양한 설정 형식을 지원할 수 있는거지? ❓
-> 그 중심에는 BeanDefinition
이라는 추상화
(인터페이스)가 있다.
💡 쉽게 이야기해서 역할과 구현을 개념적으로 나눈 것이다!
XML을 읽어서 BeanDefinition을 만들면 된다.
자바 코드를 읽어서 BeanDefinition을 만들면 된다.
스프링 컨테이너는 자바 코드인지, XML인지 몰라도 된다. 오직 BeanDefinition
만 알면 된다.
BeanDefinition
을 빈 설정 메타정보라 한다.
@Bean
, <bean>
당 각각 하나씩 메타정보가 생성된다.
스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다.
BeanDeifinition
에만 의존한다.AnnotationConfigApplicationContext
는 AnnotatedBeanDefinitionReader
를 사용해서 AppConfig.class
를 읽고 BeanDefinition
을 생성한다.
GenericXmlApplicationContext
는 XmlBeanDefinitionReader
를 사용해서 appConfig.xml
설정 정보를 읽고 BeanDefinition
을 생성한다.
새로운 형식의 설정 정보가 추가되면, XxxBeanDefinitionReader
를 만들어서 BeanDefinition
을 생성하면 된다.
AnnotationConfigApplicationContext
의 코드 일부분이다. AnnotatedBeanDefinitionReader
를 사용하는 것을 볼 수 있다.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);
}
}
}
ApplicationContext
타입으로 선언하면 getBeanDefinition()
을 사용할 수 없다.실제로 사용하는 입장에서는 BeanDefinition을 뽑아서 사용할 일은 거의 없다.
그래서ApplicationContext
에는getBeanDefinition()
같은 복잡한 메소드들이 정의되어 있지 않다.
출력결과
beanDefinitionName = appConfig, beanDefinition = Generic bean: class [hello.core.AppConfig$$EnhancerBySpringCGLIB$$d6ae189e]; scope=singleton; abstract=false; lazyInit=null; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null
beanDefinitionName = memberService, beanDefinition = Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=appConfig; factoryMethodName=memberService; initMethodName=null; destroyMethodName=(inferred); defined in hello.core.AppConfig
beanDefinitionName = orderService, beanDefinition = Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=appConfig; factoryMethodName=orderService; initMethodName=null; destroyMethodName=(inferred); defined in hello.core.AppConfig
beanDefinitionName = memberRepository, beanDefinition = Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=appConfig; factoryMethodName=memberRepository; initMethodName=null; destroyMethodName=(inferred); defined in hello.core.AppConfig
beanDefinitionName = discountPolicy, beanDefinition = Root bean: class [null]; scope=; abstract=false; lazyInit=null; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=appConfig; factoryMethodName=discountPolicy; initMethodName=null; destroyMethodName=(inferred); defined in hello.core.AppConfig
상당히 길다..
BeanClassName : 생성할 빈의 클래스 명 (자바 설정처럼 팩토리 역할의 빈을 사용하면 없음, 위 코드에서도 없다.)
factoryBeanName : 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
factoryMethodName : 빈을 생성할 팩토리 메서드 지정, 예) memberService
Scope : 싱글톤 (기본값)
lazyInit : 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연처리 하는지 여부
InitMethodName : 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
DestroyMethodName : 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
Constructor arguments, Properties : 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리 역할의
빈을 사용하면 없음)
이런 메타정보들을 기반으로 인스턴스를 생성한다.
"이런 정보들이 있구나" 정도로만 보고 넘기자.
BeanDefinition
을 직접 생성해서 스프링 컨테이너에 등록할 수도 있다.
-> 실무에서도 거의 없는 경우이다.
BeanDefinition
에 대해서는 스프링이 다양한 형태의 설정 정보를 BeanDefinition
으로 추상화해서 사용하는 것 정도만 이해하면 된다.
가끔 스프링 코드나 스프링 관련 오픈 소스의 코드를 볼 때, BeanDefinition
이라는 것이 보일 때가 있다.
-> 이때 위에서 설명한 메커니즘을 떠올리면 된다.
스프링 빈 등록 방식은 여러 가지가 있지만 크게 자주 쓰는 두 가지정도가 있다.
appConfig.xml
처럼 직접 스프링 빈을 스프링 컨테이너에 등록하는 방식
팩토리 메소드를 사용하는 방식
AppConfig.java
를 보면 memberService()
메소드를 호출해 스프링 빈으로 등록하기 때문에 팩토리 메소드를 사용한 방식이다.