들어가며

  • 이전까지 스프링 컨테이너에서 어떻게 각 객체를 싱글톤 기법으로 관리하고 있는지에 대해 알아보았다.
  • 이번에는 @Configuration으로 등록한 설정 파일이 실행될 때 생기는 현상 중, 객체가 여러번 생성되는 것 처럼 보이는 상황과 그에 대해 실제로 어떤 동작이 수행되고 있는지에 대해 알아본다.

 

 

@Configuration

  • 다음의 AppConfig 코드를 보자.
@Configuration
public class AppConfig{
    
    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }
    
    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    
    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
    
    @Bean
    public DiscountPolicy discountPolicy(){
        return new FixDiscountPolicy();
    }
}
  • 이전에 스프링은 앱이 실행되면 @Configuration이 적힌 설정 파일을 이용하여 @Bean이 들어간 모든 메서드들을 실행시켜 그 객체를 반환받아 관리한다고 하였다.
  • 동시에 스프링 컨테이너는 싱글톤으로 관리되고 있기 때문에 모든 객체들은 단 하나여야만 한다.
  • 그런데 잘 생각해보면 다음과 같은 상황이 일어날 수 있다.
    • 코드가 위에서 아래로 수행된다고 가정할 때 가장 먼저 수행되는 메서드는 memberService()일 것이다.
    • memberService()메서드는 memberRepository()메서드를 실행시켜 memberRepository의 구현체를 주입받고, 서비스 구현체를 생성하여 반환한다.
      • 이때 memberRepository가 인스턴스화 하여 반환된 횟수는 1번이다.
    • 그 다음 orderService()가 수행되고 역시 memberRepository와 discountPolicy 구현체를 주입받아 자신의 orderService 구현체를 만들어 반환한다.
      • 여기서부터 무언가 이상하다. memberRepository() 메서드 에서 new를 통해 memberRepository 객체를 생성하여 반환하고 있는데 지금이 2번째 수행된 것이므로 새로운 객체가 2번 만들어진다.
    • 이후에 memberRepository()도 @Bean으로 등록했기 때문에 다시 수행되며 총 3번이 수행된다. 즉 총 3개의 새로운 객체가 생성되어 반환되는 것으로 보인다.
    • discountPolicy() 역시 2번 수행되며 2개의 새로운 객체가 생성되어 반환되는 것으로 보인다.
  • 이것은 스프링 컨테이너가 각 객체를 싱글톤으로 관리한다는 사실과 정면으로 부딪히는 주장이다. 그런데 검증을 해보면 정상적으로 스프링 컨테이너가 각 객체를 싱글톤으로 관리한다는 결과가 나온다.

 

 

바이트코드 조작

  • 왜 이런 상황이 발생함에도 불구하고 싱글톤으로 관리하는게 가능한지는 @Configuration이 붙은 AppConfig의 클래스 정보를 확인하면 알 수 있다.
  • 다음과 같은 코드를 통해 AppConfig의 클래스 정보를 확인해보자.
public class Example{
    public static void main(String[] args){
        AnnotationConfigApplicationContext ac 
            = new AnnotationConfigApplicationContext(AppConfig.class);
        
        AppConfig bean = ac.getBean(AppConfig.class);
        
        System.out.println("bean = " + bean.getClass());
    }
}
  • AnnotationConfigApplicationContext에 매개변수로 넘겨진 값도 스프링 Bean으로 등록된다. 그래서 지금 넘어간 AppConfig도 Bean으로 가져올 수 있다.
  • 이제 이 Bean을 조회해서 클래스 정보를 알아보자. 우리가 생각하는 것이 맞다면 class.hello.core.AppConfig와 같이 출력되어야 한다.
bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70

그런데 출력된 값이 다르다. 뒤에 어떤 값이 추가적으로 붙어있다.

  • 이것은 스프링에서 해당 클래스를 CGLIB이라는 바이트코드 조작 라이브러리를 통해 AppConfig 클래스를 상속받은 임의의 클래스를 만들어 그 클래스를 스프링 빈으로 등록한 것이다.
  • 즉 실제 AppConfig 클래스는 스프링 빈으로 등록되어있지 않다는 것이다.
  • AppConfig를 상속받은 클래스는 다음과 같이 구현되어있을 것으로 추측된다.
@Bean
public MemberRepository memberRepository(){
    if(memoryMemberRepository 가 스프링 컨테이너에 존재한다면){
        return 스프링 컨테이너에서 해당 객체 찾아서 반환
    } else{
        기존 로직을 호출하여 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
        
        return 생성된 객체;
    }
}
  • 즉 해당 객체가 이미 스프링 컨테이너에 존재하는지를 먼저 확인하고, 존재하지 않는 것이 확인되면 새로운 객체를 생성하여 컨테이너에 등록하고 해당 객체를 반환한다.
  • 만약 이미 존재한다면 해당 객체를 스프링 컨테이너에서 찾아서 반환한다.
  • 이렇게 보이지 않는 작용을 통해서 스프링 컨테이너는 싱글톤으로 관리하는 것을 실패하지 않을 수 있다.
  • 이 작용은 @Configuration이 붙은 클래스에 대해 적용되고 있기 때문에 이 어노테이션을 사용하지 않는다면 바이트코드 조작이 일어나지 않은 클래스가 등록되게 되고 위의 필터를 거치지 않게 되어 중복이 일어난다. 즉 싱글톤으로 관리되는것이 보장되지 않는다.
  • 따라서 스프링 컨테이너가 싱글톤으로 객체를 관리하기 위해서는 항상 @Configuration을 사용하여 설정 파일을 관리해야한다.
복사했습니다!