싱글톤 컨테이너

박지원·2024년 1월 24일

Spring

목록 보기
1/9
Spring bean - spring에 의하여 생성되고 관리되는 자바 객체를 Bean 이라고 한다

웹 애플리케이션과 싱글톤

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class SingletonTest {

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

        MemberService memberService1 = appConfig.memberService();
        MemberService memberService2 = appConfig.memberService();

        System.out.println("memberService1 = " + memberService1); //다르게 생성됨
        System.out.println("memberService2 = " + memberService2);
        
         //MemberService1 != MemberService2
        Assertions.assertThat(memberService1).isNotSameAs(memberService2);

    }
}
  • 대부분 스프링 애플리케이션은 웹 애플리케이션.
  • 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다.
  • 호출될 때마다 매번 객체가 생성이 되어야한다.
  • 위의 코드 시 총 4개의 객체가 생성된 것
  • 해결방안 -> 해당 객체가 1만 생성되고, 공유하도록 설계 -> 싱글톤 패턴!

싱글톤 패턴

  • 클래스의 인스턴스가 1개만 생성되는 것을 보장하는 디자인 패턴
  • 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다 -> private
package hello.core.singleton;

public class SingletonService {
    //private static -> 딱 하나만 존재
    private static final SingletonService instance = new SingletonService();
    
    public static SingletonService getInstance(){
        return instance;
    }

    //3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
    private SingletonService() {
    }
    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }
}
  1. private 으로 new 키워드를 막아두었다
  2. 객체 인스턴스 필요시 getInstance() 메서드 통한 호출 -> 호출할 때마다 같은 객체 인스턴스 반환
  3. 딱 하나의 객체 인스턴스만 존재해야하므로, 생성자를 private 으로 선언
  4. 싱글톤 패턴을 구현하는 방법은 많은데 , 여기서는 객체를 미리 생성해두는 방법이다
  @Test
    @DisplayName("싱글톤 패턴을 적용 객체 사용")
    void SingletonService(){
        new SingletonService();
    }

이렇게 호출할 경우에는, 아래와 같은 에러 발생

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

        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);
    }
  • 같은 인스턴스가 나온다 -> 싱글톤 적용해서 객체 하나만 생성한 것
  • 스프링 컨테이너가 기본적으로 객체를 싱글톤으로 바꿔준다
  • 싱글톤 패턴을 적용하면, 이미 만들어진 객체를 사용하기 때문에 효율적

단점

  • 싱글톤 패턴 구현하는 코드가 많이 들어간다. (위의 logic 메서드 제외한 메서드들)
  • 구체클래스에 의존 -> DIP 위반한다 - getInstance 로 꺼내야한다
  • OCP 원칙을 위반할 가능성이 크다
  • 유연한 테스트가 어렵다
  • 자식 클래스를 만들기 어렵다
  • 유연성이 떨어진다

싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다
  • 스프링 컨테이너 = 싱글톤 컨테이너 역할
  • 싱글톤 레지스트리 : 싱글톤 객체를 생성하고 관리하는 기능
  • 스프링 컨테이너의 이런 기능 덕분에 싱글톤 패턴의 단점 해결하며 객체를 싱글톤으로 유지
    1. 지저분한 코드가 들어가지 x
    1. DIP,OCP,테스트,private 생성자로부터 자유롭게 싱글톤 사용
    @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);
        //조회할 때마다 같은 객체 반환?

        System.out.println("memberService1 = " + memberService1); //다르게 생성됨
        System.out.println("memberService2 = " + memberService2);

        //MemberService1 != MemberService2
        assertThat(memberService1).isSameAs(memberService2);


    }

싱글톤 방식의 주의점

  • 싱글톤 방식은 여러 클라이언트가 공유하기 때문에, 상태 유지하게 설계를 하면 안된다

  • 무상태로 설계해야한다
    - 특정 클라이언트에 의존적인 필드 X

    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다
    • 가급적 읽기만 가능해야한다
    • 필ㄷ 대신에 자바에서 공유되지 않는 지역변수, 파라미터, 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 사용자가 20000원을 주문
           statefulService2.order("userB",20000);
    
           //ThreadA: A 사용자가 주문 금액 조회
           int price = statefulService1.getPrice();
           System.out.println("price = " + price); //20000 원이 나옴
           //인스턴스가 같기 때문에 그 객체에 있는 price 가 값이 잘못나온 것
    
           Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
               }
               
               
    		// 	아래 코드는 위의 코드를 무상태로 설계했을 때의 모습이다
       
    		public int order(String name, int price){
           System.out.println("name = " + name + "price = "+ price);
           this.price = price;
            return price;
       }
      

- TreadA가 사용자 A 코드 호출,  ThreadB가 사용자 B 호출
- StatefulService` 의 `price` 필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경한다.
- 스프링은 항상 무상태로 설계해야한다

0개의 댓글