Java
[Spring] 싱글톤 컨테이너란?
keep it simple
2023. 3. 13. 04:46
싱글톤 컨테이너란?
- 싱글톤 컨테이너는 싱글톤 패턴의 문제점을 해결하면서 효율적으로 인스턴스를 공유하며 효율적으로 사용할 수 있게 도와준다.
- 기본적으로 스프링 빈들은 싱글톤으로 관리되는 빈이다. 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 컨테이너는 하나의 인스턴스만 생성하고 관리한다. 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 싱글톤을 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
- 이 기능을 사용함으로써 싱글톤 패턴을 위한 지저분한 코드를 작성할 필요도 없고 DIP,OCP,테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.
싱글톤 테스트를 해보자!
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(getMemberRepository());
}
}
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer(){
ApplicationContext 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);
}
테스트 결과
memberService1 = test.core.member.MemberServiceImpl@c35172e
memberService2 = test.core.member.MemberServiceImpl@c35172e
- 위와같이 memberService1 과 memberservice2 는 같은 참조값이다. 그러므로 인스턴스를 같이 공유하고 싱글톤 패턴으로 효율적으로 사용하는걸 볼 수 있다. 실질적으로 @Configuration 을 사용해서 스프링에서 관리하는 설정파일이라고 지정을하고 @Bean 을 사용해서 해당 메소드를 빈으로 등록해서 스프링 컨테이너가 알아서 관리한다.
- @Configuration 어노테이션에 안에 들어가면 proxyBeanMethods() 라는 메소드가 존재한다. 이 메소드는 GCLIB(런타임에 동적으로 자바 클래스의 프록시를 생성)를 사용해 프록시(임의로 만든 클래스)로 감싸준 후 스프링 빈으로 등록, 그래서 그 빈을 여러번 호출해도 싱글톤으로 재사용되고 공유하게끔 보장해주는 메소드이다. 간단하게 말하면 싱글톤 여부를 제어하기 위한 함수이다. 그래서 리턴값은 Boolean으로 되어있다. @Bean 만 사용해서는 빈으로 등록은 되지만 싱글톤이 보장이안된다. 그러므로 항상 빈으로 등록하는 경우는 @Configuration 어노테이션을 달아준후 싱글톤을 보장받아야한다.
싱글톤은 좋은 기술이지만 주의해야한다.
- 하나의 같은 인스턴스를 공유하기 떄문에 항상 항상 상태를 무상태(stateless)로 설계해야한다.
- 클라이언트한테 의존적인 필드가 있으면 안된다, 즉 값을 변경할 수 없어야한다.
- 가급적 읽기만 가능하게 설계해야한다.
- 필드 대신에 자바에서 공유되지 않은 local variable, parameter, ThreadLocal 등을 사용해야한다.
- 스프링 빈의 필드에 공유값을 설정하면 큰 장애를 발생 시킬 수 있다.
잘못된 싱글톤 사용법
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;
}
}
@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);
// ThreadA: 사용자A 주문 금액 조회
int price = statefulService1.getPrice();
System.out.println("price = " + price);
}
static class TestConfig {
@Bean
public StatefulService statefulService(){
return new StatefulService();
}
}
올바른 싱글톤 사용법
public class StatefulService {
private int price; // 상태를 유지하는 필드
public int order(String name, int price){
return price;
}
}
@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", 20000);
// ThreadA: 사용자A 주문 금액 조회
System.out.println("price = " + userAPrice);
}
- TestConfig에 생성한 StatefulService를 빈으로 등록해 테스트를 해본결과, 마지막에 사용자 A의 주문 금액을 조회했을때 20000원이 출력된다. 싱글톤으로 객체가 공유되면서 price(공유필드) 값이 중간에 B사용자의 주문금액로 변경되었다. 이럴때는 큰 문제가 생긴다. 해결 방안은 this.price = price 를 없애서 변경할 수 있지 못하게 변경하고 읽기만 하게끔 변경한후 호출하는 곳에서 지역 변수를 사용해서 출력해주면, 다시 A 주문 금액을 마지막으로 조회했을때 정상적으로 값 10000원이 출력된다.
결론
- 싱글톤에 대해 더 깊게 배웠다. @Bean으로 빈을 등록할 수 있다. 하지만 싱글톤을 보장받아야하는 경우 꼭 @Configuration을 사용하자. 그래야 싱글톤으로 인스턴스를 공유하며 사용할 수 있다. 항상 설계를 할 때 무상태로 설계를 하며 가급적 읽기용으로 사용을 해야함을 생각해야한다. 그래야 데이터가 변경됨으로써 바뀔 수 있는 크리티컬한 문제를 방지할 수 있다.
reference
- 김영한님의 인프런강의[스프링 핵심원리 기본]