Spring 코드를 보다보면 이상한 점이 있다.
내가 작성한 AppConfig.java 코드를 보자.
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public DiscountPolicy discountPolicy() {
//여기서 간단하게 정률 할인으로 할것이냐, 고정 할인으로 할것이냐 선수교체 간단히 할 수 있음 -> 객체화 good!
//return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
}
1. memberService를 만드는 Bean 코드를 보자.
▶ memberRepository()를 실행하면 → new MemoryMemberRepository()를 실행한다.
2. orderService를 만드는 Bean 코드를 보자.
▶ orderService()를 실행하면 → new OrderServiceImpl()를 실행 → memberRepository()를 실행
→ new MemoryMemberRepository()를 실행한다.
결과적으로 각각 다른 2개의 MemoryMemberRepository가 생성되면서 싱글톤이 깨지는 것 처럼 보인다.
스프링 컨테이너는 이 문제를 어떻게 해결할지 실험 코드를 짜보자.
<실험 코드 예제>
1. 테스트를 위해 MemberRepository를 조회할 수 있는 기능을 추가한다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
//테스트 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
//테스트 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
2. 테스트코드에 다음과 같은 코드를 추가한다.
package hello.core.singleton;
import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ConfigurationSingletonTest {
@Test
void configurationTest() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
//Bean객체 구체방식으로 끌어오기
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);
//로직이 여러 객체를 통해 실행되더라도 같은인스턴스를 끌어옴을 알 수 있다. -> 싱글톤 방식 준수
Assertions.assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
Assertions.assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
}
<실행결과>
다음과 같이 같은 참조값을 가리키는 것을 확인할 수 있다. → 스프링 컨테이너는 싱글톤 레지스트리다.
왜 이것이 가능할까?
@Configuration과 바이트코드 조작
스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용하기 때문이다.
package hello.core.singleton;
import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ConfigurationSingletonTest {
@Test
void configurationDeep() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
//AppConfig도 springBean으로 등록이 되는 상태
AppConfig bean = ac.getBean(AppConfig.class);
//AppConfig안에 있는 클래스 정보가 출력됨.
System.out.println("bean = " + bean.getClass());
}
}
다음의 테스트 코드를 작성했을 때,
<나의 예상>
class hello.core.AppConfig 이 출력
<결과>
<Why?>
클래스 명에 xxxCGLIB가 붙는다.
이것은 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용하여
AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다.
따라서 한개의 클래스를 공유하기 때문에 싱글톤 방식이 가능한 것이다.
+@ 김영한쌤의 CGLIB예상코드 ( 실제로는 매우 복잡하게 구현되어 있을 것)
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
} else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
return 반환
}
}
@Configuration을 적용하지 않고, @Bean 만 적용하면 어떻게 될까?
`@Configuration` 을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장하지만,
만약 @Bean만 적용하면 어떻게 될까?
<나의 예측>
→ @Bean만 사용해도 스프링 빈으로 등록되지만 싱글톤을 보장하지는 않는다.
왜냐하면 memberRepository()처럼 의존관계 주입이 필요해서
(예시)
메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.
<예제 코드>
//@Configuration을 삭제하여 Tdd를 실행해보자.
//@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public DiscountPolicy discountPolicy() {
//여기서 간단하게 정률 할인으로 할것이냐, 고정 할인으로 할것이냐 선수교체 간단히 할 수 있음 -> 객체화 good!
//return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
}
<결과>
다음과 같이 참조값이 서로 다른 두개의 객체가 생성됨을 확인할 수 있다 → 싱글톤 x
당연히 assertionThat 테스트 유효성 검사도 통과하지 못해 오류가 발생한다.
결론
스프링 설정정보는 항상 @Configuration을 통해 할 필요성이 있다.
'Develop > Spring (이론)' 카테고리의 다른 글
@componentScan 중복 등록과 충돌 (0) | 2023.08.20 |
---|---|
@ComponentScan & 의존성 주입(@Autowired) 딥다이브 (0) | 2023.08.18 |
싱글톤(Singleton) 딥다이브 ( Spring 內 싱글톤 컨테이너 비교) (0) | 2023.08.12 |
스프링 빈 설정 메타 정보 _ BeanDefinition (0) | 2023.08.11 |
스프링의 다양한 설정 형식 지원 - 자바 코드 , XM (0) | 2023.08.10 |