- 스프링은 기업용 온라인 서비스 기술을 지원하기 위해 탄생
- 대부분의 스프링 애플리케이션은 웹
- 웹 애플리케이션은 보통 여러 고객이 동시에 요청한다
사용자가 요청할 때마다 필요한 객체를 생성한다면
- 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다 → 메모리 낭비가 심하다
- 해당 객체를 1개만 생성하고 공유하도록 설계하면 해결된다 - > 싱글톤 패턴
- 클래스의 인스턴스가 1개만 생성되는 것을 보장하는 디자인 패턴
- 객체 인스턴스가 2개 이상 생성되지 않도록 막아야함
- private 생성자를 사용하여 외부에서 new 키워드를 사용 못하게 막음
- static 영역에 객체 instance를 미리 하나 생성
- 이 객체 인스턴스에 접근하기 위해서 오로지 getInstance() 메서드를 통해서만 조회할 수 있도록 설계
public class SingletonService{
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance(){
return instance;
}
private SingletonService(){};
}
싱글톤 패턴 문제점
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다
- 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP 위반
- 클라이언트가 구체 클래스에 의존해서 OCP원칙을 위반한다.
- 테스트하기 어렵다
- 내부 속성을 변경하거나 초기화 하기 어렵다
- prvate 생성자로 자식 클래스를 만들기 어렵다
-
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리
-
스프링 빈이 싱글톤으로 관리된다.
-
스프링 컨테이너는 싱글톤 컨테이너의 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다.
-
스프링 컨테이너의 이러한 기능 덕분에 개발자는 싱글톤을 구현하지 않아도, DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.
- 스프링의 기본 빈 등록 방식은 싱글톤이지만, 다른 방법도 지원한다(요청할 때 마다 새로운 객체 생성해서 반환하는 것도 지원)
- 싱글톤 객체는 상태를 유지하게 설계하면 안된다
- 특정 클라이언트에 의존적인 필드가 있으면 안된다.
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
- 가급적 읽기만 가능해야 한다.
- 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, 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;
}
}
public 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);
//ThreadA: 사용자A 주문 금액 조회
int price = statefulService1.getPrice();
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig{
@Bean
public StatefulService statefulService(){
return new StatefulService();
}
}
}
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy(){
return new FixDiscountPolicy();
}
}
- 위의 코드에서 memberService()와 memberRepository(), orderService()를 등록하는 과정에서 new MemoryMemberRepository()를 총 세 번 실행할 것 같은데 이는 싱글톤이 깨지는 것 아닐까?
@Test
void configurationTest(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
MemberRepository memberRepository1 = memberService.getMemberRepository();
MemberRepository memberRepository2 = memberService.getMemberRepository();
System.out.println("memberService -> memberRepository = " + memberRepository1);
System.out.println("orderService -> memberRepository = " + memberRepository2);
System.out.println("memberRepository = " + memberRepository);
Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
- 결과는 셋 다 같은 인스턴스를 반환한다.
- 스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해줘야 한다.
- 위의 자바코드를 보면 getMemberRepository()가 3번 호출되어야 한다.
- AppConfig에 적용시킨 @Configuration 어노테이션이 무언가를 했다.
@Test
void configurationDeep(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
//출력 : bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$13481f13
}
- AppConfig 빈을 출력한 결과 순수한 클래스가 아닌 다른 클래스가 출력되었다.
- 이렇게 어노테이션을 적용한 클래스를 CGLIB라는 바이트코드 조작 라이브러리를 활용하여 싱글톤을 보장해준다.
- @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 존재하지 않으면 생성해서 등록한다.
- 만약 AppConfig에 @Configuration이 아닌 그냥 @Bean을 붙여 사용한다면?
- memberRepository가 3번 호출되어 싱글톤을 보장하지 못한다.