@Configuration과 싱글턴

@Configuration
public class AppConfig {

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

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

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

  @Bean
  public DiscountPolicy discountPolicy() {
    return new RateDiscountPolicy();
  }

}

memberServiceorderService 둘 다 new MemoryMemberRepository를 호출하는 memberRepository를 호출한다. 각각 다른 MemoryMemberRepository가 생성되면서 싱글턴이 깨지는 것처럼 보인다.

테스트 해보면 같은 참조값이 나온다. memberRepository 인스턴스가 모두 같은 인스턴스로 공유되고 있는 것이다.

혹시 메서드가 두 번 호출이 안 되는게 아닐까? 로그를 찍어보자.

스프링 컨테이너는 각각의 @Bean을 호출해서 스프링 빈을 생성한다. 따라서 @Bean이 붙어있는 memberRepository 1번에, memberServiceorderService에서 각각 호출하는 횟수 2번까지 총 3번이 찍혀야할 것 같다.

하지만 결과를 보면 신기하게도 한 번씩만 찍혔다.

@Configuration과 바이트 코드 조작의 마법

스프링 컨테이너는 싱글턴 객체를 생성하고 관리하는 싱글턴 레지스트리다. 따라서 스프링 빈이 싱글턴이 되도록 보장해야 한다.

스프링 빈 AppConfig의 클래스 정보를 출력하면 위와 같다. 순수한 클래스라면 class hello.core.AppConfig로 출력되어야 한다.

스프링이 CGLIB이라는 바이트 코드 조작 라이브러리로 AppConfig 클래스를 상속한 임의의 다른 클래스를 만들어 스프링 빈으로 등록한 것이다.

이 조작해서 만든 다른 클래스가 싱글턴이 되도록 보장한다.

바이트 코드를 저장하는 과정은 대략 이렇게 될 것이다.

@Bean이 붙은 메서드마다 스프링 빈이 있는지 확인한 뒤 있으면 그 빈을 반환하고 없으면 새로 생성해서 빈으로 등록하고 반환하는 코드가 동적으로 생성된다. 이 덕분에 싱글턴이 보장되는 것이다.

참고로 AppConfig@CGLIBAppConfig의 자식 타입이므로 이전의 테스트 코드에서 AppConfig로 조회할 수 있었던 것이다.

@Configuration을 적용하지 않는다면?

@Configuration을 붙이면 바이트 코드를 조작하는 CGLIB 기술로 싱글턴을 보장한다. 하지만 만약 @Bean만 적용한다면 어떻게 될까?

CGLIB 기술 없이 순수한 AppConfig로 스프링 빈에 등록된 것을 알 수 있다.

memberRepository가 3번 호출되고 각각이 다른 인스턴스로 생성되었다.

@Bean만 사용해도 스프링 빈으로 등록은 되지만, 싱글턴을 보장하지 않는다. 이전에는 CGLIB이 스프링 컨테이너에 있는지 확인하고 컨테이너에서 찾아 반환해줬다. 하지만 이제는 memberServiceorderService에서 의존하는 memberRepository는 더 이상 스프링 컨테이너가 관리하는 객체가 아니기 때문에 사실상 new MemoryMemberRepository()로 가져오는 것과 다름 없다.

따라서 스프링 설정 정보는 항상 @Configuration을 사용하자.

Last updated

Was this helpful?