싱글톤 컨테이너, 조금 더 들여다보기

    싱글톤 컨테이너를 조금 더 들여다보고 어떻게 동작되는지를 좀 더 알아보면 좋을 것 같다.

    싱글톤 컨테이너, 싱글톤이 깨지는거 아냐?

    우선 아래코드를 실행해본다고 가정하자. 아래 코드에서 OrderService를 부를 때, memberRepository()로 이동해, new MemoryMemberRepository()가 실행된다. 아래 코드에서 MemberService를 부를 때, memberRepository()로 이동 해, new MemoryMemberRepository()가 실행된다. 두 코드가 다 실행되면, MemoryMemberRepository가 두번 실행되면 싱글톤 패턴이 깨지는 것처럼 보인다. 진짜 깨질까?

    @Configuration
    public class AppConfig {
        @Bean
        public MemberRepository memberRepository(){
            System.out.println("call memberRepository");
            return new MemoryMemberRepository();
        }
        @Bean
        public DiscountPolicy discountPolicy(){
            System.out.println("call discountPolicy");
            return new RateDiscountPolicy();
    //        return new FixDiscountPolicy();
        }
        @Bean
        public OrderService orderService(){
            System.out.println("call orderService");
            return new OrderServiceImpl(discountPolicy(), memberRepository());
        }
        @Bean
        public MemberService memberService(){
            System.out.println("call memberService");
            return new MemberServiceImpl(memberRepository());
        }
    }

    실제로 검증하는 코드를 아래와 같이 작성했다. 테스트를 위해 OrderServiceImpl, MemberServiceImpl에 MemberRepository()를 가져오는 테스트 코드를 추가했고, 아래와 같이 테스트 코드를 짯다. 테스트의 목적은 각각을 생성했을 때, 생성되는 MemberRepository가 같은 객체인지 아닌지를 확인하는 것이었다. 

    @Test
        @DisplayName("싱글톤 진짜 유지되는거 맞는지 확인")
        void test1(){
    
            ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
            // MemberRepository -> memberRepository()
            MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
            // OrderService -> memberRepository()
            OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
            // MemberService -> memberRepository()
            MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
    
            // 정말 싱글톤으로 같은 객체를 공유하고 있는지 확인해본다.
            MemberRepository memberRepository1 = memberService.getMemberRepository();
            MemberRepository memberRepository2 = memberService.getMemberRepository();
    
            System.out.println("memberServiceimpl -> memberRepository1 = " + memberRepository1);
            System.out.println("memberServiceimpl -> memberRepository2 = " + memberRepository2);
            System.out.println("orderServiceimpl -> memberRepository = " + orderService.getMemberRepository());
    
            Assertions.assertThat(memberRepository1).isSameAs(memberRepository2);
            Assertions.assertThat(memberRepository1).isSameAs(orderService.getMemberRepository());
    
            
    
    
        }

    위 코드를 실행해보면 아래 결과가 나오게 된다. MemberService에서 2번 부른 것도 같은 결과, orderserviceImpl에서 MemberRepository를 부른 것도 같은 인스턴스라는 것을 알게 되었다. 근데 어떻게 이런 결과가 나오게 되는 것일까?

    실제로 아래 결과가 나오기까지 Java 코드만 살펴보면 MemberRepository() 생성자가 세 번 실행되어야 하는 것이 맞다. 생성자가 세번 실행되면, 셋다 같은 인스턴스가 나와야 한다. 그렇다면 정말로 호출이 세번 이루어지는 살펴보면 되지 않을까?

    실제로 각 Bean 정보가 몇번이나 호출되는지 알아보기 위해서 Bean Configuration에 print 함수를 추가해서, 몇번 호출되는지를 알아봤다. 예상되는 결과는 다음과 같다

    1. MemberServiceI -> memberRepository()

    2. MemberServiceI -> memberRepository()

    3. OrderService -> memberRepository()

    실행한 결과는 아래 콘솔창에서 확인할 수 있는데, 각 Bean 생성자가 한번씩 실행이 된 것이 전부다. 즉, 한번씩만 생성되었다. 어떻게 이런 일이 일어날 수 있는 것일까? 

     

    @Configuration과 바이트코드 조작의 마법 (@Configuration을 적용한 AppConfig)

    - @Configuration 어노테이션이 붙으면, CGLIB가 적용된 AppConfig 임의의 클래스가 등록됨.

     

    스프링 컨테이너는 일반적으로 싱글톤 컨테이너로 사용된다. 싱글톤 컨테이너는 각 빈 객체가 1번씩만 생성되는 것이 보장되어야 한다. 그런데 위의 코드에서는 세 번은 생성이 되어야 한다. 이것을 해결하기 위해 스프링은 바이트코드를 조작하는 라이브러리를 사용해서 구현해준다.

     

    AppConfig.class 설정 정보를 넘겨주면, AppConfig 자체도 스프링 빈으로 등록이 된다. 그렇다면 이 AppConfig 빈을 불러와서 아래와 같이 한번 출력해본다.

        @Test
        @DisplayName("싱글톤 딥 테스트")
        void configurationDeep(){
            ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
            AppConfig bean = ac.getBean(AppConfig.class);
            System.out.println("ac.getClass() = " + bean.getClass());
    
        }

    출력한 결과는 아래에서 확인할 수 있는데 AppConfig 뒤에 $$로 CGLIB라는 형식의 이름이 더 붙은 것을 볼 수 있다. 순수한 클래스라면 AppConfig로 출력되어야하는데 그렇지 않다. 이는 AppConfig를 스프링 빈에 등록하는 과정에서 내가 만든 AppConfig 클래스가 바로 등록되지 않은 것을 의미한다.

    스프링은 내가 만든 AppConfig 클래스를 바로 등록하는 것이 아니라, CGLIB 라이브러리를 활용해 AppConfig를 상속받은 임의의 클래스를 만들어서 스프링 빈에 등록한 것이다. 이것을 그림으로 표현하면 아래와 같이 볼 수 있다. 빈 이름 자체는 appConfig지만, 실제로 등록된 인스턴스는 appConfig @ CGLIB 객체다. 

     

     

    그렇다면 이 CGLIB 라이브러리는 어떤 로직이 적용되면서, 단 하나의 빈 객체만 등록될 수 있도록 도와주는 것일까? 모르긴 몰라도 아래와 같은 형태의 코드가 될 것이다. @Configuration이 붙은 클래스에서 @Bean이 붙은 메서드를 하나씩 살펴본다. 그리고 1) 빈 객체가 이미 스프링 컨테이너에 등록되어있는지 확인하고, 등록이 되어있다면 그것을 찾아서 반환한다. 2) 만약에 등록되어있지 않다면 하나 생성해서 등록 후 반환한다. 이런 동작 방식 때문에 싱글톤이 보장되는 것이다.

    @Bean
    public MemberRepository memberRepository() {
     
     if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
     	return 스프링 컨테이너에서 찾아서 반환;
     } else { //스프링 컨테이너에 없으면
     	기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
     	return 반환
     }
    }

     

     

    @Configuration 없이 @Bean만 등록한다면? 

    - Bean만 사용해도 스프링 빈에는 등록이 된다. 단, 싱글톤 패턴은 보장되지 않는다.

    - 그냥 Spring은 항상 @Configuration을 쓴다고 생각하자.

    앞서 @Configuration을 넣으면 CGLIB를 활용한 임의의 AppConfig 자식 클래스가 생기고, 이 CGLIB는 등록되어있는 빈은 추가로 등록하지 않아 싱글톤 패턴을 유지해준다고 이야기했다. 여기서 Configuration을 빼고, AppConfig 빈 객체를 출력해보면 아래와 같이 순수한 AppConfig 빈 객체가 저장된 것을 볼 수 있다. 즉, 순수한 AppConfig가 등록이 되었기 때문에 싱글톤 패턴이 유지되지 않을 것으로 예상된다.

    실제로 위의 검증 코드를 다시 실행해보면, 아래처럼 memberRepository가 세 번 호출된 것을 확인할 수 있다. 싱글톤 패턴이 유지가 되지 않는 것을 알 수 있다. 그렇다면 인스턴스는 같을까? 

    생성된 인스턴스도 한번씩 출력을 해봤다. 출력을 해보면 모두 다른 인스턴스인 것을 확인할 수 있다.

     

     

    댓글

    Designed by JB FACTORY