public class SingletonTest {
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer(){
AppConfig appConfig = new AppConfig();
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
}
}
이때 test는 sout으로 눈으로 디버깅 하는 것으 좋으나 항상 assetions 객체를 이용하여 자동화 되도록 해주어야한다.
즉, 트래픽이 초당 100이 나오면 초당 100개의 객체가 생성되고 소멸되는데 이는 메모리 소모가 정말 심하다.
그래서 우리는 하나의 객체 인스턴스를 생성하여 공유하여 쓰면 된다. 이게 싱글톤 패턴의 개념이다.
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance(){
return instance;
}
private SingletonService() {}
public void logic() {
System.out.println("싱클톤 객체 로직 호출");
}
}
getInstance()
메서드를 통해서만 조회할 수 있다.정말 잘 설계한 객체는 컴파일 오류만으로 웬만한 오류 잡을 수 있다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(getMemberRepository());
}
@Bean
public static MemoryMemberRepository getMemberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(getMemberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
// return new FixDiscountPolicy();
}
}
이 자바 AppConfig(spring container)를 다시 다 싱글톤으로 바꿔줘야겠다고 생각했다면
괜찮다! 왜냐하면 Spring Container를 사용하면 spring container가 다 알아서 객체를 싱글톤으로 관리해준다.
그렇다면 문제가 없냐? 그건 아니다.
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
Assertions.assertThat(memberService1).isSameAs(memberService2);
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
}
즉, 스프링 컨테이너가 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유함으로써 효율적으로 재사용할 수 있다.
참고로 스프링의 기본 빈 등록 방식은 싱글톤이긴 한데(99%) 항상 싱글톤은 아니다(1%).
요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다.
(예를들어 req할 때 새로 꺼내쓴다거나, http req 라이프 사이클에 맞춰 빈 라이프 사이클을 맞추거나, 고객이 들어올 때 만들어서 고객이 나갈 때 삭제하는 이런걸 스코프라고 하는데 나중에 알아보자.)
싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안 된다.
@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 10000원 주문
statefulService2.order("userB", 20000);
// ThreadA : 사용자 A 가 주문 금액 조회
int price = statefulService1.getPrice();
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
// 결과 price = 20000
공유필드는 항상 경계하고 무상태(stateless)로 설계해야한다.
생성한 클래스를 간단하게 테스트 코드 만드는 단축키
mac : cmd+shift+T
, window : Ctrl+shift+T