Spring Singletone Pattern

강정우·2023년 11월 2일
0

Spring-boot

목록 보기
10/73
post-thumbnail

Spring과 web application의 특수성

  • spring은 사실 web application 말고도 다양한 애플리케이션을 만들 수 있다.
    데몬, 이라든지 배치파일이라든지 말이다. 그런데 web application을 주로 만들 때 사용하고 이는 사용자가 여려명이 각각 요청하면 각 요청마다 해당 객체를 생성한다는 것이다.
public class SingletonTest {
    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer(){
        AppConfig appConfig = new AppConfig();
        MemberService memberService1 = appConfig.memberService();
        MemberService memberService2 = appConfig.memberService();
        Assertions.assertThat(memberService1).isNotSameAs(memberService2);
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);
    }
}

  • 위 코드를 실행하면 주소값이 다른 걸 알 수 있는데 이는 JVM 메모리에 객체가 계속 생성되어 올라가는 것이다.

이때 test는 sout으로 눈으로 디버깅 하는 것으 좋으나 항상 assetions 객체를 이용하여 자동화 되도록 해주어야한다.

  • 즉, 트래픽이 초당 100이 나오면 초당 100개의 객체가 생성되고 소멸되는데 이는 메모리 소모가 정말 심하다.

  • 그래서 우리는 하나의 객체 인스턴스를 생성하여 공유하여 쓰면 된다. 이게 싱글톤 패턴의 개념이다.

싱클톤 패턴

  • 클래스 인스턴스가 딱 1개만 생성되는 것을 보장하는 즉, 하나의 JVM(하나의 서버)에서 그 객체 인스턴스가 딱 하나 생성되는 것을 보장하는 것이다.
    그럼 우선 JAVA만을 이용하여 테스트 코드를 작성해보자.
public class SingletonService {
    private static final SingletonService instance = new SingletonService();
    
    public static SingletonService getInstance(){
        return instance;
    }

    private SingletonService() {}

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

}
  1. static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.
  2. 이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있다.
  3. 생성자 함수를 private으로 선언함으로써 외부에서 객체가 생성되는 것을 막아준다.
  4. 위 방법은 가장 안전하고 편리하게 구현한 싱글톤 패턴중 하나이다. 이 외에도 여러 방법이 존재한다.

정말 잘 설계한 객체는 컴파일 오류만으로 웬만한 오류 잡을 수 있다.

  • 그렇다면 우리가 앞서 작성한
@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(getMemberRepository());
    }
    @Bean
    public static MemoryMemberRepository getMemberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(getMemberRepository(), discountPolicy());
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
//        return new FixDiscountPolicy();
    }
}
  • 이 자바 AppConfig(spring container)를 다시 다 싱글톤으로 바꿔줘야겠다고 생각했다면
    괜찮다! 왜냐하면 Spring Container를 사용하면 spring container가 다 알아서 객체를 싱글톤으로 관리해준다.

  • 그렇다면 문제가 없냐? 그건 아니다.

  1. 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
    • 위 SingletonService 클래스에서 logic 메서드하나 만들려면 나머지 위 함수들(getter, setter, etc..)을 만들어야한다.
  2. 의존관계상 클라이언트가 구체 클래스에 의존한다.
    • getInstance 즉, 인터페이스가 아닌 구체 class의 의존하게 된다. -> DIP를 위반한다.
  3. 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  4. 테스트하기 어렵다.
  5. 내부 속성을 변경하거나 초기화 하기 어렵다.
  6. private 생성자로 자식 클래스를 만들기 어렵다.
  7. 결론적으로 유연성이 떨어진다.
    • DI를 적용하기 어렵다. -> 안티 패턴으로 불리기도 한다.
  • 그런데 위 문제점들을 모두 없애고 싱글톤 패턴의 장점은 가져가는 싱글톤 컨테이너(스프링 컨테이너)에 대해 알아보자.

Singletone Container(Spring Container)

  • 싱글톤 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 Singletone registry라고 한다.
  • 스프링 컨테이너의 이런 기능 덕분에 싱글톤 패턴의 모든 단점을 해결하면서 객체를 싱글톤(1개)로 유지할 수 있다.
    * 싱글톤 패턴을 위한 지저분한 코드(getter, setter, etc..)가 필요없으며,
    DIP, OCP, 테스트 용이, pricate 생성자(상속 문제)로 부터 자유롭게 싱글톤을 사용할 수 있다.
 @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);
        Assertions.assertThat(memberService1).isSameAs(memberService2);
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);
    }

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

  • 참고로 스프링의 기본 빈 등록 방식은 싱글톤이긴 한데(99%) 항상 싱글톤은 아니다(1%).
    요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다.
    (예를들어 req할 때 새로 꺼내쓴다거나, http req 라이프 사이클에 맞춰 빈 라이프 사이클을 맞추거나, 고객이 들어올 때 만들어서 고객이 나갈 때 삭제하는 이런걸 스코프라고 하는데 나중에 알아보자.)

주의점

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

  • 즉, 무상태(stateless)하게 설계해야한다.
    * 특정 클라이언트에 의존적인 필드가 있으면 안 된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!
    • 가급적 읽기만 가능해야한다.
    • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal등을 사용해야한다.
      • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있따!!
  • 다음은 예제 테스트 코드이다.
@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 10000원 주문
  statefulService2.order("userB", 20000);

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

  Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}

// 결과 price = 20000
  • 사용자 A는 분명 만원을 조회했는데 싱글톤 객체의 같은 필드를 공유함으로써 사용자 B의 데이터가 덮어씌줘져 결과값이 20000이 되었다. 이런 불상사를 방지하기위해

공유필드는 항상 경계하고 무상태(stateless)로 설계해야한다.

생성한 클래스를 간단하게 테스트 코드 만드는 단축키
mac : cmd+shift+T, window : Ctrl+shift+T

profile
智(지)! 德(덕)! 體(체)!

0개의 댓글