본문 바로가기
WEB/Spring

[Spring] @Configuration과 싱글톤 패턴의 관계

by 정권이 내 2023. 12. 13.

싱글톤 패턴 조건

Bean 설정 정보를 구현한 AppConfig 클래스에서 memberRepository 메서드는 MemoryMemberRepository 클래스의 인스턴스를 반환합니다.

그렇다면 memberService, orderService 메서드에서 memberRepository 메서드를 호출할때 마다 새로운 인스턴스를 생성하여 싱글톤 패턴이 안될것 같은데 스프링에서 어떻게 싱글톤 패턴을 유지하는지 알아 보겠습니다.

 

AppConfig.java

@Configuration
public class AppConfig {

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

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

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

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

테스트 코드

테스트 코드로 MemoryMemberRepository 클래스의 인스턴스가 같은지 확인해보겠습니다. 우선 테스트를 위해 member, order의 구현 클래스에 MemberRepository를 반환해주는 메서드를 생성합니다.

 

MemberServiceImpl.java

public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository;

	...
	...

    //Singleton 테스트
    public MemberRepository getMemberRepository(){
        return memberRepository;
    }
}

 

OrderServiceImpl.java

public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

	...
	...

    //Singleton 테스트
    public MemberRepository getMemberRepository(){
        return memberRepository;
    }
}

 

ConfigurationSingletonTest.java

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);

        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();

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

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

 

결과

memberRepository = com.example.springdemostudy.member.MemoryMemberRepository@7728643a
memberService -> memberRepository1 = com.example.springdemostudy.member.MemoryMemberRepository@7728643a
orderService -> memberRepository1 = com.example.springdemostudy.member.MemoryMemberRepository@7728643a

 

MemberServiceImpl, OrderServiceImpl 클래스에서 생성한 MemberRepository 클래스 인스턴스의 값이 동일한 것을 확인할 수 있습니다.

테스트 코드에서 memberService, orderService, memberRepository 객체를 생성할 때 MemberRepository 클래스에 총 3번 접근을 하게 되는데 실제로 그렇게 되는지 AppConfig 클래스를 수정하여 테스트해보겠습니다.

 

AppConfig.java

@Configuration
public class AppConfig {

    @Bean
    public MemberRepository memberRepository() {
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

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

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

    @Bean
    public OrderService orderService() {
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
}

 

결과

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

예상대로라면 call AppConfig.memberRepository를 3번 출력해야 하지만 한 번만 출력 되었습니다. 이유는 @Configuration에 있습니다.

@Configuration 어노테이션

@Configuration 어노테이션에 대해 설명하기 전에 우선 ConfigurationSingletonTest 클래스에 새로운 테스트 메서드를 추가해보겠습니다.

 

ConfigurationSingletonTest.java

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

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

 

결과

bean = class com.example.springdemostudy.AppConfig$$EnhancerBySpringCGLIB$$d918027a

일반적인 클래스라면 com.example.springdemostudy.AppConfig 라는 이름으로 출력되지만 클래스명에 CGLIB라는 문자가 붙어서 나오는 것을 확인할 수 있습니다.

스프링에서는 싱글톤 패턴을 보장하기 위해 @Configuration 어노테이션 설정시 사용자가 만든 클래스를 스프링 빈에 등록하는 것이 아니라 해당 클래스를 상속받은 임의의 클래스를 만들고 그 클래스를 스프링 빈으로 등록합니다.

img

 

스프링의 CGLIB 기술에 대해 자세한 설명을 할 수는 없지만 쉽게 생각해서 @Bean 어노테이션이 선언된 메서드가 스프링 컨테이너에 이미 존재하면 해당 스프링 빈을 반환하고 없으면 새로 생성하여 스프링 빈으로 등록하는 방식으로 동작하기 때문에 싱글턴 패턴이 보장되는 것입니다.

 

만약 AppConfig 클래스에 @Configuration 어노테이션이 없으면 실행에는 문제가 없지만 싱글톤 패턴을 유지할 수 없습니다.

@Bean 선언은 되있기 때문에 각 메서드들이 스프링 빈으로 등록되지만 memberRepository에서 new 키워드로 MemoryMemberRepository 객체를 생성할 때마다 스프링 빈에 등록되기 때문에 싱글톤 패턴을 유지할 수 없습니다.

 

AppConfig 클래스에서 @Configuration을 주석 처리하고 configurationTest 메서드를 실행해보았습니다.

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

memberRepository = com.example.springdemostudy.member.MemoryMemberRepository@551a20d6
memberService -> memberRepository1 = com.example.springdemostudy.member.MemoryMemberRepository@411341bd
orderService -> memberRepository2 = com.example.springdemostudy.member.MemoryMemberRepository@4c4d362a

 

call AppConfig.memberRepository는 싱글톤 패턴이 깨졌기 때문에 3번 호출되었고 각 클래스 객체의 이름도 모두 다르게 출력되는 것을 확인할 수 있습니다. 결론으로 스프링 설정 정보 클래스에는 항상 @Configuration을 등록하면 됩니다.

 

Spring 연관 포스팅

반응형

댓글