스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다. 여기서 스프링 빈이 싱글톤으로 관리되는 빈이다.
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
assertThat(memberService1).isSameAs(memberService2);
}
참고!
스프링의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아님!
요청할 때마다 새로운 객체를 생성해서 반환하는 기능도 제공하고 있다..!🧐
이는 빈 스코프에서 살펴보자
🚨 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다.
public class StatefulService {
private int price; // 상태를 유지하는 필드
public void order(String name, int price) {
System.out.println("name = " + name + "price =" + price);
this.price = price;
}
public int getPrice() {
return price;
}
}
static class TestConfig{
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
class StatefulServiceTest {
@Test
void statefulServiceSingleton(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// ThreadA : A 사용자가 10000원 주문
statefulService1.order("userA", 10000);
// ThreadB : B 사용자가 20000원 주문
statefulService2.order("userB", 20000);
// 사용자 A 주문 금액 조회
int price = statefulService1.getPrice();
System.out.println("price = " + price);
assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
}
StatefulService
의 price
필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경한다.🚨 공유 필드는 매우 조심해야 한다! 스프링 빈은 항상 무상태로 설계해야 한다! 🚨
public class StatefulService {
public int order(String name, int price) {
System.out.println("name = " + name + "price =" + price);
return price;
}
}
class StatefulServiceTest {
@Test
void statefulServiceSingleton(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// ThreadA : A 사용자가 10000원 주문
int userAPrice = statefulService1.order("userA", 10000);
// ThreadB : B 사용자가 20000원 주문
int userBPrice = statefulService2.order("userB", 10000);
// 사용자 A 주문 금액 조회
// int price = statefulService1.getPrice();
System.out.println("price = " + userAPrice);
assertThat(userAPrice).isEqualTo(10000);
}
@Configuration
public class AppConfig {
// @Bean memberService -> new MemoryMemberRepository()
// @Bean orderService -> new MemoryMemberRepository()
// MemoryMemberRepository 가 2번 호출되면서 싱글톤이 깨졌다고 볼 수 있을까?
@Bean
public MemberService memberService(){
return new MemberServiceImp(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){
return new OrderServiceImp(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
}
memberService
빈을 만드는 코드를 보면 memberRepository()
를 호출한다.MemoryMemberRepository()
를 호출한다.orderService
빈을 만드는 코드 또한 memberRepository()
를 호출한다.MemoryMemberRepository()
를 호출한다.즉, 다른 2개의 MemoryMemberRepository가 생성되면서 싱글톤이 깨지는 것처럼 보인다.
/* 테스트 용도를 위해 각 구현체에 다음과 같은 함수를 추가해준다 */
/* MemberServiceImp & OrderServiceImp */
public MemberRepository getMemberRepository() {
return memberRepository;
}
/* ConfigurationSingletonTest */
public class ConfigurationSingletonTest {
@Test
void configurationTest() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberServiceImp memberService = ac.getBean("memberService", MemberServiceImp.class);
OrderServiceImp orderService = ac.getBean("orderService", OrderServiceImp.class);
MemberRepository memberRepository1 = memberService.getMemberRepository();
MemberRepository memberRepository2 = orderService.getMemberRepository();
System.out.println("memberService -> memberRepository1 = " + memberRepository1);
System.out.println("orderService -> memberRepository2 = " + memberRepository2);
assertThat(memberService.getMemberRepository()).isEqualTo(memberRepository);
assertThat(orderService.getMemberRepository()).isEqualTo(memberRepository);
}
}
예상과 달리, 두 객체와 더불어 실제 빈, 세 가지 모두 같은 것을 가리키는 것을 확인할 수 있다.
new MemoryMemberRepository
호출해서 다른 인스턴스가 생성되어야 하는데? 어떻게 된 일일지 세세하게 알아보자.memberRepository
가 3번이 아닌 1번 호출된 것을 확인할 수 있다.@Configuration
을 적용한 AppConfig
를 살펴보면 알 수 있다.@Test
void configurationDeep(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// AppConfig도 스프링 빈으로 등록된다.
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean);
}
AnnotationConfigApplicationContext
에 파라미터로 넘긴 값은 스프링 빈으로 등록된다. 따라서 AppConfig
도 스프링 빈으로 등록된다.
AppConfig
스프링 빈을 조회해서 클래스 정보를 출력해보면..🔎
위와 같은 결과가 출력된다. 원래 순수한 클래스라면 class hello.core.AppConfig
가 출력되어야 하는데 클래스 명 뒤에 xxxCGLIB가 붙으면서 복잡해진 결과가 출력되는 것을 확인할 수 있다.
@Bean
public MemberRepository memberRepository(){
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else {
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
AppConfig@CGLIB는 AppConfig의 자식타입이므로, AppConfig 타입으로 조회할 수 있다!
@Configuration 생략하게 된다면?
다음과 같은 결과가 출력되며, MemberRepository가 총 3번 호출되고, 서로 다른 인스턴스를 참조하고 있는 것을 확인할 수 있다.
@Bean
만 사용하면 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
memberRepository()
처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.🚨 스프링 설정 정보는 항상
@Configuration
을 사용하자!