인프런 김영한님의 스프링 강의를 듣고 정리한 내용입니다. 출처
ApplicationContext applicationContext =
new AnnotationConfigApplicationContext(AppConfig.class);
ApplicationContext
를 스프링 컨테이너라고 한다.ApplicationContext
는 인터페이스이다.@Bean(name="memberService2")
public 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 findBeanByName2() {
MemberService memberService = ac.getBean("memberService", MemberServiceImpl.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("빈 이름으로 조회 X")
void findBeanByNameX() {
assertThrows(NoSuchBeanDefinitionException.class,
() -> ac.getBean("xxxx", MemberService.class));
}
@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); }
}
}
}
스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회 방법
ac.getBean(빈이름, 타입)
ac.getBean(타입)
조회대상 스프링 빈이 없으면 예외 발생, NoSuchBeanDefinitionException
public class ApplicationContextSameBeanFindTest {
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();
}
}
}
타입으로 조회시 같은 스프링 빈이 둘 이상이면 오류가 발생한다. 이때는 빈 이름을 지정하자.
ac.getBeansOfType()
을 사용하면 해당 타입의 모든 빈을 조회 할 수 있다.
public class ApplicationContextExtendsFindTest {
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("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 findAllBeanByParentType() {
Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
assertThat(beansOfType.size()).isEqualTo(2);
}
@Configuration
static class TestConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
}
부모타입으로 조회하면, 자식타입도 함께 조회한다.
모든 자바 객체의 최고 부모인 Object
타입으로 조회하면 모든 스프링 빈을 조회한다.
getBean()
을 제공한다.BeanFactory를 직접 사용할 일은 거의 없고 부가기능이 포함된 ApplicationContext를 사용한다.
BeanFactory나 ApplicationContext를 스프링 컨테이너라고 한다.
지금까지 했던 것들이 애노테이션 기반임.
최근에는 스프링 부트를 많이 사용하면서 XML기반 설정은 잘 사용하지 않는다.
하지만 많은 레거시 프로젝트들이 XML로 되어있고 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.MemberRepository"/>
<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>
public class XmlAppContext {
@Test
void xmlAppContext() {
ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
}
스프링이 이렇게 다양한 설정 형식을 지원하는 것의 중심에는 BeanDefinition
이라는 추상화가 있다.
BeanDefinition
을 빈 설정 메타정보라고 하고, @Bean
, <bean>
당 각각 하나씩 메타 정보가 생성된다.
스프링 컨테이너는 이 메타 정보를 기반으로 스프링 빈을 생성한다.
public class SingleTonTest {
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer(){
AppConfig appConfig = new AppConfig();
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
assertThat(memberService1).isNotSameAs(memberService2);
}
}
대부분의 스프링 애플리케이션은 웹 애플리케이션이고, 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다.
위 테스트코드를 통해 알 수 있듯이 전 글에서 만들었던 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다.
이렇게 된다면 만약 고객 트래픽이 초당 100이 나오면 초당 100개의 객체가 생성되고 소멸되는데 메모리 낭비가 너무 심하다. 따라서 이 문제를 해결하기위해 해당 객체가 딱 1개만 생성되고 공유되도록 설계하는 싱글톤 패턴을 이용한다.
public class SingleTonService {
private static final SingleTonService instance = new SingleTonService();
public static SingleTonService getInstance() {
return instance;
}
private SingleTonService(){
}
}
static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.
이 객체 인스턴스가 필요하면 오직 getInstance()
메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.
딱 1개의 객체 인스턴스만 존재해야 하므로 생성자를 private으로 막아서 혹시라도 외부에서 객체 인스턴스가 생성되는 것을 막는다.
싱글톤 패턴의 문제점
결론적으로 유연성이 떨어지고 안티패턴으로 불리기도 한다.
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리한다.
객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다. 무상태(stateless)로 설계해야 한다.
@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 RateDiscountPolicy();
}
}
AppConfig 코드에서 memberService와 orderService 빈을 만드는 코드를 보면 memberRepository()
를 호출하고 new MemoryMemberRepository()
를 호출한다.
결과적으로 각각 다른 2개의 MemoryMemberRepository
가 생성되면서 싱글톤이 깨지는 것처럼 보인다.
하지만 테스트를 해보면 그렇지 않다. 스프링 컨테이너는 이것을 하나의 객체로 유지되게 해준다.
스프링은 클래스의 바이트 코드를 조작하는 라이브러리를 사용해서 스프링 빈이 싱글톤이 되도록 보장해준다.
비밀은 @Configuration
을 적용한 AppConfig
에 있다.
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass())
AppConfig 스프링 빈을 조회해서 클래스 정보를 출력해보면
class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
가 나온다.
그림처럼 @Configuration
을 적용한 AppConfig
클래스는 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다.
그 임의의 다른 클래스가 @Bean
이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드를 동적으로 만들어줘서 싱글톤이 보장되도록 해준다.
@Configuration
을 적용하지 않고 @Bean
만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지는 않는다.