당니의 개발자 스토리
@Configuration과 바이트코드 조작의 마법 본문
@Configuration과 바이트코드 조작의 마법
드디어 Configuration과 Bytecode 조작의 마법에 대해서 알아보겠습니다. 마법이라고 붙힌 거는 저희가 순수하게 Java를 사용해왔던 거랑 좀 다르게 돌아가기 때문에 그렇게 적었어요.

스프링 컨테이너는 여러분 싱글톤 레지스트리란 말이에요. 따라서 스프링 빈이 싱글톤이 되도록 어떻게든 보장을 해줘야 돼요. 그런데 스프링이 Java 코드까지 어떻게 하기는 어렵죠. 아무리 스프링이 용 빼는 재주가 있어도 Java 코드 자체를 막 어떻게 할 수는 없잖아요.
그런데 저 Java 코드만 보면, 분명히 memberRepository()가 3번 호출 돼야 되는 게 맞죠. 그런데 이상하게 memberRepository()는 한 번만 호출이 됐단 말이에요.
자 비밀은 어디에 있냐면, 바로 @Configuration을 적용한 AppConfig에 비밀이 있습니다. 얘를 한번 꺼내서 보여드릴게요.
ConfigurationSingletonTest 밑에 Test를 하나 더 만들겠습니다.

getBean()을 통해서 AppConfig를 조회하겠습니다.

AnnotationConfigApplicationContext한테 AppConfig를 넘기면, AppConfig.class도 스프링 빈으로 등록이 됩니다.
getBean을 통해서 AppConfig를 꺼냈으니 출력을 해보겠습니다.

getClass() 하면, 얘의 class type(클래스 정보)이 뭔지 보는 거에요.
출력해보겠습니다.

원래는 여기까지만 나와야 정상인데, 뒤에 $$ 하고 이상한게 붙었어요.

원래같으면 이런 식으로 깔끔하게 클래스 정보만 나오는게 맞아요.
'$$ 이건 뭐야? 어떻게 된 거지?' 이제부터 말씀을 드리겠습니다.


우선, 순수한 클래스라면 class hello.core.AppConfig 이렇게 출력이 돼야 돼요. 그런데 예상과는 전혀 다르게 클래스명에 뭐뭐뭐 CGLIB가 붙으면서, 상당히 클래스 이름 자체가 복잡해졌어요.
이건 내가 만든 클래스가 아니에요.

'어? 내가 분명 AppConfig.class를 넣었는데 무슨 말이세요!' 라고 할 수 있는데, 스프링 빈을 등록하는 과정 속에서 스프링이 뭔가 조작을 해요. 그래서 다른 거를 스프링 빈으로 등록해 버린 거예요.
이것은 내가 만든 클래스가 아니라, 스프링이 바이트코드를 조작하는 CGLIB 라는 라이브러리를 사용해서, AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 거예요.
그냥 그림으로 보여드릴게요.

AppConfig가 있어요. 이걸 갖다가 이 CGLIB 라는 바이트코드 조작 라이브러리를 가지고, AppConfig를 상속받은 다른 애를 하나 만든 거에요. 그 다른 클래스가 AppConfig@CGLIB 이에요.
그리고 나서, 스프링이 어떻게 하냐면 내꺼(AppConfig) 말고, AppConfig@CGLIB 라는 자기가 조작한 클래스를 스프링 빈으로 등록을 대신 해버리는 거에요.
그래서 스프링 컨테이너에는 분명히 빈 이름은 AppConfig인데, 인스턴스 객체가 AppConfig@CGLIB인 게 빈으로 들어가 있는 거에요.
그래서 "AppConfig" 라는 이름으로, 내가 등록한 애는 사라지고 AppConfig@CGLIB만 등록이 되어있어요.
내가 만든 객체가 아닌, 바로 그 임의의 다른 클래스를 싱글톤이 되도록 보장해주는 겁니다. 아마 이 바이트 코드를 조작해서 만든 코드가

이런 식으로 되어 있을 거에요. 실제로는 CGLIB의 내부 기술을 사용하는데 매우 복잡해요. 그냥 예시로 아마 이렇게 될 거다라고 적은 거구요.
AppConfig 내의 memberRepository()를 상속받은 코드가 이렇게 만들어져있을 거예요. 내가 만든 코드(AppConfig)는 부모인 거고요, 얘는 자식 클래스 타입(AppConfig@CGLIB)입니다.
그러니까 AppConfig@CGLIB가 AppConfig의 memberRepository()를 Override 했을 거예요.

만약에 memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면, '스프링 컨테이너에서 찾아서 반환해!' 라고 할 거에요. 그래서 한 번만 호출이 됐던 거예요. 이미 있었으니까.
그런데 스프링 컨테이너에 없으면, 기존 로직을 호출하는 거예요.
부모에 있는 memberRepository()를 호출하거나, 아니면 AppConfig 어딘가에다가 내부적으로 생성을 해놓고 만들었던 원래 그 코드 있죠.

이걸 호출을 해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록한 다음에 반환하는 거에요.
그럼 처음에 memberRepository()가 호출되면, 스프링 컨테이너에 MemoryMemberRepository가 등록이 안되어 있으니까, else를 타서 기존 로직을 호출해서,

MemoryMemberRepository를 스프링 컨테이너에 등록할 거에요. 그런데 두번째부터는 memberRepository()를 호출을 하면, 오버라이드 된 memberRepository()가 호출이 되는 거죠. memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있죠? 스프링 컨테이너에서 찾아서 원래 있는 걸 반환을 해주는 거에요.

이렇게 해서 @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 새로 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어집니다.
이 덕분에 싱글톤이 보장이 되는 겁니다.
정리해보면, memberRepositry 라는 이름의 스프링 빈이 없으면 내가 만들었던 기존 코드를 호출해주고, 등록되어 있으면 있는걸 꺼내서 반환해버리는 거에요.

참고로 AppConfig@CGLIB는 AppConfig의 자식 타입이므로, getBean 했을 때 AppConfig 타입으로 조회 할 수 있습니다.
우리가 Test 코드에서

'AppConfig 클래스 타입으로 스프링 빈으로 등록되어 있는 걸 찾아줘!' 하고 getBeen을 했는데, AppConfig@CGLIB가 조회된 이유는 얘가 AppConfig의 자식 타입이라서 된 겁니다. 부모 타입으로 조회하면, 자식들이 다 끌려 나온다 했죠?
우리가 만든 AppConfig.class 자체는 스프링 빈으로 등록되어 있지 않고, 실제로 등록되어 있는 건 AppConfig@CGLIB 인거죠. 물론, 스프링 빈 이름 자체는 AppConfig 일 겁니다.
참고로 이러한 메커니즘은 나중에 AOP 이런데서도 동일한 메커니즘을 사용합니다.
'어? 만약에 @Configuration을 적용하지 않고, @Bean만 적용하면 어떻게 되나요?'

안 붙여도 됩니다. 안 붙여도 스프링 컨테이너에 얘들이 스프링 빈으로 다 등록됩니다. 대신에 문제가 있어요.
@Configuration을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장해요. 그런데 만약 @Bean만 적용하면 어떻게 될까요?

이렇게 주석처리를 하고,

test를 다시 실행해볼게요.

드디어 우리가 만들었던 순수한 AppConfig가 그대로 호출이 되죠. CGLIB 기술을 안 씁니다.

AnnotationConfigApplicationContext에다가 AppConfig를 전달하면, AppConfig도 스프링 빈으로 등록되고, AppConfig에 있는 @Bean 들도 다 스프링 빈으로 등록됩니다.
그런데 무슨 문제가 있냐? 출력된 거 보세요.

지금 memberRepository가 3번 호출됐죠.

다른 싱글톤이 깨진 거에요. 내가 만든 순수한 자바 코드가 도는 거에요.
이 출력 결과를 통해서 MemberRepository가 총 3번 호출된 것을 알 수 있죠. 1번은 @Bean에 의해 스프링 컨테이 너에 등록하기 위해서이고, 2번은 각각 memberRepository()를 호출하면서 발생한 코드입니다.
그래서 인스턴스가 같은지 테스트인

configurationTest()를 돌려보겠습니다.


당연히 오류가 생기구요. memberService가 참조하는 MemoryMemberRepository의 주소값과 orderService가 참조하는 MemoryMemberRepository의 주소값이 다른 걸 볼 수 있습니다.
그리고 한 가지 문제가 더 있는데,

이렇게 되면, MemberServiceImpl에 주입된 memberRepository는 스프링 빈이 아니에요. 이거는

그냥 이렇게 한 거랑 똑같아요.
스프링 컨테이너가 관리하지 않는 애에요. 물론, OrderService에 있는 애도 마찬가지에요.
왜냐면 기존에 CGLIB를 통해서는 어떻게 했습니까? 만약에 memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면, 스프링 컨테이너에 있는 걸 찾아서 반환해줬잖아요.
그래서 싱글톤도 보장이 되고, 스프링 컨테이너에 등록되어 있는 걸 가져오는 것도 보장이 됐는데, 지금처럼 이렇게 AppConfig에서 정말 생자로 java 코드를 호출해버리면,

사실 이렇게 객체가 새로 생성돼서 치환이 되는 거에요.
이 MemoryMemberRepository는 스프링 컨테이너가 관리하는 스프링 빈이 아니에요. 그냥 내가 직접 new 해주는 애랑 똑같은 거에요. 그래서 memberRepository()는 스프링 컨테이너로 관리도 안 돼요.

엔터프라이즈 버전에서는 경고를 줍니다. @Bean을 직접적으로 call 했다고 알려줍니다.

다시 @Configuration을 살려놓으면 정상적으로 돌아갑니다.
사실 이걸 해결할 수 있는 방법이 이런 것도 되거든요.

의존관계 주입이라는 걸 자동으로 해서 이렇게 넣으면, 또 문제가 해결되긴 해요. 그러면 스프링에서 다시 끌어온 거를 집어 넣어주는 거거든요.
@Autowired에 대해선 뒤에서 심도있게 배울 겁니다.
자 정리해 보겠습니다.


@Bean만 사용해도 스프링 빈으로 등록이 됩니다. 하지만 싱글톤은 보장하지 않아요.
그래서 @Configuration이 없으면 memberRepository()처럼 의존관계 주입이 필요해서, 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다는 겁니다.
크게 고민할 게 없어요. 왜 이름이 Configuration이겠어요? 설정 정보 있는데에다가는 그냥 다 @Configuration을 넣으세요.
스프링 설정 정보에는 항상 @Configuration을 사용하면 스프링이 자연스럽게 싱글톤을 잘 보장을 해줍니다.
이렇게 해서 @Configuration과 Bytecode의 조작의 마법까지 알아봤습니다.
질문에 대한 답변

'스프링 > 스프링 핵심 원리 - 기본편' 카테고리의 다른 글
| 탐색 위치와 기본 스캔 대상 (0) | 2024.01.24 |
|---|---|
| 컴포넌트 스캔과 의존관계 자동 주입 시작하기 (0) | 2024.01.24 |
| @Configuration과 싱글톤 (0) | 2024.01.23 |
| 싱글톤 방식의 주의점 (0) | 2024.01.23 |
| 싱글톤 컨테이너 (0) | 2024.01.23 |