오봉이와 함께하는 개발 블로그
Spring - 싱글톤 컨테이너(@Configuration과 싱글톤, Configuration과 바이트코드 조작의 마법) 본문
BE/Spring
Spring - 싱글톤 컨테이너(@Configuration과 싱글톤, Configuration과 바이트코드 조작의 마법)
오봉봉이 2022. 6. 8. 18:47728x90
@Configuration과 싱글톤
- AppConfig를 보면 이상한 점이 있다.
- memberService 빈을 만드는 코드를 보면 memberRepository()를 호출
- memberRepository()를 호출하면 new MemoryMemberRepository()를 호출한다.
- orderService 빈을 만드는 코드도 동일하게 memberRepository() 를 호출한다.
- memberRepository()를 호출하면 new MemoryMemberRepository() 를 호출한다.
- 각각 다른 두 개의 MemoryMemberRepository가 생성되며 싱글톤이 깨지는 것 처럼 보이는데 스프링 컨테이너는 이 문제를 어떻게 해결할까??
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
검증
- 테스트를 위해 MemberRepository를 조회할 수 있는 기능을 추가한다. 기능 검증을 위해 잠깐 사용하는 것이니 인터페이스에 조회기능까지 추가하지는 말자.
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
// 테스트 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
// 테스트 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
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 -> memberRepository1 = " + memberRepository1);
System.out.println("orderService -> memberRepository2 = " + memberRepository2);
System.out.println("memberRepository = " + memberRepository);
assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
}
memberService -> memberRepository1 = hello.core.member.MemoryMemberRepository@4bff64c2
orderService -> memberRepository2 = hello.core.member.MemoryMemberRepository@4bff64c2
memberRepository = hello.core.member.MemoryMemberRepository@4bff64c2
- 확인해보면 memberRepository 인스턴스는 모두 같은 인스턴스가 공유되어 사용됨.
- AppConfig의 자바 코드를 보면 분명히 각각 2번 new MemoryMemberRepository 호출해서 다른 인스턴스가 생성되어야 하는데... 알아보도록 하자
검증
@Configuration
public class AppConfig {
// call AppConfig.memberRepository
// call AppConfig.memberService
// call AppConfig.memberRepository
// call AppConfig.orderService
// call AppConfig.memberRepository
@Bean
public MemberService memberService() {
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemoryMemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
- 위 주석과 같이 총 memberRepository는 3번이 호출되어야 하는 거 아닐까?
-
- 스프링 컨테이너가 빈에 등록하기 위해 @Bean이 붙어있는 memberRepository()호출
-
- memberService() 로직에서 memberRepository() 호출
-
- orderSerivce() 로직에서 memberRepository() 호출
-
하지만 결과는 모두 1번만 호출!
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
Configuration과 바이트코드 조작의 마법
- 스프링 컨테이너는 싱글톤 레지스트리
- 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 함
- 위 자바 코드를 보면 분명 3번이 호출되어야 하는데...
스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다!
비밀은 @Configuration을 적용한 AppConfig에 있다
@Test
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig been = ac.getBean(AppConfig.class);
System.out.println("been = " + been.getClass());
// 결과 -> been = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$993824f4
}
순수한 클래스면 class hello.core.AppConfig라고 나와야 하는데 xxxCGLIB가 붙은 형태인 class hello.core.AppConfig$$EnhancerBySpringCGLIB$$993824f4라고 나온다.
이는 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해 AppConfig클래스를 상속받은 임의의 다른 클래스를 만들고 그 다른 클래스를 스프링 빈에 등록한 것
xxxCGLIB 그림
내가 만든 클래스가 아닌, 임의의 다른 클래스가 싱글톤을 보장해준다.
AppConfig@CGLIB 예상 코드
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) { return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록 return 반환
}
}
- @Bean이 붙은 메소드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환, 없으면 생성해서 등록하고 반환하는 코드가 동적으로 만들어짐.
@Configuration을 적용하지 않고, @Bean만 적용하면?
- AppConfig에 @Configuration 삭제
bean = class hello.core.AppConfig
call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
call AppConfig.memberRepository
memberService -> memberRepository1 = hello.core.member.MemoryMemberRepository@7d1cfb8b
orderService -> memberRepository2 = hello.core.member.MemoryMemberRepository@48e92c5c
memberRepository = hello.core.member.MemoryMemberRepository@2e1ef60
- 테스트 코드도 실패하고, 다 다른 MemoryMemberRepository 인스턴스를 가지고 있다.
정리
- @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤은 보장하지 않음
- memberRepository()처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않음
- 스프링 설정 정보는 항상 @Configuration을 사용하자.
출처 : 인프런 김영한 지식공유자님의 스프링 완전 정복 로드맵 강의
728x90
'BE > Spring' 카테고리의 다른 글
Spring - 컴포넌트 스캔(필터, 중복 등록과 충돌) (0) | 2022.06.09 |
---|---|
Spring - 컴포넌트 스캔(컴포넌트 스캔과 의존관계 자동 주입 시작하기, 탐색 위치와 기본 스캔 대상) (0) | 2022.06.08 |
Spring - 싱글톤 컨테이너(싱글톤 컨테이너, 싱글톤 방식의 주의점) (0) | 2022.06.08 |
Spring - 싱글톤 컨테이너(웹 애플리케이션과 싱글톤, 싱글톤 패턴) (0) | 2022.06.07 |
Spring - 스프링 컨테이너와 빈 - 5(스프링 빈 설정 메타 정보 - BeanDefinition) (0) | 2022.06.06 |
Comments