글을 읽기 전에,
싱글톤 패턴에 대해 모르신다면?
https://velog.io/@dlsrjsdl6505/%EC%8B%B1%EA%B8%80%ED%86%A4-%ED%8C%A8%ED%84%B4
지난시간에, 앱의 구성에 대해 설정하는
AppConfig클래스를 다음과 같이 작성했다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
}
그렇다면, 위의 설정과 같은 AppConfig로
어플리케이션 서비스를 실행한다고 생각해보자.
해당 서비스에 있어 여러 고객이 memberService 인터페이스의 매서드를 사용할 일이 생겼다.
동시에 여러 고객이 memberService()
를 사용하게 된다면,
그때마다
return new MemberServiceImpl(memberRepository());
위의 로직이 실행되어
새로운(new) 멤버서비스(MemberServiceImpl(memberRepository()))가 반환되게 된다.
해당 내용을 그림으로 보면, 다음과 같다.
실제로 싱글톤을 보여주기 위해 아래의 테스트코드를 작성하고 실행하면
@Test
@DisplayName("스프링 없는 DI 컨테이너. 즉 싱글톤 적용 X")
void pureContainer(){
AppConfig appConfig = new AppConfig();
// 조회 : 호출할 때 마다 객체 생성
// 일부러 싱글톤 패턴을 배제하기 위해!
// appConfig.memberService(); 를 사용
MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();
//서로 다르다는 것 보여주기
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
//실제로 다른지 매서드로 확인. (isNotSameAs 라서 달라야 통과)
Assertions.assertThat(memberService1).isNotSameAs(memberService2);
}
아래의 사진과 같이 다른 memberService객체가 반환됨을 알 수 있다.
그럼, 이렇게 여러 객체가 생성되고 소멸되는것의 문제점은 무엇일까?
트래픽에 따라 수많은 객체가 생성되고 소멸되므로 메모리 낭비가 심할 수 있다.
동시성 문제에 따라, 상태가 꼬여버릴 수 있다.
이제 이러한 문제를 해결해주는 방식이 인스턴스가 딱 1개만 생성되는 것을 보장하는 싱글톤 패턴이다.
여기서 또 알아야 할 내용이 있다.
순수 자바 코드로 호출할 때 마다 똑같은 인스턴스만을 반환하는
싱글톤을 구현할 수 있다.
(위에서 주어진 싱글톤 패턴 링크에 구현 코드가 있습니다!)
하지만 그렇게 순수 자바로 구현을 하면,
SingletonService.getInstance()
와 같은 방식을 통해 인스턴스를 얻기에,
해당 싱글톤 객체를 생성하는 클래스 자체에 의존하게 된다.
즉, 구체 클래스에 의존하게 되므로,
의존 역전 원칙에 어긋나게 된다.
물론 틀린 코드는 아니지만, 더 나아질 부분이 많아지고 유연성이 떨어지는 코드가 된다는 소리이다.
그래서 Spring에서는, 순수 자바 싱글톤 패턴을 지양하고
컨테이너 안의 객체 인스턴스도 싱글톤으로 관리한다.
우리가 지금까지 사용했던 @Bean
어노테이션을 붙여 사용한 빈이
싱글톤으로 관리되는 빈이다.
이러한 기능이 싱글톤 패턴의 단점을 해결하는 동시에
싱글톤 패턴의 이점은 취할 수 있게 해준다.
@Bean
의 싱글톤 패턴 적용 확인을 위해, 아래의 테스트코드를 추가했다.
이전에는 Spring 없이 곧바로 꺼내오는
appConfig.nenberService()
를 사용했지만,
이번에는 스프링 프레임워크 사용을 위해
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
를 사용했다.
@Test
@DisplayName("스프링 컨테이너와 싱글톤!")
void springContainer(){
ApplicationContext 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);
//실제로 같은지 매서드로 확인. (isSameAs 라서 같아야 통과)
Assertions.assertThat(memberService1).isSameAs(memberService2);
//스프링 컨테이너 덕분에 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있다.
}
이렇게 해당 테스트코드를 실행하고 결과를 보면,
아래와 같이 같은 객체를 반환한다는 것을 알 수 있다.
그리고 해당 로그는 아래의 사진과 같은 셈이다.
무상태성으로 설계해야 한다.
무상태성이란? - 지금까지 거쳤던 상태들을 모르는것
유상태성 예시
A : B님, 저 물건 C 주세요
B : 예
A : 얼마인가요?
B : 500원이요
A : 네 여기 500원 드릴게요~
무상태성 예시
A : B님, 저 물건 C 주세요
B : 예
A : 얼마인가요?
B : 뭐가요?
A : 물건 C의 가격이요
B : 아 ㅎㅎ; 500원입니다
A : 네 그거 주세요
B : 뭐를요?
물론 여기까지 오면 무상태성은 이해할 수 있지만,
싱글톤 방식에서 자주 일어나는 주의점 예시인 유상태성에 대한 문제를 코드를 통해 말씀드리겠다.
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(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
//TreadA : A사용자 10000원 주문
statefulService1.order("userA", 10000);
//TreadB : B사용자 20000원 주문
statefulService2.order("userB", 20000);
//TreadA : A사용자 주문 금액 조회
//(원래대로라면 10000원 나와야함, 여기서는 A 주문하고 A 가격조회 사이에 B사용자 주문이 끼어든 상태라서 20000이 나와버림.)
int price = statefulService1.getPrice();
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
//진짜 공유필드는 조심해야 한다! 스프링 빈은 항상 무상태(stateless)로 설계하자.
//무상태 설계는 StatefulService 클래스에 주석으로 달아놓음. 물론 여기 클래스에서도 바꿔야할 부분 있음! (order의 리턴값이 바뀌니.)
}
static class TestConfig{
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
처음 작성한 StatefulService
클래스를 사용해
Spring 방식의 싱글톤으로
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
각각 다른 트래픽을 처리한다.
//TreadA : A사용자 10000원 주문
statefulService1.order("userA", 10000);
//TreadB : B사용자 20000원 주문
statefulService2.order("userB", 20000);
여기서,
int price = statefulService1.getPrice();
이 코드의 결괏값으로는 10000이 나와야한다.
서비스 1은 만원을 주문했으니까.
하지만 결과는 웬걸, 생각했던 것과 다르다.
그 이유는 다음과 같다.
아래 코드의 사이에 낀 satefulService2를 보자.
statefulService1.order("userA", 10000);
statefulService2.order("userB", 20000);
int price = statefulService1.getPrice();
싱글톤 패턴이므로,
statefulService1과 statefulService2는 같은 인스턴스이다.
여기서 문제가 발생한다.
이렇게, 싱글톤 패턴을 사용할때는
싱글톤 객체의 모든 값은 공유되므로,
하나의 객체를 사용한다! 라는 점을 신경써서 개발해야 한다.
레퍼런스 : 김영한님 pdf
소스코드 : https://github.com/ingeon2/coreofspring-SOLID