본문 바로가기

Spring

Spring- @configuration과 싱글톤

 들어가기에 앞서...

 스프링은 싱글톤을 최대한 보장하고자 한다. 그렇다면 그 사실을 염두하고 아래의 코드를 보자.

AppConfig.java

package hello.core;

@Configuration 
public class AppConfig {

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

}

 DI 컨테이너인 AppConfig 클래스의 memberService( ), orderService( )는 memberRepository( )를 호출한다. 그리고 memberRepository는 MemoryMemberRepository()를 new 한다. 

 

 즉 다시말해...

  • memberService 빈은 memberRepository()를 호출한다 -> new MemoryMemberRepository()를 호출한다.
  • orderService 빈은 memberRepository()를 호출한다 -> new MemoryMemberRepository()를 호출한다.

 그렇다면 이는 여러개의 MemoryMemberRepository 객체가 생성되는 것으로 생각할 수 있다. 결국 싱글톤이 깨지는 것 처럼 보인다. 스프링 컨테이너는 이를 어떤식으로 해결할까? 테스트 코드를 보자.

 

테스트 코드

package hello.core.singleton;

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);
        MemoryMemberRepository memoryMemberRepository = 
                ac.getBean("memberRepository", MemoryMemberRepository.class);
                
               					....

    }
}

아마 위 코드를 실행하면 결과는 이런식이 되지 않을까?

 

위 코드의 추측한 결과

//memberService 생성과정에서 출력
call AppConfig.memberService
call AppConfig.memberRepository
//orderService 생성과정에서 출력
call AppConfig.orderService
call AppConfig.memberRepository
//memoryMemberService 생성과정에서 출력
call AppConfig.memberRepository

 

실제결과는 다음과 같다.

실제 결과

call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService

원래 예상대로라면 "call AppConfig.memberRepository"가 세번 호출되었어야 하는데 반해 실제로는 한번만 호출이 되었다.

 

 @Configuration과 바이트 조작코드 

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

    AppConfig bean = ac.getBean(AppConfig.class);

    System.out.println("bean.getClass() = " + bean.getClass());
}

bean.getClass() = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$641ec7cb

 출력 결과를 보면 class hello.core.AppConfig 뒤에 $$EnhancerBySpringCGLIB$$641ec7cb와 같이 이상한게 적혀있는 것을 볼 수 있다. 만일 bean이 내가만든 클래스라면 "class hello.core.AppConfig"가 나와야한다. 그런데 뒤에 이상한 것들이 붙어있는 것으로 보아 이는 내가 만든 클래스가 아니라는 것을 알 수 있다. 

 

 이것은 사실 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트 코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 클래스를 스프링 빈으로 등록한 것이다.

AppConfig를 상속받은 AppConfig@CGLIB가 스프링 빈으로 등록된다

그리고 스프링 빈으로 등록된 임의의 다른 클래스는 싱글톤이 보장되도록 해준다.

아까전에 "call AppConfig.memberRepository"가 한번만 출력된 것이 이 때문이라는 것이다. 

AppConfig@CGLIB는 아마 이런식으로 등록되어 있을 것이다.

 

@Bean
public MemberRepository memberRepository(){

	//싱글톤을 보장해주는 if else
    if(스프링에 memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있다면){
        return 스프링 컨테이너에서 찾아서 반환;
    }
    else{
        기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
        return 생성한 MemberRepository 객체;
    }
    
}

 결과적으로 @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다. 

 @Configuration을 사용하면 CGLIB를 사용해서 싱글톤을 보장하는 것이다. 

 @Configuration을 붙이지 않는 경우

그렇다면 @Configuration을 사용하지 않는다면 어떨까? AppConfig에서 @Configuration만 제거하고 테스트 코드를 재실행 해보자.

@Configruation을 제거한 AppConfig

//@Configuration
public class AppConfig{
    ....
}

테스트 코드 결과

call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
bean.getClass() = class hello.core.AppConfig

확실히 @Configuration을 제거하기 전과는 다름을 볼 수 있다. 우리가 기존에 추측했던 것처럼 객체들이 new(싱글톤 깨짐)되고 있고 또한 스프링 빈에 등록된 클래스또한 AppConfig@CGLIB가 아니라 AppConfig이다.

 

   정리

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