본문 바로가기

WEB/Spring

@Configuration의 싱글톤 보장

 

스프링을 배우면 "스프링이 ~하게 관리해준다."와 같이 뭘 알아서 해준다는 것을 많이 들어봤을 것이다. 필자는 '스프링이 알아서 다 해준다고?? 어떻게??' 라는 의문점이 있었다. 뭐 스카이캐슬에 김주영쌤 같은 느낌

하지만 @Configuration 을 배우면서 어떤 식으로 스프링이 관리해준다는 말의 의미를 깨닫게 되었다. 앞으로 이어질 @Configuration에 대한 내용을 통해 독자도 필자와 같은 깨달음을 이해할 수 있길..

 

 

@Configuration 을 적용한 AppConfig에는 놀라운 비밀이 있다. 

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

사실 개발자가 AppConfig라는 설정 정보를 입력하더라도

@Configuration 을 붙이면 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서

AppConfig 클래스를 상속받은 임의의 다른 클래스(AppConfig@CGLIB)를 만들고, 그 다른 클래스(AppConfig@CGLIB)를 스프링 빈으로 등록한 것이다!

(필자의 속마음 : 와 나는 내가 만든 AppConfig를 스프링 빈으로 등록되는 줄 알았더니 더 똑똑하게 관리하려고 상속한 클래스를 만들었구나.. 대박.. 이게 바로 스프링이 "관리해준다" 이런 것인가)

   [  AppConfig  ]

       ( 상속 ⬆️)

[  AppConfig@CGLIB  ]

 

 

[AppConfig@CGLIB 예상 코드]

빈 이름은 AppConfig인데 인스턴스 객체는 내가 만든 객체가 아닌 스프링이 조작한 AppConfig@CGLIB이다.

그 임의의 다른 클래스가 바로 싱글톤이 보장되도록 해준다. 아마도 다음과 같이 바이트 코드를 조작해서 작성되어 있을 것이다.(실제로는 CGLIB의 내부 기술을 사용하는데 매우 복잡하다. 하지만 이해를 위해 임시로 예상되는 코드를 만든 것이다.)

@Bean
public MemberRepository memberRepository() { //AppConfig를 오버라이드한 코드임 

	if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) { 
           return 스프링 컨테이너에서 찾아서 반환;
	} 
    
	else { //스프링 컨테이너에 없으면
	  기존 로직(내가 만든 AppConfig에서 객체 생성 코드)을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록 return 반환
	} 
}

@Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.

→ 이러한 작업이 있기 때문에 싱글톤이 보장되는 것이다.

 


 

✔️ 문제 상황 : 자바 코드로 보았을 때 싱글톤이 깨지는 것 처럼 보인다. 

앞서 회원과 주문 서비스 예제를 생각해보면 " 1) memberService에서 회원 저장 레포지토리 사용을 위해서 객체를 생성 2) orderService에서 조회하기 위해서 회원 저장 레포지토리 사용을 위해서 객체 생성 " 인 것을 보아 MemoryMemberRepository()는 

자바 코드 상으로 2번 호출되는 것을 알 수 있고 이는 싱글톤이 깨지는 것처럼 보인다. 

1) @Bean memberService -> new MemoryMemberRepository()
2) @Bean orderService -> new MemoryMemberRepository()

 

✔️ 실제로 싱글톤이 깨지는지 확인 

하지만 스프링 컨테이너는 스프링 빈이 싱글톤이 되도록 보장해준다고 했는데 어떻게 가능한 것인가? 에 대해서 알아보자 

먼저, 테스트 코드를 통해 객체가 2개가 따로 생성되는지 확인해볼 수 있다.

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

	//Impl에 테스트용으로 만들어놓은 get을 사용하기 위해서 구체 타입을 가져온다.
	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();

		//3개 다 같은 같을 참조한다.
	System.out.println("MemberService -> memberRepository = " + memberRepository1);
	System.out.println("OrderService -> memberRepository = " + memberRepository2);
	System.out.println("memberRepository = " + memberRepository);

	assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
	assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);


	}

 

테스트 코드를 통해 AppConfig의 자바 코드를 각각 2번 new MemoryMemberRepository 호출했다.

하지만 결과를 보면 memberRepository 인스턴스는 모두 같은 인스턴스가 공유되어 사용된다.(같은 값을 참조하고 있는 것을 확인해보았다)

 

엇???AppConfig의 자바 코드를 보면 분명히 각각 2번 new MemoryMemberRepository 호출해서 다른 인스턴스가 생성되어야 하는데? 어떻게 된 일일까? 혹시 두 번 호출이 안되는 것일까? 

=> 비밀은 @Configuration 을 적용한 AppConfig라는 것이다. 

 

앞서 말한대로 개발자가 @Configuration 을 붙인 AppConfig 코드를 작성하면  스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용한다.

AppConfig 클래스를 상속받은 임의의 다른 클래스(AppConfig@CGLIB)를 만들고, 그 다른 클래스(AppConfig@CGLIB)를 스프링 빈으로 등록한 것이다!

 

✔️따로 바이트 코드 조작 라이브러리를 사용하는 이유 

스프링 컨테이너는 싱글톤 레지스트리. ->  따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다.

그런데 스프링이 자바 코드 자체까지 어떻게 하기는 어렵다. 저 자바 코드를 보면 “MemberRepository”가 분명 3번 호출되어야 하는 것이 맞다. 하지만 결과적으로 1번만 호출이 되었다.

그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다.

 

✔️AppConfig 스프링 빈 조회를 통해 확인 

사실 AnnotationConfigApplicationContext 에 파라미터로 넘긴 값은 스프링 빈으로 등록된다.

그래서 AppConfig 도 스프링 빈이 된다.

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

@Test
	void configurationDeep() {
		ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
		//AppConfig도 스프링 빈으로 등록된다.
		AppConfig bean = ac.getBean(AppConfig.class);

		System.out.println("bean.getClass() = " + bean.getClass());//클래스 정보 출력 = class hello.core.order.AppConfig$$EnhancerBySpringCGLIB$$cfee49bc
	}
  • 결과 = class hello.core.order.AppConfig$$EnhancerBySpringCGLIB$$cfee49bc

순수한 클래스라면 다음과 같이 출력되어야 한다. ⇒ class hello.core.AppConfig

 

그런데 예상과는 다르게 클래스 명에 xxxCGLIB가 붙으면서 상당히 복잡해진 것을 볼 수 있다. 즉, 앞서 말한 것처럼 스프링이 AppConfig 클래스를 상속받은 임의의 다른 클래스(AppConfig@CGLIB)를 스프링 빈으로 등록했다는 것을 알 수 있다. 

 

이것은 내가 만든 클래스(직접 코드를 짜서 만든 것)가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서

AppConfig 클래스를 상속받은 임의의 다른 클래스(AppConfig@CGLIB)를 만들고, 그 다른 클래스(AppConfig@CGLIB)를 스프링 빈으로 등록한 것이다!

 

 

[참고]

AppConfig@CGLIB는 AppConfig의 자식 타입이므로, AppConfig 타입으로 조회 할 수 있다.(부모를 getBean하면 자식까지도 다 불러온다)

 

그럼 @Configuration을 적용하지 않고 @Bean 만 적용하면 어떻게 되는 지 예상이 가능할 것이다. 

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

 

 

 

 

- 이 글은 김영한님의 스프링 기본편을 듣고 정리한 것입니다. (인프런)