본문 바로가기

SPRING/Spring

[스프링| 스프링 핵심 원리 | 기본편 | 싱글톤 컨테이너]@Configuration과 싱글톤

public class ConfigurationSingletonTest { // ConfigurationSingletonTest 클래스 선언

    @Test // JUnit 테스트 메서드임을 나타내는 어노테이션
    void configurationTest() { // 테스트 메서드 선언
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); // 스프링 컨텍스트를 초기화하고, AppConfig 클래스를 설정 정보로 사용

        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class); // "memberService" 빈을 MemberServiceImpl 타입으로 가져옴
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class); // "orderService" 빈을 OrderServiceImpl 타입으로 가져옴
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class); // "memberRepository" 빈을 MemberRepository 타입으로 가져옴

        // 모두 같은 인스턴스를 참고하고 있다.
        System.out.println("memberService -> memberRepository = " + memberService.getMemberRepository()); // memberService에서 사용하는 memberRepository 인스턴스를 출력
        System.out.println("orderService -> memberRepository  = " + orderService.getMemberRepository()); // orderService에서 사용하는 memberRepository 인스턴스를 출력
        System.out.println("memberRepository = " + memberRepository); // 직접 조회한 memberRepository 인스턴스를 출력

        // 모두 같은 인스턴스를 참고하고 있다.
        assertThat(memberService.getMemberRepository()).isSameAs(memberRepository); // memberService 내의 memberRepository가 직접 조회한 memberRepository와 같은지 확인
        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository); // orderService 내의 memberRepository가 직접 조회한 memberRepository와 같은지 확인
    }
}

이 테스트는 스프링이 AppConfig 클래스를 통해 제공하는 빈들이 모두 싱글톤으로 관리되고 있는지 확인합니다.

AppConfig에서 정의된 각 서비스(MemberServiceImpl, OrderServiceImpl)가 내부적으로 같은 MemberRepository 인스턴스를 사용하는지 테스트하여, 싱글톤 패턴의 일관성을 검증합니다.

스프링의 @Bean 어노테이션이 붙은 메서드는 싱글톤 빈을 생성하도록 스프링 컨테이너에 의해 관리되며, 이는 각 컴포넌트가 동일한 의존성 인스턴스를 공유함을 보장합니다.

 

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

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

@Configuration 애너테이션은 스프링 프레임워크에서 매우 중요한 역할을 하며, 주로 스프링 컨테이너에 빈(bean)을 정의하고 관리하는 설정 클래스에 사용됩니다. 이 애너테이션은 클래스가 스프링의 전체 설정을 담당하는 구성 클래스임을 나타냅니다. @Configuration이 적용된 클래스는 스프링 컨테이너에 의해 특별하게 처리되는데, 그 중에서도 싱글톤 보장 기능은 가장 중요한 기능 중 하나입니다.

@Configuration의 싱글톤 보장 기능

@Configuration 애너테이션은 스프링 컨테이너가 해당 클래스의 메서드를 호출할 때 일반 클래스가 아닌 CGLIB를 사용하여 클래스를 상속 받은 프록시 객체를 생성합니다. 이 프록시 객체를 통해 @Bean 메서드 호출이 관리되며, 이를 통해 스프링 컨테이너는 각 빈이 싱글톤으로 유지될 수 있도록 보장합니다.

예를 들어, AppConfig 클래스에 정의된 memberRepository() 메서드는 여러 빈에서 참조될 수 있습니다 (memberService(), orderService() 등). 만약 @Configuration 없이 일반 클래스로 처리된다면, memberRepository() 메서드가 호출될 때마다 새로운 MemberRepository 인스턴스가 생성될 수 있습니다. 그러나 @Configuration이 적용된 설정 클래스는 다음과 같이 작동합니다:

  1. 첫 호출 시 싱글톤 빈 생성: memberRepository() 메서드가 처음 호출될 때, 메모리에 MemberRepository 인스턴스를 생성하고, 이를 스프링 컨테이너의 싱글톤 캐시에 저장합니다.
  2. 이후 호출에서 캐시된 빈 반환: 동일한 설정 클래스 내에서 memberRepository() 메서드가 다시 호출될 때, 프록시는 이미 생성되어 스프링 컨테이너에 저장된 싱글톤 인스턴스를 반환합니다. 이를 통해 모든 @Bean 메서드가 동일한 인스턴스를 참조하게 되어 싱글톤이 보장됩니다.

이 프로세스 덕분에, AppConfig에 정의된 모든 빈은 애플리케이션 전체에서 유일하게 유지되며, 각 컴포넌트가 공유하는 자원에 대한 일관된 접근을 제공할 수 있습니다. 이는 리소스의 효율적 사용을 보장하고, 애플리케이션의 성능을 최적화하는 데 중요한 역할을 합니다.

public class ConfigurationDeepTest { // ConfigurationDeepTest 클래스 선언

    @Test // JUnit 테스트 메서드임을 나타내는 어노테이션
    void configurationDeep() { // 테스트 메서드 선언
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); // 스프링 컨텍스트를 초기화하고, AppConfig 클래스를 설정 정보로 사용하여 스프링 빈 컨테이너를 생성

        //AppConfig도 스프링 빈으로 등록된다.
        AppConfig bean = ac.getBean(AppConfig.class); // 스프링 컨텍스트에서 AppConfig 클래스의 인스턴스를 빈으로 가져옴

        System.out.println("bean = " + bean.getClass()); // AppConfig 인스턴스의 클래스 정보를 출력
        //출력: bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
    } // 이 출력은 AppConfig 클래스가 CGLIB에 의해 프록시된 것임을 나타냄, 즉 스프링이 AppConfig의 메서드 호출을 가로채 싱글톤을 보장하기 위해 프록시 객체를 생성
}

@Configuration의 주요 기능

  1. 빈(Bean) 설정과 관리: @Configuration이 적용된 클래스는 하나 이상의 @Bean 어노테이션이 달린 메서드를 포함할 수 있습니다. 이 메서드들은 스프링 컨테이너가 관리할 객체, 즉 빈을 생성하고 구성하는 역할을 합니다. @Bean 어노테이션이 붙은 메서드는 각각의 반환 타입에 해당하는 객체를 스프링 컨테이너에 등록합니다.
  2. 싱글톤 보장: 스프링은 @Configuration이 붙은 클래스의 메서드 호출을 감시하여, 각 @Bean 메서드가 한 번만 호출되고 그 결과가 스프링 컨테이너 내에서 싱글톤으로 유지되도록 보장합니다. 이는 CGLIB 라이브러리를 사용해 클래스의 프록시를 생성함으로써 이루어집니다.
  3. 의존성 주입(Dependency Injection) 지원: @Configuration 클래스는 다른 빈에 대한 의존성을 선언적으로 주입하는 데 사용됩니다. 예를 들어, 한 메서드에서 생성한 빈을 다른 빈 생성 메서드에서 사용할 수 있습니다. 스프링은 이 의존성을 자동으로 해결하여 각 빈이 필요로 하는 의존성을 주입합니다.
  4. 통합 관리: @Configuration 클래스는 애플리케이션의 구성을 중앙에서 관리할 수 있도록 해 줍니다. 이를 통해 개발자는 애플리케이션의 다양한 부분에서 일관된 방식으로 의존성을 관리하고 서비스를 구성할 수 있습니다.