[Spring] 싱글톤 패턴

이신영·2024년 1월 11일
1

Spring

목록 보기
6/16
post-thumbnail
post-custom-banner

싱글톤이란?

클래스의 인스턴스가 딱 하나만 생성되는걸 보장하는 디자인 패턴이다. 즉, 인스턴스가 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);

    }

이를 테스트해보기위해 코드를 작생해보면?

아까랑 다르게 셋이 똑같죠?

근데? 단점이 존재한다. 그 단점들을 나열해보자면..

  • 싱글톤 패턴을 구현 코드가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존 → DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존 → OCP 원칙 또한 위반할 가능성이 높다.
  • 테스트마다 데이터를 초기화를 해주어야 하므로 까다롭다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다.
  • Anti-패턴으로 불리기도 한다.

ㅋㅋ 이쯤되면 버려야하는거아님? 할수있지만? 스프링에서는 기존의 싱글톤패턴의 문제점을 해결해준다..!

빈 등록하는 방법을 예에에전에 포스팅했었는데(조만간 비슷한걸로 다시하나 올릴것) 그 빈을 등록하는 스프링 컨텍스트가 바로? 스프링 빈이며 싱글톤을 지원해주는 컨테이너이다!

스프링 컨테이너를 통해 알아서 객체(빈) 관리하기

    @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를 통해서 받아야했지만? springContainerAnnotationConfigApplicationContext를 이용해서 객체(빈)를 할당받고 알아서 싱글톤패턴을 적용시켜준다 라는것이다~ 편리하죠?

라고 하고 끝나면 얼마나 좋겠지만? 싱글톤을 사용할 때 큰 주의점이 있다!


싱글톤 패턴의 주의점

  • 무상태(stateless)로 설계해야 한다.
  • 특정 클라이언트에 의존적인 필드가 있으면 안된다
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
  • 공유 필드 대신에 자바에서 지역변수, 파라미터, 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;
    }
}

간단한 주문서비스를 생성한 다음에

주문 테스트 코드

    @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만원이 나와야겠지만?

A는 만원 더낸모습 ㅋㅋ

왜 이런 문제가 발생할까?

공유필드의 멤버변수인 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기능을 사용하여 싱글톤을 자동 지원해줄 수 있다. 바로 코드부터 보시죠

Appconfig 클래스

@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클래스이다. 근데 곰곰이 생각해보면? memberServiceorderService 둘 다 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을 붙이도록하자 ㅎㅎ

profile
후회하지 않는 사람이 되자 🔥
post-custom-banner

0개의 댓글