김영한 강사님의 스프링 기본편 강의와 따로 찾아본 내용을 정리한 글입니다.
스프링 컨테이너는 싱글톤 컨테이너라고도 부릅니다.
여기서 말하는 싱글톤이란 컨테이너 내의 Bean을 모두 싱글톤, 즉 하나의 객체로 관리함을 의미합니다.
이번 시간에는 왜 싱글톤으로 관리되어야 하는지와 그 구현 방법에 대해 알아보도록 하겠습니다.
왜 싱글톤으로 관리되어야할까?
싱글톤이 아닌 상황을 가정해보겠습니다. 하나의 서버에 같은 서비스를 100명의 고객이 동시에 호출한다면 서비스(여기에서는 Bean)도 100개가 생성되어야 합니다. 이러한 상황은 각 유저마다 서비스 객체가 생성되어 메모리 사용에 비효율적이므로 권장되지 않습니다. 따라서 대부분의 서비스는 싱글톤 컨테이너로 관리합니다.
스프링이 적용되지 않은 서비스를 테스트 코드를 통해 확인해보겠습니다. 이 경우에는 스프링 컨테이너에서 service 를 꺼내오는 것이 아닌 AppConfig 클래스의 멤버 변수로서 꺼내졌습니다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
@Test
@DisplayName("스프링 없는 순수 DI 테스트")
void pureContainer() {
AppConfig appConfig = new AppConfig();
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
}
memberService 의 참조값이 다름을 알 수 있습니다. 호출될 때마다 return new MemberService() 가 실행됐기 때문에 당연한 일이죠. 그렇다면 싱글톤으로 관리하려면 어떻게 해야할까요? 먼저 퓨어한 자바 코드를 통해 구현해보도록 하겠습니다.
싱글톤 구현 - 순수 자바
public class SingletonService {
// 2
private static final SingletonService instance = new SingletonService();
// 3
public static SingletonService getInstance() {
return instance;
}
// 1
private SingletonService() {}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
- 생성자는 private 으로 닫아 외부에서 new 를 통해 객체가 생성되는 것을 막는다.
- private 생성자를 통해 클래스 내부에서 private static 를 통해 instance 를 딱 하나 생성한다.
- 외부에서 인스턴스를 조회할 수 있도록 getInstance() 메소드 작성
private static final 의 의미
먼저 final 은 한번 초기화되면 값이 변경 불가능하다. static 은 모든 클래스가 단 하나의 값을 공유함을 의미합니다. 따라서 heap 메모리가 아닌 static 메모리 영역에 할당되어 모든 클래스가 하나의 값을 공유합니다.
위의 코드에서는 아무리 SingletonService 클래스가 여러개 존재해도 new SingletonService() 그 자체로 생성되어 할당된 변수 instance 는 단 하나라는 뜻입니다. 테스트 코드를 통해 확인하면 다음과 같습니다.
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest() {
SingletonService instance1 = SingletonService.getInstance();
SingletonService instance2 = SingletonService.getInstance();
System.out.println("instance1 = " + instance1);
System.out.println("instance2 = " + instance2);
Assertions.assertThat(instance1).isSameAs(instance2);
}
지금까지 싱글톤 컨테이너에 대해 알아보았습니다. 코드 구현이 복잡한 문제를 간단하게 해결해주기 위하여 스프링이 이제서야 등장합니다.
싱글톤 구현 - 스프링
@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);
Assertions.assertThat(memberService1).isSameAs(memberService2);
}
이전과는 달리 ApplicationContext (스프링 컨테이너) 에서 getBean() 메소드를 통해 꺼냈습니다. 테스트 코드를 통해 확인해보니 참조값이 같습니다. 즉, 스프링 컨테이너 속에서는 MemberService 객체가 단 하나 생성됩니다. 이것이 어떻게 가능한지 더 자세히 살펴보도록 하겠습니다.
@Configuration 의 비밀
스프링 컨테이너의 빈은 모두 싱글톤이 되도록 보장되어야 합니다. 이것이 어떻게 가능한지 살펴보도록 하겠습니다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
이 코드에서 AnnotaionConfigApplicationContext의 파라미터로 넘겨진 값은 스프링 빈으로 등록됩니다. 이때 AppConfig가 Bean 으로 먼저 등록된 후 다른 Bean 이 Singleton 으로 등록되는 것이다. 하지만 여기서 유의점은 등록된 AppConfig 빈의 이름이다.
❗hello.core.AppConfig 가 아니라 hello.core.AppConfig$$EnhancerBySpringCGLIB 왜 그럴까?
실제로 등록되는 것이 AppConfig 가 아니라 AppConfig$$EnhancerBySpringCGLIB 이기 때문이다. 이것은 AppConfig 를 상속받아 만들어지고 실제로 Bean 등록이 되는 것이 CGLIB이다!
이는 @Configuration 때문에 생긴 일인데 이 특수한 CGLIB AppConfig 는 내부적으로 등록된 빈은 또 다시 등록하지 않도록 설계되어있다.
다르게 말하면 @Configuration 이 없다면 빈이 호출될 때마다 여러번 생성되는 위험성이 있다!
이제는 어떠한 클라이언트도 모두 같은 MemberService 객체를 참조할 수 있습니다.
❗싱글톤 컨테이너에서 조심할 점
클라이언트 A, B, C 가 동시에 MemberService 를 이용합니다. 하나의 서버라면 3개의 쓰레드가 생성되어 동시에 같은 메모리를 참조하게 됩니다. 이때 만약에 공유되는 변수가 있고 그것이 stateful 하다면?
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가 order() 메소드를 통해 price 를 1000으로 바꿨고 쓰레드 B가 2000으로 바꿨다. 이때 쓰레드 A가 getPrice() 메소드로 본인의 price를 확인한다면? 원래는 1000이 나와야하지만 B의 가격인 2000이 나와버린다.
이처럼 싱글톤 서비스는 절대로 stateful 한 변수를 만들면 안된다.
'☘️Spring' 카테고리의 다른 글
[Spring] JPA N+1 문제 해결 (35) | 2023.10.30 |
---|---|
[Spring] MockMvcTest vs End-to-End Tests (1) | 2023.10.26 |
[Spring] web mvc 코드로 이해하기 (0) | 2023.09.02 |
[Spring] 의존관계 자동/수동 주입 (0) | 2023.07.09 |
[Spring] 스프링 컨테이너 개념 정리 (0) | 2023.07.04 |