모각소-5(싱글톤 컨테이너)

LEEHYUNJE·2024년 1월 28일
0
post-thumbnail

오늘은 스프링의 싱글톤 이라는 개념과 이를 컨테이너 내에서 어떻게 사용되는지에 대해서 공부하였다.

싱글톤 컨테이너

웹 애플리케이션과 싱글톤

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

    클라이언트 A,B,C가 각 클라이언트요청에 따라 객체를 생성하여 배정해준다.
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);
        Assertions.assertThat(memberService1).isNotSameAs(memberService2);
    }

}
  • 우리가 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다.
  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다. -> 메모리 낭비가 심하다.
  • 해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다. -> 싱글톤 패턴

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
  • 그래서 객체 인스턴스를 2개 이상 생성하지 못하도록 한다.
    * private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 한다.
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. 딱 1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private으로 막아서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.

싱글톤 패턴을 적용하면 고객의 요청마다 인스턴스를 생성하지 않을 수 있으므로 메모리를 낭비하지 않아도 된다.

싱글톤 패턴 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다.
  • 안티패턴으로 불리기도 한다.

"하지만 spring은 싱글톤의 모든 문제점을 없애주고, 장점은 다 가져와준다."

싱글톤 컨테이너

스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리한다.

싱글톤 컨테이너

  • 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
    • 이전에 설명한 컨테이너 생성 과정을 자세히 보자. 컨테이너는 객체를 하나만 생성해서 관리한다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
  • 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
    • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
    • DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다

  • 스프링 컨테이너 덕분에 고객의 요청이 올때마다 객체를 생성하는 것이 아닌, 만들어진 객체를 반환해준다.

##스프링의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아니다. 요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다.이를 빈 스코프라고 한다. -> 99퍼센트 싱글톤 방식이다.

싱글톤 방식의 주의점(중요)

  • 싱글톤 패턴이든, 싱글톤 컨테이너 던, 객체 인스턴스를 하나만 생성하고 공유하는 방식은 조심해야한다. 인스턴스가 stateful이기 때문에

  • 무상태로 설계해야한다(stateless).

    • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!
    • 가급적 읽기만 가능해야 한다.
    • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.

    왜 stateless로 해야하나?

  • 같은 인스턴스를 사용하기 때문에 다른 사용자가 설정해둔 인스턴스 값에 다른 사용자가 정보를 바꿀 수도 있기 때문이다.

  • 공유필드는 항상 조심해야한다. -> 항상 spring은 stateless로 설계하자.

  • 실무로 몇년에 한번씩 꼭 만나는 문제이므로 조심하자.

    @Configuration과 싱글톤

@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository()); // 멤버 서비스를 부르게 된다면, 생성자 주입.
    }
    @Bean
    public MemberRepository memberRepository() { // 메모리 멤버 리포지토리를 사용할거야
        return new MemoryMemberReopository();
    }
    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy()); // 여기서 구현체를 지정해줄 수있음
    }
    @Bean
    public DiscountPolicy discountPolicy(){ //할인 정책은 고정으로 할거야
        //return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}
  • 결과적으로 각각 다른 2개의 MemoryMemberRepository 가 생성되면서 싱글톤이 깨지는 것 처럼 보인다. 스프링 컨테이너는 이 문제를 어떻게 해결할까?
public class ConfigurationSingletonTest {

    @Test
    void configurationTest(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);

        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();

        System.out.println("memberService -> memberRepository1 = " + memberRepository1);
        System.out.println("orderService -> memberRepository2 = " + memberRepository2);
        System.out.println("orderService -> memberRepository2 = " + memberRepository);

        Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }
}
  • 테스트 결과 모두 같은 인스턴스이다. 충격
  • AppConfig의 자바코드를 보면 인스턴스를 계속 생성하는데 왜 같을까?

@Configuration과 바이트코드 조작의 마법

스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링이 싱클톤이 되는 것을 보장해야하는데, 자바코드까지 직접 관여하기는 어렵다.
-> 그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다.

@Test
void configurationDeep() {
 ApplicationContext ac = new
AnnotationConfigApplicationContext(AppConfig.class);
 //AppConfig도 스프링 빈으로 등록된다.
 AppConfig bean = ac.getBean(AppConfig.class);

 System.out.println("bean = " + bean.getClass());
 //출력: bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
}
  • 위 코드의 출력 결과가 AppConfig로 끝나지 않고 그 뒤에 이상한 결과들이 나온다.

    실제로는 AppConfig로 등록하는 것이 상속을 하여 다른 인스턴스로 할당이 되는 것이다.

  • 위 같은 이유로 AppConfig에서 3번을 호출하더라도 1번만 출력이 나오는 이유이다.
  • 덕분에 싱글톤이 유지된다.

#참고 AppConfig@CGLIB는 AppConfig의 자식 타입이므로, AppConfig 타입으로 조회 할 수 있다

@Configuration 을 적용하지 않고, @Bean 만 적용하면 어떻게 될까?

-> 싱글톤이 적용되지 않는다. 이유는 중복된 인스턴스 생성 요청에도 계속해서 인스턴스를 생성하기 때문이다.

정리

  • @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
  • memberRepository() 처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.
  • 크게 고민할 것이 없다. 스프링 설정 정보는 항상 @Configuration 을 사용하자

모각소 소감

싱글톤이라는 개념이 처음에는 생소했는데, 공부할수록 java에서 static영역이 생각나서 이번에는 쉽게 이해가 되었다. 즉 싱글톤 컨테이너란, 컨테이너 내의 빈을 고객이 요청할때 빈에 대한 인스턴스를 새로운 메모리에 생성하고 고객에게 할당하는 것이 아니라, 하나의 메모리공간에 생성하고 고객에게 할당해주는것. 점점 공부를 하지 않는 내가 보이는데 계속 할 수 있도록.

profile
현재진행중

0개의 댓글