본 포스팅은 김영한 님의 스프링 핵심원리 기본편 강의 섹션 5를 듣고 요약한 내용입니다.
1. 싱글톤 컨테이너
스프링 컨테이너는 스프링 빈을 이용하여 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리합니다.
싱글톤 패턴과 문제점은 아래 포스팅을 참고하세요.
스프링 컨테이너는 싱글턴 패턴을 따로 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리합니다. (싱글톤 컨테이너 역할을 합니다.)
따라서, 스프링 컨테이너는 싱글턴 패턴을 위한 지저분한 코드가 들어가지 않아도 됩니다. 또한, DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있습니다.
스프링 컨테이너를 사용하는 테스트 코드를 작성해 보겠습니다.
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void SpringContatiner() {
//스프링빈 이용
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
//1. 조회: 호출할 때 마다 같은 객체를 반환
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
//2. 참고값이 같은 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
//memberService1 == memberService2
assertThat(memberService1).isSameAs(memberService2);
}
기존의 AppConfig에서 스프링빈을 이용했고, getBean() 메서드를 이용하여 객체를 반환했습니다. 자 과연 두 객체가 같을까요?
두 개가 같은 객체를 공유하고 있는 것을 알 수 있습니다.
스프링 컨테이너 덕분에 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 재사용하고 있다는 것을 알 수 있습니다.
2. 싱글톤 컨테이너의 문제점
이제 싱글톤 컨테이너의 문제점에 대해서 알아보겠습니다. 자원을 공유한다는 점에서 이해가 감이 오신 분도 있을 것입니다. 쉬운 이해를 위해서 새로운 예제를 만들어봅시다. 아래와 같이 private 변수로 price를 지정하고, order()메서드에서 price를 지정하면 private메서드에서 price를 지정하도록 만들었습니다.
package hello.core.singleton;
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;
}
}
이제 테스트 코드를 작성하겠습니다. 스프링 빈을 이용하여, A 사용자에는 10000원을 주문하게 하고 B 사용자에는 20000원을 주문하게 했습니다.
package hello.core.singleton;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
public class StatefulServiceTest {
@Test
void statefulServiceSingleton(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
//ThreadA: A 사용자 10000원 주문(price =10000)
statefulService1.order("userA", 10000);
//ThreadB: B 사용자 20000원 주문(price =20000)
statefulService1.order("userB", 20000);
//ThreadA: A 사용자 주문 금액 조회
int price = statefulService1.getPrice();
//ThreadA: A 사용자 10000원을 기대했지만, 20000원 출력
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig{
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
A 사용자의 주문 금액을 보려고 했으나, 결과는 어떻게 될까요?
A 사용자의 주문 금액은 10000임에도 스프링 빈에서 공유 자원을 사용하기 때문에 B 사용자의 주문 금액 20000으로 덮어진 것을 확인할 수 있습니다.
이것을 해결하기 위해선 기존의 Service 코드의 변경이 필요합니다.
package hello.core.singleton;
public class StatefulService {
public int order(String name, int price) {
System.out.println("name = " + name + "price =" + price);
return price;
}
}
기존의 private 변수를 없애고, order가 들어오면 바로 return을 해주는 겁니다. 이러면 스프링 빈의 자원은 공유하지만, private 변수의 값이 변경되지 않으니 문제상황을 해결할 수 있습니다. 자 다시 테스트를 통해 결과를 확인해봅시다.
package hello.core.singleton;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
public class StatefulServiceTest {
@Test
void statefulServiceSingleton(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
//ThreadA: A 사용자 10000원 주문(price =10000)
int priceA = statefulService1.order("userA", 10000);
//ThreadB: B 사용자 20000원 주문(price =20000)
int priceB = statefulService1.order("userB", 20000);
//ThreadA: A 사용자 10000원 출력
System.out.println("priceA = " + priceA);
//ThreadB: A 사용자 10000원 출력
System.out.println("priceB = " + priceB);
Assertions.assertThat(priceA).isEqualTo(10000);
}
static class TestConfig{
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
A 사용자와 B 사용자 모두 잘 나오는 것을 확인할 수 있습니다.
결론
스프링 컨테이너는 스프링 빈을 이용하여 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리합니다.
공유필드는 문제가 일어날 수 있으므로 주의해서 스프링 빈은 항상 무상태(Stateless)로 설계해야 합니다.