Spring

스프링 컨테이너와 싱글톤(feat. @Configuration)

쭈녁 2023. 12. 28. 20:07

 

AppConfig 설정

@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 FixDiscountPolicy();
        return new RateDiscountPolicy();
    }

(Configration의 설정값)

 

@Configration을 사용하여 의존관계의 설정을 하고 ApplicationContext 에 해당 설정값을 넣으면 스프링 컨테이너 안에 있 스프링 컨테이너에서는 아래 그림과 같이 빈 저장소 @Bean 어노테이션으로 등록된 메서드와 클래스를 관리한다.

스프링 컨테이너 안의 빈 저장소

 

단순한 자바코드의 설정값과 다를게 없어 보인다. 하지만 @Configuration은 싱글톤을 보장하는데 큰 역할을 한다.

@Bean 혹은 @Component로 설정하면 Sping이 싱글톤을 보장한다고 알고 써 왔었다. 싱글톤을 보장하긴 한다.

하지만 위와 같은 AppConfig의 코드처럼 서로의 생성자로 관계가 얽혀있는 상황에서는 이야기가 다르다.

 

아래 테스트 코드는 AppConfig 클래스에서 @ Configuration 어노테이션을 빼고 돌려본 테스트이다.

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

@Test
@DisplayName("Configuration 어노테이션 없이 테스트")
void configTest() {
    MemberRepository findBean1 = ac.getBean("memberRepository", MemberRepository.class);
    MemberRepository findBean2 = ac.getBean("memberRepository", MemberRepository.class);
    System.out.println("findBean1 = " + findBean1);
    System.out.println("findBean2 = " + findBean2);

    Assertions.assertThat(findBean1).isEqualTo(findBean2);
}

 

같은 주소값을 반환하는데 싱글톤을 보장하는 것이 아닌가?? 라는 생각을 할 수 있다. 하지만 아래와 같이 테스트 했을 때는 두 객체의 주소값이 다른 걸 확인 할 수있다.

 

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);


@Test
@DisplayName("Configuration 어노테이션 없이 테스트2")
void configTest2() {
    MemberServiceImpl findMemberService = ac.getBean("memberService", MemberServiceImpl.class);

    MemberRepository findBean1 = findMemberService.getMemberRepository();
    MemberRepository findBean2 = ac.getBean("memberRepository", MemberRepository.class);

    System.out.println("findBean1 = " + findBean1);
    System.out.println("findBean2 = " + findBean2);

    Assertions.assertThat(findBean1).isEqualTo(findBean2);
}

 

@Bean으로 등록된 MemberRepository가 싱글톤으로 생성되지 않은 것을 확인 할 수있다.

이유는 MemberService의 생성자는 memberRepository() 메서드를 통해 MemoryMemberRepository 객체를 생성해 주입한다. 때문에 위 테스트 케이스에서는 memberRepository() 메서드가 2번 실행된 것이다. 이는 객체를 생성하는 각 메서드에서만 싱글톤을 보장한다는 뜻이다.

public class AppConfig {
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());	
        
        /*MemberServiceImpl 객체를 생성할 때 memberRepository() 생성자로 
        MemoryMemberRepository 객체를 만들어 주입 받음*/
    }

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

 

AppConfig에서 호출될 때마다 print를 찍어 확인 해보면 객체를 생성하는 메서드가 한번만 호출되는것이 아니라 해당 객체를 생성하는 메서드 내부에서 계속 호출하는 것을 볼 수있다. 

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

    @Bean
    public MemberRepository 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 DiscountPolicy discountPolicy() {
        System.out.println("AppConfig.discountPolicy");
        return new RateDiscountPolicy();
    }
}

 

참조된 생성 메서드가 참조된 만큼 호출됨

 

@Configuration 어노테이션을 적용하고 다시 테스트를 돌려보았다.

 

@Configuration
public class AppConfig {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

@Test
@DisplayName("Configuration 어노테이션 넣어 테스트")
void configTest3() {
    MemberServiceImpl findMemberService = ac.getBean("memberService", MemberServiceImpl.class);

    MemberRepository findBean1 = findMemberService.getMemberRepository();
    MemberRepository findBean2 = ac.getBean("memberRepository", MemberRepository.class);

    System.out.println("findBean1 = " + findBean1);
    System.out.println("findBean2 = " + findBean2);

    Assertions.assertThat(findBean1).isEqualTo(findBean2);
}

 

 

두개의 MemberRepository 객체가 같은 주소를 참조하고 있고(싱글톤 유지) 각 객체들이 한번씩 생성 메서드를 통해 생성됨을 볼 수 있다.

 

스프링은 객체의 싱글톤을 보장하기 위해 CGLIB 라는 Proxy 객체를 앞에 두고 AppConfig를 상속 받아 스프링 컨테이너에 등록해준다. 때문에 생성 메서드간에 중복 없이 싱글톤을 보장해 준다.