오봉이와 함께하는 개발 블로그

Spring - 싱글톤 컨테이너(@Configuration과 싱글톤, Configuration과 바이트코드 조작의 마법) 본문

BE/Spring

Spring - 싱글톤 컨테이너(@Configuration과 싱글톤, Configuration과 바이트코드 조작의 마법)

오봉봉이 2022. 6. 8. 18:47
728x90

@Configuration과 싱글톤

  • AppConfig를 보면 이상한 점이 있다.
  • memberService 빈을 만드는 코드를 보면 memberRepository()를 호출
    • memberRepository()를 호출하면 new MemoryMemberRepository()를 호출한다.
  • orderService 빈을 만드는 코드도 동일하게 memberRepository() 를 호출한다.
    • memberRepository()를 호출하면 new MemoryMemberRepository() 를 호출한다.
  • 각각 다른 두 개의 MemoryMemberRepository가 생성되며 싱글톤이 깨지는 것 처럼 보이는데 스프링 컨테이너는 이 문제를 어떻게 해결할까??
@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

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

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

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

검증

  • 테스트를 위해 MemberRepository를 조회할 수 있는 기능을 추가한다. 기능 검증을 위해 잠깐 사용하는 것이니 인터페이스에 조회기능까지 추가하지는 말자.
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;

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

public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    // 테스트 용도
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}
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("memberService -> memberRepository1 = " + memberRepository1);
        System.out.println("orderService -> memberRepository2 = " + memberRepository2);
        System.out.println("memberRepository = " + memberRepository);

        assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }
}
memberService -> memberRepository1 = hello.core.member.MemoryMemberRepository@4bff64c2
orderService -> memberRepository2 = hello.core.member.MemoryMemberRepository@4bff64c2
memberRepository = hello.core.member.MemoryMemberRepository@4bff64c2
  • 확인해보면 memberRepository 인스턴스는 모두 같은 인스턴스가 공유되어 사용됨.
  • AppConfig의 자바 코드를 보면 분명히 각각 2번 new MemoryMemberRepository 호출해서 다른 인스턴스가 생성되어야 하는데... 알아보도록 하자

검증

@Configuration
public class AppConfig {

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

    @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
    public MemoryMemberRepository memberRepository() {
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        // return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}
  • 위 주석과 같이 총 memberRepository는 3번이 호출되어야 하는 거 아닐까?
      1. 스프링 컨테이너가 빈에 등록하기 위해 @Bean이 붙어있는 memberRepository()호출
      1. memberService() 로직에서 memberRepository() 호출
      1. orderSerivce() 로직에서 memberRepository() 호출

하지만 결과는 모두 1번만 호출!

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

Configuration과 바이트코드 조작의 마법

  • 스프링 컨테이너는 싱글톤 레지스트리
    • 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 함
    • 위 자바 코드를 보면 분명 3번이 호출되어야 하는데...

스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다!
비밀은 @Configuration을 적용한 AppConfig에 있다

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

        System.out.println("been = " + been.getClass());
//      결과 -> been = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$993824f4
    }

순수한 클래스면 class hello.core.AppConfig라고 나와야 하는데 xxxCGLIB가 붙은 형태인 class hello.core.AppConfig$$EnhancerBySpringCGLIB$$993824f4라고 나온다.
이는 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해 AppConfig클래스를 상속받은 임의의 다른 클래스를 만들고 그 다른 클래스를 스프링 빈에 등록한 것

xxxCGLIB 그림

내가 만든 클래스가 아닌, 임의의 다른 클래스가 싱글톤을 보장해준다.

AppConfig@CGLIB 예상 코드

@Bean
  public MemberRepository memberRepository() {
    if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) { return 스프링 컨테이너에서 찾아서 반환;
    } else { //스프링 컨테이너에 없으면
    기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록 return 반환
    } 
}
  • @Bean이 붙은 메소드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환, 없으면 생성해서 등록하고 반환하는 코드가 동적으로 만들어짐.

@Configuration을 적용하지 않고, @Bean만 적용하면?

  • AppConfig에 @Configuration 삭제
bean = class hello.core.AppConfig
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
call AppConfig.memberRepository
memberService -> memberRepository1 = hello.core.member.MemoryMemberRepository@7d1cfb8b
orderService -> memberRepository2 = hello.core.member.MemoryMemberRepository@48e92c5c
memberRepository = hello.core.member.MemoryMemberRepository@2e1ef60
  • 테스트 코드도 실패하고, 다 다른 MemoryMemberRepository 인스턴스를 가지고 있다.

정리

  • @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤은 보장하지 않음
    • memberRepository()처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않음
  • 스프링 설정 정보는 항상 @Configuration을 사용하자.
출처 : 인프런 김영한 지식공유자님의 스프링 완전 정복 로드맵 강의
728x90
Comments