스프링 핵심 원리 이해 - 싱글톤 컨테이너 (1)

꾸준하게 달리기~·2023년 8월 19일
0

스프링 + 자바

목록 보기
15/20
post-thumbnail
post-custom-banner

들어가기 앞서

코드 내용은
https://velog.io/@dlsrjsdl6505/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EC%9D%B4%ED%95%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%99%80-%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%88
에 이어져서 작성됩니다!

글을 읽기 전에,
싱글톤 패턴에 대해 모르신다면?
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

profile
반갑습니다~! 좋은하루 보내세요 :)
post-custom-banner

0개의 댓글