본문 바로가기
[inflearn] 스프링 핵심 원리 - 기본편/섹션 5 - 싱글톤 컨테이너

@Configuration과 싱글톤

by 슬픈 야옹이 2024. 4. 20.

 

스프링 컨테이너는 기본적으로 스프링 빈을 싱글톤으로 관리해주는 싱글톤 레지스트리 기능을 제공한다.

 

어떻게 하는걸까?

 

다음 AppConfig 클래스를 살펴보자.

package hdxian.hdxianspringcore;

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

// 애플리케이션 전체를 설정하고 구성함.
// 앞으로 애플리케이션에 대한 환경 설정은 모두 이 클래스에서 수행한다.
@Configuration
public class AppConfig {

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

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


}

 

스프링 컨테이너는 @Bean 어노테이션이 붙은 메서드들을 실행해서 리턴되는 객체들을 스프링 빈으로 관리한다.

 

AppConfig에 선언된 메서드들을 보면, memberRepository()는 여기저기서 실행되므로 MemoryMemberRepsitory 객체는 여러개 생성돼야 정상이다.

.

 

스프링 빈 출력 테스트

테스트코드를 짜서 스프링 빈을 출력해보면, 그럼에도 불구하고 싱글톤으로 잘 관리되고 있음을 알 수 있다.

테스트를 위해 MemberServiceImpl, OrderServiceImpl에 getMemberRepository() 메서드를 임의로 추가했다.

import hdxian.hdxianspringcore.AppConfig;
import hdxian.hdxianspringcore.member.MemberRepository;
import hdxian.hdxianspringcore.member.MemberServiceImpl;
import hdxian.hdxianspringcore.order.OrderServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ConfigurationSingletonTest {

    @Test
    void configurationTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        // 편의상 구현 클래스에만 따로 getMemberRepository()를 정의했기 때문에 Impl 형태로 가져옴.
        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository repo = ac.getBean("memberRepository", MemberRepository.class);

        MemberRepository repo1 = memberService.getMemberRepository();
        MemberRepository repo2 = orderService.getMemberRepository();

        // 둘이 같은 객체를 참조하고 있음. 즉 MemberRepository 빈은 싱글톤으로 관리되는 중.
        System.out.println("repo1 = " + repo1);
        System.out.println("repo2 = " + repo2);

        // 얘도 위 2개 객체랑 같은 인스턴스임. 싱글톤임.
        System.out.println("repo = " + repo);

        Assertions.assertThat(memberService.getMemberRepository()).isEqualTo(repo);
        Assertions.assertThat(orderService.getMemberRepository()).isEqualTo(repo);
    }

}

 

 

테스트 결과. 같은 MemoryMemberRepository 객체가 리턴된다. (싱글톤)

테스트 결과

 

 

위의 AppConfig 클래스에도 역시 테스트를 위해 각 메서드에 print문을 추가했다.

테스트 결과, memberRepository()는 한 번만 호출되었음을 알 수 있다.

한 번만 호출된다.

 

 

스프링도 결국 자바 기반의 프레임워크기 때문에, 근본적인 자바 코드의 실행을 조작할 수는 없다.

근데도 memberRepository()는 한 번만 실행되었다. 뭔가 다른 수를 쓴거다. 

 

 

 

@Configuration과 싱글톤

스프링 컨테이너는 스프링 빈의 싱글톤을 보장하기 위해 바이트코드 조작 라이브러리를 활용한다.

 

관련 작업은 @Configuration을 통해 설정되는데, AppConfig 클래스 내용을 찍어보면 다음과 같다.

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

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

 

테스트 결과

 

찍힌 클래스 정보를 보면, 일반적인 클래스 정보가 아니다.

평범한 AppConfig 클래스였다면 AppConfig@aa11bb22 등의 평범한 정보가 나와야 한다.

 

@Configuration 어노테이션이 붙은 AppConfig 클래스는,

스프링 빈으로 등록될 때 AppConfig의 인스턴스 그대로 등록되지 않는다.

 

CGLIB이라는 바이트코드 조작 라이브러리를 이용해 AppConfig의 자식 클래스를 하나 생성하고,

그 클래스를 스프링 빈으로 등록한다.

AppConfig의 스프링 빈 등록 과정 (강의자료 발췌)

 

 

이러한 과정으로 생성된 자식 클래스가, 다른 스프링 빈들을 싱글톤으로 관리하는 코드를 동적으로 추가하여 스프링 빈을 생성하는 원리다.

AppConfig@CGLIB의 예상 의사 코드 (강의자료 발췌)

 

 

+)

AppConfig@CGLIB은 AppConfig의 자식 클래스이므로 테스트 코드에서 조회가 가능하다.

 

 

 

@Configuration 제거

AppConfig 클래스의 @Configuration을 제거(주석처리)하고 테스트를 돌려보면, memberRepository() 메서드가 다시 여러번 호출되는 것을 확인할 수 있다.

테스트 결과

 

당연히 테스트도 실패한다.

 

 

결론

@Bean만 사용해도 스프링 빈으로 등록은 된다.

하지만 memberRepository()처럼 의존성에 의한 메서드 호출에서 싱글톤을 보장하지 않는다.

-> (새 인스턴스가 생성됨. 이렇게 생성된 인스턴스는 스프링 빈도 아님.)

 

싱글톤 보장은 @Configuration의 역할이다.

 

근데 @Configuration을 쓸지 말지 고민하는 경우가 있을까?

그냥 원리만 이해하고, @Configuration은 항상 붙이면 된다.