Development/Spring

스프링 컨테이너는 스프링 빈을 어떻게 싱글톤으로 관리할까?

BongChun 2022. 12. 14. 17:54
🔥 스프링 컨테이너가 이미 존재하는 객체 인스턴스를 반환하는 @Bean 을 어떻게 처리하는지 알아보도록 한다.

 

 

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemoryMemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    @Bean
    public RateDiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}

다음 예제는 @Configuration , @Bean 을 통해 스프링 컨테이너에 스프링 빈을 등록을 위한 자바 설정코드이다. 스프링은 싱글톤으로 객체를 관리한다고 했지만 위의 예시를 보면 memberRepository() 가 3번 호출되어 new 키워드를 통해 객체가 3번 생성되게 된다. 이것을 스프링은 어떻게 해결하여 싱글톤으로 관리하게 되는지 알아보도록 한다.

검증을 위한 코드 추가

public MemberRepository getMemberRepository() {
    return memberRepository;
}
  • 실제 같은 객체인지 확인을 위해 MemberServiceImplOrderServiceImplMemberRepository 를 반환하는 getter 메소드를 작성한다.

테스트 코드

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

        System.out.println("memberService -> memberRepository = " + memberService.getMemberRepository());
        System.out.println("orderService -> memberRepository = " + orderService.getMemberRepository());
        System.out.println("memberService = " + memberRepository);

        Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }
}

위의 예시 테스트코드는 통과하고 출력의 값을 확인해보면 같은 객체 인스턴스인 것을 알 수 있다. 스프링 빈을 등록할 때 AppConfig 안의 메서드들이 모두 한번씩 실행될텐데 어떻게 같은 인스턴스를 반환할 수 있는 것일까?

검증을 위한 코드 추가2

이번에는 AppConfig 에 호출 로그를 남겨 확인해보도록 하자.

@Configuration
public class AppConfig {

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

다음과 같이 AppConfig 에 호출 시 콘솔에 메소드가 출력되도록 코드를 추가하였다.

 

스프링 컨테이너는 @Bean 어노테이션이 붙은 메소드를 호출하여 반환하는 객체를 컨테이너에 등록한다. 그리고 컨테이너는 싱글톤으로 관리되기 때문에 같은 타입의 객체가 존재할 수 없다. 하지만 작성한 자바 설정코드에는 4개의 @Bean 이 있고 모두 실행된다면 new 키워드로 인해 각각의 객체가 생성될 것이라고 타당한 추측을 할 수 있다.

 

이런 의문을 품고 다시 테스트 코드를 실행해본 결과는 아래와 같다.

 

한번씩 메소드가 실행된다면 memberRepository() 가 3번 호출되어 3개의 객체 인스턴스가 생성되어야 하는데 1번만 호출되었다.

바이트코드 조작

@Bean 을 가진 메소드 뿐 아니라 AnnotationConfigApplicationContext 에 파라미터로 넘긴 AppConfig 도 스프링 빈으로 등록이 된다. 따라서 getBean() 을 통해 객체를 받아와 정보를 출력할 수 있다.

 

객체 정보를 출력해보면 위와 같은 형식으로 된 것을 알 수 있다. 우리가 정의하지 않은 xxxCGLIB와 같은 내부 클래스가 정의되어 있는 것을 볼 수 있는데 이것은 스프링이 CGLIB 이라는 바이트코드를 조작하는 라이브러리를 사용해서 AppConfig 를 상속받은 클래스를 작성하고, 상속한 클래스를 스프링 빈으로 등록한 것이다. 이것때문에 AppConfig 는 상속한 클래스의 부모 타입이기 때문에 AppConfig 타입으로도 조회가 가능하기도 하다.

 

내부의 코드가 어떻게 정의되어 있는지는 상당히 복잡해 간단히 설명을 한다면,

@Bean 이 붙은 메소드를 조회할 때 이미 컨테이너에 존재하는 스프링 빈이 있다면 존재하는 빈을 등록하고, 존재하지 않다면 기존의 메소드를 호출해서 반환되는 빈을 등록하는 로직으로 이루어졌을 가능성이 높다.

 

이런 로직을 적용하기 때문에 싱글톤으로 관리가 되는 것이다.

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

결과적으론 @Bean 만을 사용해도 스프링 빈으로 등록을 할 수 있다. 하지만 @Configuration 을 사용하지 않을 경우 싱글톤으로 관리되는 것을 보장하지 않는다.

 

순수한 자바 코드로 작동하기 때문에 이 글의 처음 부분에 의문을 가졌던 new 키워드를 통한 객체 인스턴스 생성이 메소드마다 호출될 것이다. 따라서 특별한 경우가 아니라면 스프링 빈을 등록할 때는 무조건 @Configuration 을 사용하는 것이 좋다.

 

 

> reference

[스프링 핵심 원리 - 기본편] - 김영한

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8