Spring/김영한 스프링 핵심원리 - 기본편

[섹션5-5] @Configuration과 싱글톤

보름달빵 2024. 2. 18. 18:27

 

 

스프링 컨테이너에서 빈을 싱글톤으로 등록하는 방법


AppConfig

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService(){

        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public static MemoryMemberRepository memberRepository() {

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

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

//        return new FixDiscountPolicy();
        return  new RateDiscountPolicy();
    }
}

 

 

AppConfig 기반의 스프링 컨테이너를 생성한다면  @Bean으로 등록된 메서드들을 한번씩 호출해서 빈으로 등록시킬 것이다. 

 

그런데 코드를 자세히 보면 memberService() 는 여러번 호출 된다. 

 

  • memberService 빈을 만드는 코드를 보면 memberRepository() 를 호출한다.
     이 메서드를 호출하면  new MemoryMemberRepository()를 호출한다.
  • memberRepository 빈을 만들기 위해 memberRepository( ) 를 호출한다.
  • orderService 빈을 만드는 코드도 동일하게 memberRepository()  를 호출한다.
     이 메서드를 호출하면 new MemoryMemberRepository() 를 호출한다. 

 

결과적으로 3번의 new MemoryMemberRepository를 통해 서로 다른 memberRepository 객체가 생성되는것 아닐까? 

하지만 스프링은 객체를 싱글톤으로 관리해준다고 했는데....

 

어떻게 여러번 메서드를 호출했는데 객체는 1개 일 수 있을까? 

 

 

 

 

스프링은 이 문제를 어떻게 해결해주는지 알아보자

 

 

 


 

 

 

 

 

먼저 테스트 코드를 통해 memberService 을 만들었을 때 생성된 memberRepository와 

orderService를 통해 만들어진 memberRepository 와 빈으로 등록된 memberRepository 가 모두 같은 객체인지 확인해보자 

 

 

ConfigurationSingletonTest

  • 테스트를 위해 MemberRepository를 조회할 수 있는 기능을 추가한다. 기능 검증을 위해 잠깐 사용하는 것이니 인터페이스에 조회하는 기능까지 추가하지는 말자
  • MemberServiceImpl 과 OrderServiceImpl 에 해당 클래스의 memberRepository 값을 알려주는 메서드를 추가해주자
public MemberRepository getMemberRepository() {
    return memberRepository;
}

 

public class ConfigurationSingletoneTest {

    @Test
    void ConfiguraionSingletonTest(){

        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("memberRepository = " + memberRepository);

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


    }

}
//실행결과

memberService -> memberRepositoryhello.core.member.MemoryMemberRepository@45efc20d
orderService -> memberRepositoryhello.core.member.MemoryMemberRepository@45efc20d
memberRepository = hello.core.member.MemoryMemberRepository@45efc20d

 

실행결과 3개의 memberRepository는 모두 같은 객체임을 알 수 있다. 

스프링은 객체를 싱글톤으로 관리한다고 했으므로 당연한 결과일 수 있다. 

 

 

하지만 AppConfig의 코드를 보면  new MemoryMemberRepository를 하니까 호출 할 때마다 새로운 인스턴스가 생성되어야 하는게 아닐까? 

 

어떻게 이런일이 가능할까? 

 

 

AppConfig에 호출 로그 남기기 

@Configuration
public class AppConfig {

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

    @Bean
    public  MemoryMemberRepository 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());
    }
    @Bean
    public DiscountPolicy discountPolicy(){

//        return new FixDiscountPolicy();
        return  new RateDiscountPolicy();
    }
}
  • 각각의 메서드가 몇 번 실행되는지 확인하기 위해 한번 실행될 때 마다 출력화면에 값이 나오도록 호출코드를 추가해주자

 

 

 

ConfigurationSingletonTest를 실행 시킨 결과를 예상해보자면 

 

먼저, 스프링 컨테이너가 각각 @Bean을 호출해서 스프링 빈을 생성한다. 

  • 스프링 컨테이너가 스프링 빈에 등록하기 위해 @Bean이 붙어있는memberRepository() 호출 
  • memberService() 로직에서 memberRepository() 호출 
  • orderService() 로직에서 memberRepository() 호출 
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository

 

물론 순서는 일치하지 않을 수 있지만, "call AppConfig.memberRepository" 가 세번 호출 되지 않을까? 

 

 

 

//실행결과

call AppConfig.memberService
16:47:35.516 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory --
                Creating shared instance of singleton bean 'memberRepository'
call AppConfig.memberRepository
16:47:35.518 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory --
                Creating shared instance of singleton bean 'orderService'
call AppConfig.orderService
16:47:35.520 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory --

 

call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService

 

 

 

실행결과 각각의 메서드들은 모두 한번씩만 호출되었다. 

 

 

어떻게 이런일이 가능할까?

 

코드를 보면  호출을 세 번하는데 왜  실제로는 한번만 호출될 수 있었던 걸까? 

 

 

이와 관련한 내용은 다음 글에서 정리해보자