BackEnd/Spring

@Configuration과 싱글톤 (with. CGLIB)

PgmJUN 2024. 4. 11. 01:45

 

 

@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();
     }
}

해당 코드는 OrderService, MemberService, MemberRepository 빈(Bean)을 정의하는 설정 파일의 코드이다.

이렇게 코드를 작성해둔다면 @Bean 애노테이션에 의해 스프링 빈으로 등록되고 싱글톤으로 관리된다.

 

그런데 이상한 부분이 있다.

MemberRepository 를 빈으로 등록하기 위해 return new로 구현체를 생성해주는 것은 알겠지만,
memberRepository() 메서드를 orderService(), memberService() 에서도 호출해주고 있으니
MemoryMemberRepository를 3번 생성해주는 것이 아닐까?

정말 싱글톤으로 관리가 되고 있는 것일까?

 

 

싱글톤 테스트

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

@SpringBootTest
public class AppConfigTest {

    @Test
    @DisplayName("싱글톤 테스트")
    void singleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository1 = orderService.getMemberRepository();

        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        MemberRepository memberRepository2 = memberService.getMemberRepository();

        Assertions.assertThat(memberRepository).isSameAs(memberRepository1);
        Assertions.assertThat(memberRepository).isSameAs(memberRepository2);
    }
}

 

위 코드는 memberRepository가 싱글톤이 맞는 지 검증하는 테스트이다.

스프링 컨테이너에 존재하는 MemberRepository 빈과 OrderService, MemberService에 주입된 MemberRepository 가 같은 지 검사하는 테스트 코드로 구성되었다.

 

이 테스트 코드의 결과는 어떨까?

 

 

테스트 결과

 

테스트의 결과는 성공이다.

스프링이 MemberRepository를 싱글톤으로 관리할 수 있도록 처리해주었다는 얘기인데 과연 어떻게 그럴 수 있는 걸까?

 

 


 

@Configuration

그 이유는 바로 @Configuration에 있다.

 

스프링이 스프링 빈을 싱글톤으로 관리하도록 보장해준다고 하는데, 사실 스프링이 자바 코드까지 변경하기는 어렵다.

 

그래서 스프링은 바이트코드를 조작하는 CGLIB 라이브러리를 사용하는데

@Configuration 내에서 @Bean 을 통해 스프링 빈을 등록 시,
바이트코드 조작 라이브러리가 Config 객체의 코드를 조작하여 싱글톤을 보장하도록 코드를 변경시킨 CBLIB 객체를 생성하고
그 객체를 스프링 컨테이너에 빈으로 등록한다.

 

 

실제로 AppConfig의 class를 출력시켜보면 아래와 같이 출력되는 것을 확인할 수 있었다.

AppConfig appConfig = ac.getBean(AppConfig.class);
System.out.println(appConfig.getClass());
        
// 출력 값: class com.example.demo.AppConfig$$SpringCGLIB$$0
// 일반 객체의 경우, 이렇게 출력된다(class com.example.demo.AppConfig)

 

 

AppConfig가 CGLIB에 의해 컨테이너에 등록된 AppConfig@CGLIB 예상 코드를 보자면 아래와 같지 않을까 싶다. (실제로는 훨씬 복잡하게 구현되어있을 것이며, 이 코드는 간략한 예상 코드이다.)

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

 

 

 


 

@Bean 만 사용한다면?

 

그렇다면 스프링 빈을 등록을 위해 사용되는 @Bean 애노테이션만 사용한다면, 스프링 빈을 싱글톤으로 관리하기 어려울까?

한번 테스트해보자

 

public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

 

이전 AppConfig 코드에서 @Configuration 애노테이션을 제거했다.

 

 

테스트

그리고 아까와 똑같은 테스트를 다시 진행해보자

 

결과는 실패이다.

 

그럼 @Configuration 애노테이션이 없다면 어떻게 동작하는 것일까?

 

 

내부 동작

  • 우선 Configuration 애노테이션이 없으니 CGLIB 라이브러리가 바이트코드를 조작하지 않고 싱글톤을 보장하지 않는다.
  • 위와 같은 AppConfig 코드라면 memberRepository가 총 3개 생성되고 각각의 스프링 빈으로 등록된다.

 

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

 

memberRepository() 메서드에 위와 같이 로그를 추가하는 경우

 

 

총 3번의 memberRepository 생성이 발생하는 것을 알 수 있다.

 

 

@Configuration 애노테이션이 존재하는 경우에는 단 1번만 호출되는 것을 확인할 수 있었다.

 

 

 


결론

  • 컴포넌트 스캔 방식이 아닌 직접 Config 객체 선언하여 IoC 컨테이너를 구성하는 경우에는 @Configuration, @Bean 애노테이션을 통해 스프링 빈들의 싱글톤을 보장할 수 있다.
  • 스프링에서 @Configuration 내부에 @Bean 애노테이션을 통해 선언한 빈들이 싱글톤을 보장할 수 있는 것은 @Configuration 애노테이션에 의해 동작하는 CGLIB이라는 바이트 코드 조작 라이브러리에 의해서이다.
  • @Configuration 애노테이션 없이 @Bean 만 붙이더라도, 스프링 빈 등록은 되지만 싱글톤 보장은 되지 않는다.