주 내용은 자바위주로 정리가 되어있습니다.
클래스의 인스턴스가 딱 1개만 생성하는 디자인 패턴.
public class Test {
void Test() {
AppConfig appConfig = new AppConfig();
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
}
}
위 코드처럼 서비스 요청이 있을 때마다 객체가 생성된다고 하자.
요청 100개가 있으면 객체를 100번 생성해야하는 메모리의 낭비가 발생하는데 이를 방지함.
여러 방법이 있으나 가장 대표적인 holder 방법으로 구현한다.
public class ExampleClass {
// 외부에서 생성자에 접근하지 못하게.
private ExampleClass() {}
//
private static class InnerClass() {
private static final ExampleClass instance = new ExampleClass();
}
public static ExampleClass getInstance() {
return InnerClass.instance;
}
}
스프링에서는 이러한 싱글톤의 단점들을 해결하면서 장점을 취할 수 있는 스프링 컨테이너를 제공한다.
싱글톤 패턴이든, 스프링과 같은 싱글톤 컨테이너를 사용하든, 하나의 인스턴스를 공유하기 때문에 상태를 유지(stateful)하지 않고 무상태(stateless)로 설계해야 한다.
문제 코드(공유필드가 있을 경우)
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;
}
}
class StatefulServiceTest {
@Test
void statefulServiceSingleton(){
AnnotationConfigApplicationContext 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);
int price = statefulService1.getPrice();
System.out.println("priceA = " + price);
}
static class TestConfig {
@Bean
public StatefulService statefulService(){
return new StatefulService();
}
}
}
priceA = 20000이 출력되는 에러가 발생.
개선 코드
public class StatefulService {
// private int price; // 상태를 유지하는 필드
public int order(String name, int price){
System.out.println("name = " + name + " price = " + price);
return price;
}
}
class StatefulServiceTest {
@Test
void statefulServiceSingleton(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
int userAPrice = statefulService1.order("userA", 10000);
int userBPrice = statefulService1.order("userB", 20000);
Assertions.assertThat(userAPrice).isEqualTo(10000);
Assertions.assertThat(userBPrice).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService(){
return new StatefulService();
}
}
}
스프링 패턴을 적용하지 않아도, 인스턴스를 싱글톤으로 관리해준다.
@Configuration
public class AppConfig {
@Bean
public MemoryMemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();
}
// new DiscountPolicy가 중복되지 않게
@Bean
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
@Bean
public MemberService memberService(){
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService(){
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
}
AppConfig.class라는 빈 관련 설정파일에서 sout를 통해 알아보자. 위 코드 상으로는 memberRepository()가 3번 호출되므로 System.out.println("call AppConfig.memberRepository");도 3번 실행되지만 결과는?
call AppConfig.memberRepository
23:08:07.772 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'discountPolicy'
23:08:07.776 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberService'
call AppConfig.memberService
23:08:07.779 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderService'
call AppConfig.orderService
이렇게 1번만 호출되는 것을 확인할 수가 있는데 이것이 가능한 이유는 CGLIB라는 바이트코드 조작 라이브러리 덕분이다.
스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다. 그런데 스프링이 자바 코드까지 어떻게 하기는 어렵다. 위 자바 코드를 보면 분명 3번 호출되어야 하는 것이 맞다. 그래서 스프링은 클래스의 바이트코드를 조작하는 CGLIB라는 라이브러리를 사용한다.
@Test
void configurationDeep(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println(bean.getClass());
}
위 코드를 실행하면 class com.example.basic.AppConfig335108b9라고 뜬다.
순수한 클래스라면 class hello.core.AppConfig라고 떠야하는데 클래스 명에 xxxCGLIB가 붙으면서 상당히 복잡해진 것을 볼 수 있다. 이것은 내가 만든 클래스가 아니라 스프링이 CGLIB를 이용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다.
AppConfig@CGLIB 예상 코드
중요한것은 @Configuration 어노테이션을 사용해야만 CGLIB가 작동해서 싱글톤을 보장한다는것! @Bean만 사용해도 스프링빈으로 등록은 된다 하지만, 싱글톤을 보장하지 않고 생성되는 인스턴스도 모두 다르다.
출처 : 김영한의 스프링 핵심 원리 -기본편