1.웹 애플리케이션과 싱글톤?

Kuno17·2023년 5월 1일
0
post-thumbnail

웹 애플리케이션과 싱글톤

  • 스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생.
  • 대부분의 스프링 애플리케이션은 웹 애플리케이션이다.
  • 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다.

즉 클라이언트A,B,C 가 memberServic 요청을 하면 DI컨테이너(AppConfig)에서 new memberService x01,x02,x03 ... 과 같이 요청마다 객체를 생성하는 문제가 발생하는게 아닌가?

직접 테스트해보자

    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();

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

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

        //3. 참조값이 다른 것을 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);
        
        //memberServic1 != memberService2
        Assertions.assertThat(memberService1).isNotSameAs(memberService2);
    }

결과는 다음과 같이 나왔다!

memberService1 = hello.core.member.MemberServiceImpl@5dda768f
memberService2 = hello.core.member.MemberServiceImpl@7a8c8dcf

즉 요청마다 새로운 객체를 만들고 있다. -> 즉 메모리를 계속해서 잡아먹는다 -> 매우 비효율적.

싱글톤 패턴!

이런 문제를 해결하기 위한 방법이 바로 싱글톤 패턴이다.
즉 해당 객체가 딱1개만 생성되고 공유하도록 하는 방법이다.

public class SingletonService {

    //static으로 생성(단1개만 생성)
    private static final SingletonService instance = new SingletonService();

    //instace의 참조를 꺼낼 수 있는 방법은 이제 이 메서드 밖에 없다.
    public static SingletonService getInstance() {
        return instance;
    }

    private SingletonService() {}

    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }
}
  1. static 영역에 객체 instance를 미리 하나 생성한다.
  2. 이제 오직 getInstance() 메서드를 통해서만 조회할 수 있다 -> 항상 동일한 Instance를 반환.
  3. 딱1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private로 막아서 외부에서 new 키워드로 생성을 못하게 처리.

이제 TestCode를 통해서 확인해보자 같은 Instance를 가리키는지 확인할 수 있다.

    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    void singletonServiceTest() {
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);
    }

결과는 다음과 같이 같은 Instance가 나온것을 알 수 있다.

singletonService1 = hello.core.singleton.SingletonService@ba2f4ec
singletonService2 = hello.core.singleton.SingletonService@ba2f4ec

그렇다면 AppConfig도 싱글톤으로 변경해주면 될까?
맞다 그렇지만 모두 수작업으로 변경할 필요가 없다. 스프링 컨테이너를 사용하면 기본적으로 객체를 싱글톤으로 관리해준다!

싱글톤 패턴의 문제점! (단점이 더 많아!)

  • 구현하는 코드 자체가 많이 사용된다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다 -> DIP위반 (구체 클래스.getInstance() 이런식으로 불러와야하는 문제가 발생)
  • 클라이언트가 구체 클레스에 의존해서 OCP원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경,초기화하기 어렵다.
  • private 생성자로 자손 클래스 만들기 어렵다.
  • 결론적으로 유연하지 못하다.
  • 안티패턴으로 불리기도 한다.

그렇다면 어떻게 싱글톤으로 관리하면서 단점들을 상쇄할 수 있을까?

싱글톤 컨테이너 (내가 해결함)

일일이 싱글톤 패턴코드를 적용하는게 아니라 스프링 컨테이너를 거치게 되면 자동으로 싱글톤으로 관리하게 된다.
그러면 추가적인 코드를 작성할 필요도 없으며 구체 클래스에 의존할 필요성이 없어진다.

TestCode로 확인해보자

    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer() {

        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);

        //3. 참조값이 다른 것을 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        //memberServic1 != memberService2
        assertThat(memberService1).isSameAs(memberService2);
    }

결과는 다음과 같다.

memberService1 = hello.core.member.MemberServiceImpl@5dd91bca
memberService2 = hello.core.member.MemberServiceImpl@5dd91bca

스프링 컨테이너 덕분에 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라. 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있다.

싱글톤 방식의 주의점!

싱글톤 패턴을 사용하던 싱글톤 컨테이너를 사용하던! 인스턴스를 하나만 생성해서 공유하는 방식은 여러 클라이언트가 하나의 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안된다.

  • 무상태로 설계해야 한다.
    • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
    • 가급적 읽기만 가능하게 한다.
    • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal등을 사용한다.
  • 스프링 빈의 필드에 공유값을 설정하면 정말 큰 장애가 발생할 수 있다!!!

다음과 같은 경우로 확인할 수 있다.
price라는 상태를 가지는 필드를 가진 class가 있고 컨테이너를 통해서 싱글톤방식으로 공유되고 있다.

public class StatefulService {

    private int price; //상태를 유지하는 필드

    public void order(String name , int price) {
        System.out.println("name = "  + name + " pirce = " + price);
        this.price = price; // 여기가 문제!
    }

    public int getPrice(){
        return price;
    }
}

그런데! 유저A 유저B가 order()메서드를 통해서 주문을 넣을때 바로 price의 값이 변경되버린다는 점이다.
문제는 유저A의 값이 유저B가 주문한 값으로 변경되되는 점아다.
TestCode는 다음과 같다.

    @Test
    void  statefulServiceSingleton(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        //ThreadA :  A사용자 10,000원 주문
        statefulService1.order("userA",10000);
        //ThreadA :  B사용자 20,000원 주문
        statefulService2.order("userB",20000);

        // ThreadA : 사용자A 주문 금액 조회
        int price = statefulService1.getPrice();
        System.out.println("price = " + price);
    }

    static class TestConfig {

        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }

결과는 다음과 같다.

name = userA pirce = 10000
name = userB pirce = 20000
price = 20000

이 문제는 아주 간단한 예시일뿐 이 상황에서도 유저A의 결제가 20000원으로 이루어질 위험이 존재한다는 것이다.
더욱더 복잡한 문재가 발생한다면 피해가 기하급수적으로 커질 수 있다.

그렇기 때문에 this.price를 통해서 변경하는 부분을 제거하고 return을 바로해버리면 된다.
그리고 price를 출력하는게 아니라 userAprice ,userBprice와 같은 지역변수로 받을 수 있다.

의외로 공유필드와 관련된 문제가 실무에서도 꼭 한번씩은 나온다고 한다!
스프링 빈은 항상 무상태(stateless)로 관리하는것이 좋다.


후기

인프런 김영한님의 강의를 공부하면서 정리한 내용이다.
단순이 강의 내용을 따라하면서 정리한것이지만 그냥 라이브코딩으로 따라하는것 보다는 생각이 정리되는점이 많아서 훨씬 이해도가 높을 수 있었던것 같다.

profile
자바 스터디 정리 - 하단 홈 버튼 참조.

0개의 댓글