AppConfig의 생성자 호출

아래 코드를 보다시피 AppConfig에서 new MemoryMemberRepository()는 3회 호출된다. 그러면 MemoryMemberRepository의 인스턴스가 1개 이상 생성되어 싱글톤 패턴이 깨지는 것 아닌가?라고 생각할 수 있다. 그러나 그렇지 않다. 

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService() {
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(orderRepository(), memberRepository(), discountPolicy());
    }

    @Bean
    public OrderRepository orderRepository() {
        System.out.println("call AppConfig.orderRepository");
        return new MemoryOrderRepository();
    }

    @Bean
    public MemberRepository memberRepository() {
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        System.out.println("call AppConfig.discountPolicy");
        return new FixDiscountPolicy();
    }
}

테스트를 위해 AppConfig에 println() 메서드를 추가하고 memberService와 orderService에 MemberRepository의 인스턴스를 반환하는 메서드를 추가했다. 그리고 생성된 MemoryMemberRepository의 인스턴스가 같은지 다른지 확인하기 위해 테스트 코드를 작성했다.

public class ConfigurationSingletonTest {

    @Test
    void configurationTest() {
        ApplicationContext 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 -> memberRepository = " + memberRepository1);
        System.out.println("orderService -> memberRepository = " + memberRepository2);
        System.out.println("memberRepository = " + memberRepository);

        assertThat(memberRepository1).isSameAs(memberRepository2);
    }
}
/* 실행 결과
15:57:26.981 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig'
15:57:26.987 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberService'
call AppConfig.memberService
15:57:27.024 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberRepository'
call AppConfig.memberRepository
15:57:27.028 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderService'
call AppConfig.orderService
15:57:27.029 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderRepository'
call AppConfig.orderRepository
15:57:27.033 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'discountPolicy'
call AppConfig.discountPolicy
memberService -> memberRepository = study.conversiontospring.member.MemoryMemberRepository@7e7b159b
orderService -> memberRepository = study.conversiontospring.member.MemoryMemberRepository@7e7b159b
memberRepository = study.conversiontospring.member.MemoryMemberRepository@7e7b159b
*/

확인해 보면 주소값이 모두 같으므로 MemoryMemberRepository의 인스턴스는 모두 같은 인스턴스다. 그리고 AppConfig의 코드대로라면 memberRepository() 메서드가 3회 호출되어 "call AppConfig.memberRepository"라는 로그도 3회 출력되어야 한다. 그러나 처음 한 번만 출력되고 이후에는 등장하지 않는다.

@Configuration과 바이트 조작

스프링 컨테이너는 싱글톤 레지스트리이기 때문에 스프링 빈이 싱글톤이 되도록 보장해야 한다. 이를 위해 스프링 프레임워크는 클래스의 바이트 코드를 조작하는 라이브러리를 사용한다. 스프링 설정파일에 @Configuration 애노테이션을 붙이면 스프링이 바이트 코드를 조작할 수 있다. 테스트 코드를 통해 확인해보자. AnnotationConfigApplicationContext에 매개변수로 전달한 클래스도 스프링 빈으로 등록된다. 그래서 AppConfig도 스프링 빈으로 등록된다.

public class ConfigurationSingletonTest {

    @Test
    void configurationDeep() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        AppConfig bean = ac.getBean(AppConfig.class);
        System.out.println("bean = " + bean.getClass());
    }
}
/* 실행 결과
bean = class study.conversiontospring.AppConfig$$EnhancerBySpringCGLIB$$6b11b47c
*/

만약 개발자가 직접 등록한 AppConfig라면 클래스 정보는 class study.conversiontospring.AppConfig와 같을 것이다. 그러나 결과는 그렇지 않다. 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig를 상속받은 임의의 클래스를 정의하고 그 클래스를 스프링 빈으로 등록했기 때문이다. 스프링이 정의한 클래스가 스프링 빈이 싱글톤이 되도록 보장해준다.

@Bean 애노테이션이 붙은 메서드마다 스프링 빈이 이미 존재하면 존재하는 빈을 반환하고, 그렇지 않으면 생성해서 스프링 빈으로 등록하는 코드가 동적으로 만들어진다. 아마 스프링이 임의로 등록한 AppConfig의 코드는 아래와 같은 형식일 것이다.

    @Bean
    public MemberRepository memberRepository() {
        if (MemoryMemberRepository가 컨테이너에 등록되어 있으면) {
            return 컨테이너에서 조회하여 반환
        } else {
        	기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 빈으로 등록
            return 반환
        }
    }

 

+ Recent posts