@Configuration과 싱글톤

황준하·2023년 7월 26일

Spring 기본

목록 보기
22/38
post-thumbnail

@Configuration과 싱글톤

//@Bean memberService -> new MemoryMemberrepository() 를 호출한다. 객체가 생성된다.

//@Bean orderService -> new OrderServiceImpl()을 호출 -> new MemoryMemberRepository()를 또 호출한다.

결과적으로 각각 다른 2개의 MemoryMemberRepository가 생성되면서  싱글톤이 깨지는 것이 아닌가?

MemberServiceImpl

package hello.core.Member;

public class MemberServiceImpl implements MemberService{
	
    ...
    
    //테스트 용도
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

OrderServiceImpl

package hello.core.order;

import ...

public class OrderServiceImpl implements OrderService{

   ...

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

Test

package hello.core.singleton;

import ...
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 -> memberRepository = " + memberRepository1);
        System.out.println("orderService -> memberRepository = " + memberRepository2);
        System.out.println("memberRepository -> memberRepository = " + memberRepository);
        
        assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }
}

실행

memberService -> memberRepository = hello.core.Member.MemoryMemberRepository@7d9e8ef7
orderService -> memberRepository = hello.core.Member.MemoryMemberRepository@7d9e8ef7
memberRepository -> memberRepository = hello.core.Member.MemoryMemberRepository@7d9e8ef7

add on-demand static import => alt Enter

확인해보면 memberRepository 인스턴스는 모두 같은 인스턴스가 공유되어 사용된다.

혹시 두번 호출이 안되는 것일까?

Appconfig

package hello.core;

import ...

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        System.out.println("call AppConfig.memberService");      //soutm
        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());
    }

	...
}

System.out.println("AppConfig.memberService");      //soutm

실행

...

call AppConfig.memberService
21:18:51.765 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberRepository'
call AppConfig.memberRepository
21:18:51.767 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderService'
call AppConfig.orderService

...

그러나 출력결과는 모두 1번만 호출된다.

@Configuration과 바이트코드 조작

  • 스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다. 그런데 스프링이 자바 코드까지 어떻게 하기는 어렵다.
  • 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다.
package hello.core.singleton;

import ...
public class ConfigurationSingletonTest {

    @Test
    void configurationTest() {
    
       ...

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

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

실행

...

bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$5218c930

순수한 클래스라면 'class hello.core.AppConfig' 와 같이 출력되야 한다.

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

그 임의의 다른 클래스가 싱글톤이 보장되도록 해준다. 아마도 다음과 같이 바이트 코드를 조작해서 작성되어 있을 것이다.

(실제 CGLIB의 내부 기술은 매우 복잡하다.

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

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

//@Configuration
public class AppConfig {

Test

...
call AppConfig.memberService
call AppConfig.memberRepository
...
call AppConfig.memberRepository
...
call AppConfig.orderService
...
call AppConfig.memberRepository
...
bean = class hello.core.AppConfig

configurationTest()

memberService -> memberRepository = hello.core.Member.MemoryMemberRepository@2bdd8394
orderService -> memberRepository = hello.core.Member.MemoryMemberRepository@5f9edf14
memberRepository -> memberRepository = hello.core.Member.MemoryMemberRepository@68746f22

이 출력결과를 통해서 MemberRepository가 총 3번 호출된 것을 알 수 있다.

  • @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
  • memberRepository() 처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱클톤을 보장하지 않는다.
  • 스프링 설정 정보는 항상 @Configuration을 사용하자.

0개의 댓글