[기본기] 7-1. SingleTon & Stateful의 문제

khyojun·2022년 9월 30일
0
post-thumbnail

본 게시글은 김영한님의 스프링 핵심 원리 기본편을 정리한 글입니다.


📌 SingleTon이 뭘까?

SingleTon에 대해서 설명을 하기 이전 우선 이것이 왜 생겨나게됬는지에 대해서 한 번 알아보고 가보자.
다음과 같은 그림을 한 번 볼까?

지금까지처럼 이제 웹 어플리케이션 개발을 하게 될 경우 이제 클라이언트들은 한 명도 아니고 여러명이기때문에 객체를 다음과 같이 컨테이너에서 하나씩 생성을 하게 된다. 그러면 이것의 문제점은 직관적으로만 봐도 자원이 너무나도 낭비가 된다는 것을 알 수 있다. 이게 3명이라서 이렇게 보일 수도 있는데 막 너무나도 사람들이 많아지면???? 너무 심하게 자원이 낭비된다는 것을 알 수 있다. 그래서 이러한 것을 해결하기위하여서 싱글톤 패턴이 생기게 되었다.

그러면 이러한 문제의 해결방법으로는 해당 객체가 1개가 생성이 되고 그 객체를 공유하도록 설계를 하면 된다. 그렇게 설계하는 방법이 바로! 싱글톤 패턴이다.

SingleTon 패턴

위에서 말했던 대로 싱글톤 패턴은 클래스의 인스턴스가 딱 1개만 존재할 수 있도록 보장하는 디자인 패턴이다.
코드를 한 번 보자

public class SingletonService {

    // 싱글톤 디자인 패턴 주로 이와 같은 형식을 사용
    // 1. private static final 구문을 작성하고 객체를 생성

    private static final SingletonService instance = new SingletonService();
    
    // 2. public으로 열어서 객체 인스턴스가 필요한 경우 static 메서드를 활용하여 조회할 수 있도록 한다.
    public static SingletonService getInstance(){
        return instance;
    }

    // 3. 밖에서 함부로 객체를 생성하지 못하도록 private으로 선언하였다.
    private SingletonService(){

    }
    
    public void logic(){
        System.out.println("싱글톤 객체 로직 호출");
    }
    
}

코드에 대한 설명은 위에 주석으로 설명이 되있는 것을 참고하면 좋을 거 같다. 내가 생각하기에 상당히 중요한 부분이 생성자 부분인데 바로 private으로 선언을 하여서 함부로 밖에서 불러오지 못할 수 있도록 하는 부분이다. 이 부분 덕분에 객체가 하나밖에 생성이 불가능하다는 것이었다.


부르게 되었을 경우 다음과 같이 오류가 발생한다.

그러면 테스트 코드도 한 번 확인해보자.


    @Test
    @DisplayName("기본적인 순수 DI 컨테이너")
    void pureDIContainer(){

       AppConfig appConfig = new AppConfig();

        // 1. 조회 : 호출할 때마다 객체를 생성
        MemberService memberService = appConfig.memberService();
        MemberService memberService2 = appConfig.memberService();


        // 2. 참조값이 각각 다름
        System.out.println("memberService = " + memberService);
        System.out.println("memberService2 = " + memberService2);



        // 3. 두 친구는 결국에는 다른 친구였던 것.
        Assertions.assertThat(memberService).isNotSameAs(memberService2);

    }

    @Test
    @DisplayName("싱글톤 패턴을 활용한 객체 사용")
    public void singleTonServiceTest(){
        //private으로 객체 생성 방지
        //new SingletonService() 오류 발생

        //getInstance로 객체 반환
        SingletonService singletonService = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        System.out.println("singletonService = " + singletonService);
        System.out.println("singletonService2 = " + singletonService2);

        Assertions.assertThat(singletonService).isSameAs(singletonService2);
    }

2 가지의 테스트가 있다. 위의 경우에서는 진짜 순수하게 컨테이너를 사용을 한 것과 밑에 있는 것은 싱글톤 패턴을 활용한 객체였는데 각각의 출력물은 다음과 같다.

1. 순수 컨테이너

위의 경우에는 다른 객체임을 표시

2. 싱글톤 패턴일 경우

위의 싱글톤일 경우에는 같은 객체를 가리키고 있음을 알 수 있었다.

SingleTon의 문제점

싱글톤을 물론 위에서 설명한대로 클라이언트가 막 너무 많이 요청해서 하는 경우에서는 상당히 좋은 디자인 패턴이다. 그치만 그렇다고해서 계속 남발해서 사용을 하면 안된다.
하나씩 나열해보자면 다음과 같다.

  • 싱글톤 패턴을 구현하는 코드 자체가 길다 -> private static 등 밑의 코드들을 작성하는 부분이 길다.
  • 의존관계상 클라이언트가 구현체 클래스에 의존할 수 밖에 없다. -> .getInstance를 통하여서 꺼내와야 한다. 그렇게 될 경우 DIP를 위반하는 것이다.
  • 클라이언트가 구체 클래스에 의존하니까 결국 OCP도 위반할 경우가 있다.
  • 테스트하기도 어렵다.
  • 내부 속성을 변경하거나 초기화를 하기가 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다.
  • 안티패턴이라고 부르기도 한다.

그래서 이것을 해결하기 위한 SingleTon 컨테이너

스프링 컨테이너는 싱글톤 패턴의 문제를 해결하기 위해서, 객체 인스턴스를 싱글톤으로 관리를 하여준다. 그래서 우리는 이때까지 모르고 사용한 것일 수도 있지만 생성한 빈들은 이미 싱글톤 패턴으로 스프링 컨테이너에서 관리가 되어지고 있다.

스프링 컨테이너는 기본적으로 싱글톤 컨테이너의 역할을 한다. 이런 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다. 스프링 컨테이너는 위와 같이 말했던 것처럼 이러한 자동적으로 관리해주는 좋은 기능등을 통하여서 모든 단점들을 해결할 수 있다. 즉, 코드가 지저분해지지 않아도 되고 DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용을 할 수 있다.

@Test
    @DisplayName("spring 컨테이너 활용하여 싱글톤생성")
    void singleTonwithSpring(){

        // 스프링 컨테이너는 알아서 객체가 싱글톤 상태를 유지할 수 있도록 함.
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        // 조회 과정
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);

        System.out.println("memberService = " + memberService);
        System.out.println("memberService2 = " + memberService2);

        Assertions.assertThat(memberService).isSameAs(memberService2);
    }

이렇게 할 경우 자동적으로 스프링 컨테이너안에서 싱글톤 객체로서 관리를 해주는 상태가 되었다.

다음과 같이 된다는 것을 확인을 할 수 있다.

주의사항!

그치만 이렇게 좋은 스프링 컨테이너를 사용할 때도 주의해야할 사항이 있다. 그 Stateful, Stateless상태에 대해서 이전에 HTTP에 대한 공부를 할 때 잠깐 언급을 하였었는데 우리가 싱글톤을 사용을 할 때 그 싱글톤 객체의 상태를 Stateful하게 사용하게 되면 상당히 어찌보면 위험하다! 그래서 Stateless하게 사용하는 것이 중요하다.

더 세세하게 말하자면 다음과 같은데

  • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
  • 특정 클라이언트가 값을 바꿀 수 있는 필드가 있으면 안된다.
  • 가급적 수정보단 읽기만 가능하도록!

스프링 빈의 필드의 특정 값을 공유할 수 있도록 하면 되게 큰 장애가 일어날 수 있다!

📂 StatefulService

public class StatefulService {
    private int price;
    private String name;

    public void order(String name, int price){
        System.out.println("name = " + name + " price = " + price);
        this.price=price;
        this.name=name;
    }

    public int getPrice() {
        return price;
    }

    public String getName() {
        return name;
    }
}

📂 StatefulServiceTest

public class StatuefulServiceTest {
    @Test
    @DisplayName("싱글톤을 stateful로 했을때의 문제")
    void statefulSingletonProblem(){

        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Stateful.class);

        StatefulService statefulService = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);
        
        statefulService.order("userA", 10000);
        statefulService2.order("userB", 20000);

        int price= statefulService.getPrice();
        String name = statefulService.getName();

        System.out.println("price = " + price);
        
        //stateful하게 유지를 하기 때문에 뒤에 order로 추가하게 된 20000이 값으로 들어가게 됨. 이름도 바뀜.
        Assertions.assertThat(price).isEqualTo(20000);
        Assertions.assertThat(name).isEqualTo("userB");

    }
    
    @Configuration
    static class Stateful{
        @Bean
        public StatefulService statefulService(){
            return new StatefulService();
        }
    }

}

이렇게 하면 나중에 테스트를 하고 보면 우리의 예상대로라면 10000원이 나와야 되지 않나? 라고 생각을 하는데 실제로는 20000원이 나오고 user의 이름도 userA가 아닌 userB가 나온다! 이게 진짜 생각보다 심각한 문제점이다. "나는 당연히 A 따로 B 따로 하려고 했는데 왜 이렇게 나오냐." 라고 할 수도있는데 이건 스프링 컨테이너가 싱글톤 컨테이너라는것을 잘 이해하지 못하고 하는 말일 수 있다. 그래서 위 필드에서 보게 되면 name 과 price라는 필드를 공유 필드로 사용을 하여서 생겨난 문제점이다. 그러므로 아주 주의하여야 하겠다. 실제로 실무에서는 이런 실수가 생각보다 자주는 아니지만 가끔 발생한다고 한다. 그니까 결론은 Stateless 하게 설계를 할 수 있도록 하자.

오늘의 결론.

스프링이 진짜 코딩을 하는데에 개발자에게 있어서 상당히 좋은 기능들을 많이 제공하여준다. 그치만 그렇다고 해서 싱글톤을 너무 주의하지 않고 남발하여 사용하지 말고 조심할 수 있도록 하자. Stateless를 꼭 기억하자!

출처

  1. 김영한님의 스프링 핵심 원리 기본편(https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8)
profile
코드를 씹고 뜯고 맛보고 즐기는 것을 지향하는 개발자가 되고 싶습니다

0개의 댓글