클래스의 인스턴스가 딱 하나만 생성되는걸 보장하는 디자인 패턴이다. 즉, 인스턴스가 2개이상 생성되지않게 막아야한다. 이에 대한 막는 방법이 있어야겠지요?? 그 방법이 뭔지 알아보자~~
서비스를 운영할 때 위와같이 동일한 memberService
를 각각 한번씩 요청받는다면 총 세개의 memberSerivce
를 생성해야한다. 만약 요청을 전부 메모리에 할당한다면 메모리공간이 부족할 수 있다는 문제점이 발생한다.
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContainer(){
Appconfig appconfig = new Appconfig();
MemberService memberService1 = appconfig.memberService();
MemberService memberService2 = appconfig.memberService();
MemberService memberService3 = appconfig.memberService();
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
System.out.println("memberService3 = " + memberService3);
assertThat(memberService1).isNotEqualTo(memberService2);
assertThat(memberService1).isNotEqualTo(memberService3);
assertThat(memberService2).isNotEqualTo(memberService3);
}
서로다른 memberService가 생성되었고 만약 이게 수십만건 수백만건이 한꺼번에 요청이 온다면? 메모리공간이 부족해지는 문제가 발생할수있다.
위와같이 동일한 요청들을 하나의 객체로 처리하기위한 디자인패턴이 싱글톤패턴이다.
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance(){
return instance;
}
private SingletonService(){
//private 생성자로 외부에서 객체생성을 방지함
}
public void login(){
System.out.println("싱글톤 객체 로직 호출");
}
}
우선 가장 중요한건 싱글톤 이니까 프로그램 내에서 단 하나의 인스턴스로만 사용해야한다는 것이다. 외부에서 객체를 생성하지 못하도록 방지를 해야하는데 이를 위해 private
를 사용했다. 또한 하나만 존재하기위해 static
을 달아두어 클래스변수로 선언한다.
즉, 내부에서도 instance
라는 객체는 하나만 존재하게되었고 외부에서 new
생성이 되지않는다.
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest(){
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
SingletonService singletonService3 = SingletonService.getInstance();
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
System.out.println("singletonService3 = " + singletonService3);
assertThat(singletonService1).isSameAs(singletonService2);
assertThat(singletonService1).isSameAs(singletonService3);
assertThat(singletonService2).isSameAs(singletonService3);
}
이를 테스트해보기위해 코드를 작생해보면?
아까랑 다르게 셋이 똑같죠?
근데? 단점이 존재한다. 그 단점들을 나열해보자면..
ㅋㅋ 이쯤되면 버려야하는거아님? 할수있지만? 스프링에서는 기존의 싱글톤패턴의 문제점을 해결해준다..!
빈 등록하는 방법을 예에에전에 포스팅했었는데(조만간 비슷한걸로 다시하나 올릴것) 그 빈을 등록하는 스프링 컨텍스트가 바로? 스프링 빈이며 싱글톤을 지원해주는 컨테이너이다!
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer(){
//Appconfig appconfig = new Appconfig();
ApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
MemberService memberService3 = ac.getBean("memberService", MemberService.class);
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
System.out.println("memberService3 = " + memberService3);
assertThat(memberService1).isSameAs(memberService2);
assertThat(memberService1).isSameAs(memberService3);
assertThat(memberService2).isSameAs(memberService3);
}
위의 singletonServiceTest
는 내가 직접 구현한 SingletonService
를 통해서 받아야했지만? springContainer
는 AnnotationConfigApplicationContext
를 이용해서 객체(빈)를 할당받고 알아서 싱글톤패턴을 적용시켜준다 라는것이다~ 편리하죠?
라고 하고 끝나면 얼마나 좋겠지만? 싱글톤을 사용할 때 큰 주의점이 있다!
라고 하는데 그냥 보면 모르니까 우선 코드로 상황이해부터 해보자!
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 : 클라이언트 주문 금액 조회
int priceA = statefulService1.getPrice();
int priceB = statefulService2.getPrice();
System.out.println("A price = " + priceA);
System.out.println("B price = " + priceB);
assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
StatefulService
빈을 두개 등록한다음? 두 빈이 각각 주문을 하고 주문금액을 조회하면 A클라이언트는 만원 B클라이언트는 2만원이 나와야겠지만?
공유필드의 멤버변수인 price
가 상태(state)를 가지고있기때문에 동시에 여러 클라이언트에서 사용될 때 상태의 일관성이 깨질 수 있다는 것이다. 즉 price
의 분리나 price의 값을 건드리지 않도록 수정해야한다는 것!
public class StatefulService {
private int price;
public int order(String name, int price){
System.out.println("name = " + name + "price = " + price);
this.price = price;//여기에 문제가 발생할것
return price;
}
public int getPrice(){
return price;
}
}
order
를 void->int형으로 바꾸어서 리턴값을 쥐어줌
@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 userA_Price = statefulService1.order("userA", 10000);
//ThreadB : B클라이언트 : 20000원 주문
int userB_Price = statefulService2.order("userB", 20000);
//ThreadA : 클라이언트 주문 금액 조회
System.out.println("A price = " + userA_Price);
System.out.println("B price = " + userB_Price);
assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
order
에 리턴값이 생겼기때문에 Price를 각각 할당해보고 확인하면?
물론 이게 고치는 최선의 방법은 아닌데 이런식으로 문제가 발생하였고 상태를 해결하기위해 코드에서 고쳐야한다! 를 보셔야합니다 😅
중요한건 스프링빈은 상태를 가져선 안된다!
스프링에서는 @Configuration
기능을 사용하여 싱글톤을 자동 지원해줄 수 있다. 바로 코드부터 보시죠
@Configuration
public class Appconfig {
@Bean
public MemberService memberService() {
System.out.println("Appconfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
System.out.println("Appconfig.memberRepository");
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
System.out.println("Appconfig.orderService");
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
}
우리가 이전에 설정해둔 Appconfig
클래스이다. 근데 곰곰이 생각해보면? memberService
와 orderService
둘 다 memberRepository
를 부르고있다. 이러면 memberRepository
를 세번 부르게되는거아닌가? 라는 생각이 들 수 있다. 그걸 직관적으로 보여주기위해 sout을 통해 터미널에 출력해보자
@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 = orderService.getMemberRepository();
System.out.println("memberService -> memberRepository = " + memberRepository1);
System.out.println("orderService -> memberRepository2 = " + memberRepository2);
System.out.println("memberRepository = " + memberRepository);
assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
생각대로라면 Bean등록을 세번하는데 memberRepository는 세번 호출되어야한다. 그런데 결과는?
띠용? 이게 어찌된일인고.. 우선 아래의 테스트까지 출력해보자 @Test
void configurationDeep(){
ApplicationContext ac = new AnnotationConfigApplicationContext(Appconfig.class);
Appconfig bean = ac.getBean(Appconfig.class);
System.out.println("bean = " + bean.getClass());
}
이건 빈의 클래스를 보여주는 테스트코드이다.
원래라면 내 디렉토리 경로에 있는 Appconfig에서 받아와야하는거 아닌가? 하는데 SpringCGLIB
뭐시기인게 나왔네? 이게 뭔가하니 CGLIB라는 스프링의 바이트코드 조작 라이브러리이다. 즉, 자동적으로 싱글톤을 지원해주는 라이브러리이며 @Bean
이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.
이런식으로 스프링 컨테이너 내에서 AppConfig
클래스를 상속받은 임의의 다른 클래스를 만들고 그 클래스를 빈으로 등록한것이다! 이걸 입증하기위해 반대로 @Configuration
을 빼버리고 configurationDeep
테스트를 진행한다면?
CGLIB를 지원하지않게되어서 Appconfig
클래스 내에 있는 bean들이 다수 생성되어 memberRepository
가 여러번 호출된 것!
그래서 결론은? @Bean
만으로 스프링 빈이 등록되긴하지만 싱글톤까진 지원해주지는 않는다! config에는 항상 @Configuration
을 붙이도록하자 ㅎㅎ