BackEnd/스프링 핵심 원리 - 기본편

@Configuration과 싱글톤

인프라 감자 2023. 2. 3. 22:17

@Configuration과 싱글톤

@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();
    }
}
  • memberService를 호출하게 되면 memberRepository가 호출되고 MemoryMemberRepository 가 호출 된다. 
  • orderService를 호출하게 되면 memberRepository가 호출 되고 MemoryMemberRepository 가 호출 된다. 

분명 싱글톤 인데 호출이 여러 개 된다. 이것은 싱글톤이 깨지는 걸까? 

@Test
public 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);

    //모두 같은 인스턴스를 참고하고 있다.
    System.out.println("memberService -> memberRepository = " + memberService.getMemberRepository());
    System.out.println("orderService -> memberRepository = " + orderService.getMemberRepository());
    System.out.println("memberRepository = " + memberRepository);

    //모두 같은 인스턴스를 참고하고 있다.
    assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
    assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}

Test 해보기 위해 각각 만들어서 출력해보 앗다.

신기하게도 모두 똑같이 출력되었다.

 

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

AppConfig 파일을 이렇게 변경해서 출력되는 과정을 보기로 했다.

  • 스프링 컨테이너가 스프링 빈에 등록하기 위해 @Bean이 붙어있는 memberRepository() 호출
  • MemberService에서 MemberRepository를 호출했으므로 "call AppConfig.memberRepository" 출력
  • orderService() 로직에서 memberRepository() 호출

총 "call AppConfig.memberRepository"를 3번 출력할 거 같다.

  1. MemberService를 호출 했으므로 "call AppConfig.memberService" 출력
  2. memberRepository() 호출 했으므로 "call AppConfig.memberRepository" 출력
  3. 등록해야 하므로 "call AppConfig.memberRepository" 출력
  4. orderService를 호출해야 하므로 "call AppConfig.orderService" 출력
  5. 마지막으로 "call AppConfig.memberRepository"

이렇게 출력될 것 같다. 

하지만 신기하게도 한 번씩만 출력된다!!! 왜 그런 것일까???

 

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

스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다. 그런데 스프링이 자바 코드까지 어떻게 하기는 어렵다. 저 자바 코드를 보면 분명 3번 호출되어야 하는 것이 맞다. 그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다. 모든 비밀은 @Configuration을 적용한 AppConfig에 있다.

 

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

        //AppConfig도 스프링 빈으로 등록된다.
        AppConfig bean = ac.getBean(AppConfig.class);

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

AnnotationConfigApplicationContext에 파라미터로 넘긴 값은 스프링 빈으로 등록된다. 그래서 AppConfig 도 스프링 빈이 된다.

그래서 AppConfig 스프링 빈을 조회해서 클래스 정보를 출력해 봤다. 

신기하게도 AppConfig 뒤에 다른 문자들이 붙어있다.

 

그런데 예상과는 다르게 클래스 명에 xxxCGLIB가 붙으면서 상당히 복잡해진 것을 볼 수 있다.

이것은 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다.

즉, AppConfig가 AppConfig@CGLB라는 것을 하나 만들어서(상속받아서) 스프링 컨테이너에 집어넣는다. 이름은 appConfig이지만 istance는 AppConfig@CGLIB이다!! 

이 임의의 다른 클래스가 바로 싱글톤이 보장되도록 해준다.

 

  • AppConfig@CGLIB 예상 코드
@Bean
public MemberRepository memberRepository() {
 
 	if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
	    return 스프링 컨테이너에서 찾아서 반환;
    } else { //스프링 컨테이너에 없으면
 	   기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
 	   return 반환
 	}
}

 

이 코드는 강사님이 설명해 주실 때 사용하신 AppConfig@CGLIB 예상 코드이다.

만약 memoryMemberRepository가 스프링 컨테이너에 존재하지 않으면 바로 스프링 컨테이너에 등록한다. 하지만 존재하면 스프링 컨테이너에서 찾아서 반환한다. 이렇게 하기 때문에 각각 제일 처음 스프링 컨테이너에 등록할 때 만 호출된 것이다. 덕분에 싱글톤이 보장되는 것이다.

AppConfig@CGLIB는 AppConfig의 자식 타입이므로, AppConfig 타입으로 조회할 수 있다.

 

@Configuration 을 적용하지 않고, @Bean 만 적용하면 어떻게 될까?

@Configuration 을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장을 한다.

하지만 Bean만 실행하면 bean = class hello.core.AppConfig이 출력 된다.

이 5가지가 모두출력 되게 된다.  이 출력 결과를 통해서 MemberRepository가 총 3번 호출된 것을 알 수 있다. 1번은 @Bean에 의해 스프링 컨테이너에 등록하기 위해서이고, 2번은 각각 memberRepository() 를 호출하면서 발생한 코드다.

싱글톤이 깨지게 된다!!

 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com