당니의 개발자 스토리

@Configuration과 싱글톤 본문

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

@Configuration과 싱글톤

clainy 2024. 1. 23. 22:10

@Configuration과 싱글톤

이번에는 @Configuration의 비밀에 대해서 하나씩 파헤쳐보는 시간을 가지겠습니다.

@Configuration은 사실 싱글톤을 위해서 존재하는 겁니다.AppConfig 코드를 볼게요.

AppConfig 코드를 보면, 지금 이상한 게 하나 있어요. 분명히 싱글톤 이라고 했죠?

그런데 얘를 보세요. 만약에 스프링 빈으로 등록될 때, memberService를 호출하겠죠. 그럼 어떻게 됩니까?

이건 자바 코드이기 때문에 스프링이 어쩔 수 있는게 아니겠죠. 그래서 memberService를 호출하면, new MemberServiceImpl를 호출하면서, memberRepository()도 같이 호출합니다.

그럼 자바 코드니까 이 메서드가 호출 될 거에요. 그러면, new 해서 MemoryMemberRepository()가 생성되죠.

그러니까 @Bean이 있는데, memberService()를 하면, memberRepository()를 호출하면서, 결과적으로는 MemoryMemberRepository()를 한 번 호출해줍니다.

그 다음에 또 어떻게 됩니까? @Bean orderService 하면, 스프링이 'orderService를 빈으로 생성하나보네' 하고,

스프링 컨테이너가 이 메서드를 호출하겠죠. 그러면, new OrderServiceImpl하는 건 당연한데, 얘가 또 memberRepository()를 호출해요. 이건 순수한 자바 코드라서 스프링이 제어할 수 있는 건 아니기 때문에,

또 이 메서드를 호출하면서, 또 MemoryMemberRepository()가 호출됩니다. 지금 몇번 호출했습니까? 2번 호출했죠.

"이러면 싱글톤이 깨지는 거 아닌가요???" 하고 생각하는 게 정상입니다. 왜냐면 자바 코드로 new를 써서 생성했기 때문에 객체를 새로 생성한 것처럼 보이잖아요.

싱글톤이 깨질지, 안 깨질지 모르겠으면, 그냥 고민하지 말고 바로 테스트 코드로 돌려보는 게 좋습니다.


실험을 해보기 전에 정리를 해드릴게요.

memberService 빈을 만드는 코드를 보면, memberRepository()를 호출하죠. 즉, memberService()를 호출하면 new MemoryMemberRepository() 객체가 생성돼요.

그리고 orderService 빈을 만드는 코드도 동일하게 @Bean 이잖아요. 그럼 orderService()를 호출하면, memberRepository()를 호출할 거에요. 그럼 또 new MemoryMemberRepository() 해서 생성하겠죠. 그럼 객체가 2번 생성되는 겁니다.

우리가 싱글톤으로 하는 이유가 호출할 때마다 생성되는 게 아니라, 한 번만 가져다 쓰려고 했던 거였는데 지금 2개가 만들어져서 호출되는 거죠.

결과적으로 각각 다른 2개의 MemoryMemberRepository가 생성되면서 싱글톤이 깨지는 것처럼 보입니다.

스프링 컨테이너는 이 문제를 어떻게 해결할까요?

분명히 스프링 컨테이너는 싱글톤을 보장해준다고 그랬잖아요. 그런데 지금 자바 코드를 보면 이해가 안 된단 말이에요.


직접 테스트 해봅시다.

테스트 하는 방법은 간단합니다.

여기에 있는

memberRepository의 실제 참조값을 확인해보면 되겠죠.

그래서 MemberServiceImpl에다가 getMemberRepository() 라는 메서드를 만들겠습니다.

테스트 용도로 만든 거기 때문에, 인터페이스에다가는 만들지 않을게요.

그리고 나서, OrderServiceImpl에 들어가서

여기에다가도 getMemberRepository()를 만들어줍니다.

그러면,

OrderServiceImpl에 있는 memberRepository랑 MemberServiceImpl에 있는 memberRepository랑 자바 코드만 봐서는 달라야되는 거죠. 자바 코드에서 2번 생성했으니까요.

확인하기 위해서, test를 만들어보겠습니다.

singleton에다가 ConfigurationSingletonTest 클래스를 만들게요.

스프링 컨테이너를 만들고 나서, getBean으로 꺼내보겠습니다.

Impl로 클래스를 지정해서 꺼내는 이유는, 구현체로 꺼내야 .getMemberRepository() 해서 꺼낼 수 있기 때문입니다.

원래는 구체 타입으로 꺼내는 건 안 좋아요.

이렇게 해놓고, 이게 과연 같은지, 다른지 봐야되겠죠.

돌려보면,

다른 걸 알 수 있습니다.. 세상에..!!!

이유는 AppConfig.java에서

static으로 적어줬기 때문입니다.

static 처럼 정적 메서드@Bean으로 올리면, 싱글톤 보장을 지원받지 못한다고 합니다.

따라서, static으로 해주면 @Configuration, @Bean으로 해도 싱글톤 보장을 해주지 않기 때문에 static을 다 지워주셔야 합니다.

그리고 나서 다시 돌려보면,

싱글톤을 보장해주는 걸 볼 수 있습니다. 검증을 하기 전에 한가지를 더 해 보겠습니다.

얘도 으로 등록했기 때문에,

이렇게 꺼낼 수 있습니다.

이렇게 출력해보면,

세 놈 다 같은 겁니다. 세 개의 스프링 빈이 다 같은 겁니다.

AppConfig를 보면, 이렇게 했을 때 세 번의 new MemoryMemberRepository()가 호출되어야 하는게 맞는데, 도대체 어떻게 된 걸까요?

일단 테스트를 마저 완성하겠습니다. 참조값을 비교하는 거기 때문에 isSameAs를 써야합니다.

이렇게 하고 돌려보면, 테스트가 성공했습니다.


다시 정리하자면,

확인해보면 memberRepository 인스턴스는 모두 같은 인스턴스가 공유돼서 사용되고 있죠.

한 마디로 같은 애가 한 번만 생성이 됐을 거라는 거에요.

그런데 AppConfig의 자바 코드를 보면, test에서 했던 것처럼 분명히 3번 new MemoryMemberRepository()를 호출해서 각각 다른 인스턴스가 생성될 것 같은데 어떻게 된걸까? 하는 거죠.

혹시 두 번 호출이 안되는 것일까? 실험을 통해서 확인해봅시다.


AppConfig에다가 호출 로그를 남겨보는 거에요.

soutm 이라고 있는데, 이걸 하면

편합니다. 다른 것도 해줍니다.

이렇게 하면 결과는 어떻게 출력되어야 할까요?

먼저, @Bean을 보고 컨테이너가 memberService()로 들어와서,

이 출력부가 호출되겠죠. 그래서 처음에는 call AppConfig.memberService가 출력될 거에요.

그 다음에 new MemberServiceImpl(memberRepository())를 호출하면서, memberRepository()로 이동하겠죠. 그래서 call AppConfig.memberRepository를 호출할 거에요.

물론, 메서드의 호출 순서는 보장하지 않아요. 지금 임의로 생각해보는 거에요.

그리고 스프링 컨테이너가 memberRepository()를 호출하면, call AppConfig.memberRepository가 또 호출되겠죠.

그 다음 orderService 출력하면,

call AppConfig.orderService가 출력되겠고, 그 다음에 memberRepository()를 호출하니까 call AppConfig.memberRepository를 또 호출하겠죠.

최종적으로는

이렇듯 memberRepository 메서드가 3번 호출이 돼야 합니다.

이제 예상이 맞는지, 몇 번 호출되는지 테스트로 돌려보겠습니다.

그냥 이거 돌리면 되겠죠. ac를 초기화할 때, 빈을 등록하면서 AppConfig 가 호출되니까요.

call이 몇 개 있나요? 3개입니다. 이상하죠. 각각 1번씩만 호출됐습니다.

즉, 우리의 생각과는 다르게

call AppConfig.memberRepository이 3번 호출돼야 하는데, 한번만 호출된 거죠.

'이게 뭐지?' 싶죠. 뭔진 모르겠지만 여기서 느낄 수 있을 거에요.

"아 스프링이 정말 어떠한 방법을 써서라도 싱글톤을 보장해주는구나" 라는 걸 아실 수 거에요.

그런데 도대체 왜 그런지는 자바 코드로 설명이 잘 안되죠.


그래서 이거에 대해서 다음 시간에 @Configuration과 바이트코드 조작의 마법이라는 제목으로 설명을 드리겠습니다.