반응형

스프링은 기본적으로 싱글톤 객체를 생성하고 관리 해준다. 물론 싱글톤이 아니도록 설정할 수도 있다.
하지만 거의 99%는 싱글톤으로 사용을 한다.
그렇다면 스프링은 어떻게 싱글톤을 보장해 주는 것일까? 지금부터 알아 보도록 하자.

자바 코드가 객체 생성을 한 번만 하는게 이상하다.


아래와 같은 빈을 등록한다고 하자.

@Configuration
public class AppConfig {

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

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

순수하게 자바 코드로만 봤을 때는 해당 빈은 memberRepository가 3번 생성 되어야 한다.
물론 스프링이 싱글톤으로 보장 해준다고 했지만, 그래도 어떻게 될까? 테스트를 해보는 것이다.

ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class) 이 코드
로 AppConfig와 메서드들은 스프링 빈으로 등록이 된다.
그런데 분명 memberRepository는 memberService()에서 1번, orderService()에서 1번, memberRepository()에서 1번하여
총 3번이 호출되고, 3개의 다른 인스턴스가 생성되어야 맞다.

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

    MemberServiceImpl memberService = ac.getBean(MemberServiceImpl.class);
    OrderServiceImpl orderService = ac.getBean(OrderServiceImpl.class);

    MemoryMemberRepository realMemberRepository = ac.getBean(MemoryMemberRepository.class);
    MemberRepository memberRepository1 = memberService.getMemberRepository();
    MemberRepository memberRepository2 = orderService.getMemberRepository();

    assertThat(memberRepository1).isSameAs(memberRepository2);
    assertThat(realMemberRepository).isSameAs(memberRepository1);
}

/*
AppConfig의 로그 결과를 봐도 이렇게 1번만 호출을 한다.

call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
*/

하지만 결과적으로 이 테스트는 통과한다. 의심을 했지만 역시 스프링은 싱글톤으로 잘 관리를 해주는 것이다.

그렇다면 어떻게 싱글톤으로 관리가 되지?


코드상으로는 싱글톤과 관련된 코드가 어디에도 보이지 않는다.

그렇다면??

스프링은 바이트 코드를 조작하는 라이브러리를 사용한다. 바로 CGLIB 라이브러리이다.

테스트를 해보자.

new AnnotationConfigApplicationContext(AppConfig.class)을 하게 되면 @Bean뿐 아니라 @Configuration이 붙은
클래스도 스프링 빈으로 등록된다. 즉, AppConfig도 빈으로 등록해서 하위 타입으로 새로운 가짜 객체를 생성하여 사용하는 것이다.
로그 결과를 보면 클래스명뒤에 CGLIB이 붙어있는데 이것이 바로 가짜 AppConfig를 등록해서 사용한다는 것이다.
순수 AppConfig였다면 로그 결과는 class hello.core.AppConfig 였을 것이다.

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

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

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

// 로그 : bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$dfea0dbb

정리

  • 실제 객체가 아닌 가짜 객체(하위 타입)를 만들어 스프링 빈으로 등록한다. 이걸 CGLIB 객체라고 한다.
    하위 타입이기 때문에 ac.getBean(AppConfig.class)로 CGLIB 빈이 조회가 되는 것이다.
  • CGLIB 객체에서 바이트코드를 조작하여 싱글톤을 유지하는 것이다.

예상되는 AppConfig의 CGLIB코드는 아래와 같다.

@Bean
public MemberRepository memberRepository() {
        if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) { 
                return 스프링 컨테이너에서 찾아서 반환;
        } else { //스프링 컨테이너에 없으면
                기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록 return 반환
        } 
}
  • @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고,
    스프링 빈이 없다면 생성해서 스프링 빈으로 등록한 후 반환하는 코드이다.

@Configuration이 없고, @Bean만 있다면 어떻게 될까?


@Configuration을 제거하고 테스트를 해보자.

public class AppConfig {

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

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

먼저 AppConfig를 보면 CGLIB이 아닌 순수 AppConfig가 등록된 것을 로그로 확인할 수 있다.
memberRepository 호출 횟수를 봐도 3번이 호출된 것도 확인할 수 있다.
1번은 memberRepository()을 @Bean에 의해 스프링 빈으로 등록한 것이고, 나머지 2번은
memberService()와 orderService()에서 memberRepository()를 호출한 것이다.

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

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

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

/*
bean = class hello.core.AppConfig
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
*/

즉, @Bean이 붙은 memberRepository() 메서드의 MemoryMemberRepository는 싱글톤 빈으로 등록이 되고,
나머지 2번은 순수 자바 객체를 호출한 것이므로 스프링 빈으로 등록 자체가 안된 MemoryMemberRepository를 주입한 것이다.
결국 memberService와 orderService에 있는 MemoryMemberRepository는 스프링 빈 자체가 아닌 것이다.

추가 적으로 실제로 3개의 인스턴스가 모두 다른지 확인 해보자.
실제로 다른 인스턴스이고, 스프링 빈으로 등록된 것은 realMemberRepository뿐인 것이다.

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

    MemberServiceImpl memberService = ac.getBean(MemberServiceImpl.class);
    OrderServiceImpl orderService = ac.getBean(OrderServiceImpl.class);

    MemoryMemberRepository realMemberRepository = ac.getBean(MemoryMemberRepository.class);
    MemberRepository memberRepository1 = memberService.getMemberRepository();
    MemberRepository memberRepository2 = orderService.getMemberRepository();

    System.out.println("memberRepository1 = " + memberRepository1);
    System.out.println("memberRepository2 = " + memberRepository2);
    System.out.println("realMemberRepository = " + realMemberRepository);
}

/*
memberRepository1 = hello.core.member.MemoryMemberRepository@39655d3e
memberRepository2 = hello.core.member.MemoryMemberRepository@34f22f9d
realMemberRepository = hello.core.member.MemoryMemberRepository@3d1848cc
*/

정리

  • @Bean만 사용해도 스프링 빈으로 등록은 되지만, 싱글톤 보장은 되지 않는다.
    • memberRepository()처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다는 것이지
      @Bean이 달린 memberRepository()의 MemoryMemberRepository는 싱글톤 빈으로 등록이 되는 것이다.
  • 결국 그냥 @Configuration을 붙여라
반응형
복사했습니다!