목차
- 싱글톤 컨테이너 - 웹 애플리케이션과 싱글톤
- 싱글톤 컨테이너 - 싱글톤 패턴 (디자인패턴에서의..)
- 싱글톤 컨테이너 - 싱글톤 컨테이너 -> 스프링 컨테이너가 자동으로 싱글톤 패턴을 적용해줌
- 싱글톤 컨테이너 - 싱글톤 방식의 주의점
- 싱글톤 컨테이너 - @Configuration과 싱글톤
- 싱글톤 컨테이너 - @Configuration과 바이트코드 조작의 마법
Configuration과 싱글톤
memberSerivce()와 orderService()가 모두 호출된 상황을 떠올리자.
그림과 같이 2번 memberRepository()가 호출되며 new MemoryMemberRepository()가 실행되는 것으로 보인다.
그렇다면, singleton이 위반되는 것이 아닌가? (객체가 2번생성됨)
실제로 그런지 코드로 테스트해보자
먼저 테스트 용도로 memberServiceImpl과 orderServiceImpl에 자신의 memberRepository를 반환해주는 함수를 추가한다.
memberServiceImpl
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository; // 구현체를 직접 선택하지 않도록 함 -> 오직 인터페이스만 존재 (추상화에만 의존함 DIP)
//테스트용도 (29. @Configuration과 싱글톤 단원)
public MemberRepository getMemberRepository(){
return memberRepository;
}
}
orderServiceImpl
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;// -> 오직 인터페이스만 존재 (추상화에만 의존함 DIP)
//테스트용도 (29. @Configuration과 싱글톤 단원)
public MemberRepository getMemberRepository(){
return memberRepository;
}
}
singleton패키지 하위에 CounfigurationSingletonTest클래스를 생성한 후,
OrderServiceImpl / MemberServiceImpl / memberRepository를 싱글톤 컨테이너(스프링제공)으로 호출하여, 서로 다른 3가지의 객체가 만들어졌는지 확인한다
CounfigurationSingletonTest
package hello.core.singleton;
import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
public class ConfigurationSingletonTest {
@Test
void configurationTest(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
//구현체를 의존하는 것은 안좋지만 getMemberRepository를 테스트해보기 위함
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
MemberRepository memberRepository = ac.getBean("memberRepository",MemberRepository.class);
// 3번의 return new MemoryMemberRepository(); 가 실행됨 -> 객체도 3개?
MemberRepository memberRepository1 = memberService.getMemberRepository();
MemberRepository memberRepository2 = orderService.getMemberRepository();
System.out.println("memberService -> memberRepository = " + memberRepository1);
System.out.println("orderService -> memberRepository = " + memberRepository2);
System.out.println("memberRepository -> memberRepository = " + memberRepository);
//Test완성
assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
}
결과는, 서로 다른 객체가 생성되지 않고 싱글톤 패턴을 유지하는 것을 확인할 수 있다.
자바 코드의 흐름대로라면 3개의 객체가 생성되는 것이 맞을 탠데, 어떻게 이것이 가능한지
일단 메소드가 출력될 때 마다 출력해보면서 플로우를 따라가 보자.
방법은 다음과 같다.
AppConfig 에 메소드가 호출될 때 마다 출력을 넣어본다.
그 후 위의 테스트를 다시한번 실행하며, memberRepository가 실제로 3번 호출되는지 확인해보자.
AppConfig
package hello.core;
import hello.core.discount.DisountPolicy;
import hello.core.discount.FixDiscountPolicy;
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 // Spring에서 제공하는 DI 컨테이너
public class AppConfig { //관심사의 분리
//config를 통해서 memberService / OrderService(인터페이스) 를 조회시 -> 조회하는 애들은 인터페이스와만 의존관계를 가진다! (구현체를 조회한게 아니라 인터페이스를 조회함)
//config에서 등록한 각각 구현체 MemoryMemberRepo / FixDiscountPolicy 구현체로 생성한
// memberSerivceImpl / OrderServiceImpl(구현체)를 반환한다.
@Bean
public MemberService memberService(){ // 메소드 명 : 스프링 빈의 이름 (key)
System.out.println("call AppConfig.memberService");
return new MemberServiceImpl(memberRepository()); // 리턴 값 : 스프링 빈의 value
// MemberServiceImpl을 생성자를 이용해서 생성함 -> 생성시 매개변수로 구현체를 넣어줌
// MemberServiceImpl자체에서 MemberRepository구현체를 선택하지 않고, 여기서 넣어주기 때문에 DIP/OCP를 지켰다
}
@Bean
public MemberRepository memberRepository() {
System.out.println("call AppConfig.memberRepository");
return new MemoryMemberRepository();//나중에 구현체를 변경할 때에도 쉽게 변경하기 쉬움
}
@Bean
public OrderService orderService(){
System.out.println("call AppConfig.orderService");
return new OrderServiceImpl(memberRepository(), disountPolicy());
}
@Bean
public DisountPolicy disountPolicy(){
return new RateDiscountPolicy();
// return new FixDiscountPolicy();
}
}
결과는 다음과 같다.
memberRepository가 1회만 실행된다 ! (3회가 아닌)
그 원인에 대해 알아보자.
@Configuration과 바이트코드 조작
위 질문의 답은 @Configuration덕분임.
@Configuration
우리가 만든 appConfig를 한번 출력해보자.
ConfigurationSingletonTest
@Test
void configurationDeep(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}
다음과 같은 테스트를 하나 추가해서 돌려본다.
결과는 다음과 같이 순수한 AppConfig가 아닌 뒤에 CGLIB이 붙은 것을 볼 수 있다.
순수한 클래스라면 다음과 같이 출력되어야 한다.
class hello.core.AppConfig
이것은 내가 만든 클래스(AppConfig) 가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스(AppConfig@CGLIB)를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다!
그림으로 보면 다음과 같다.
AppConfig@CGLIB 예상코드
memberRepository() method가 오버라이드(실제 AppConfig로부터)되어 변경된 메소드 (예상버전)
@Bean
public MemberRepository memberRepository() {
if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 찾아서 반환;
}
else { //스프링 컨테이너에 없으면
기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록 return 반환
}
}
즉, 3번이 아닌 1번 호출된 이유는
2번째 호출 부터는 if문에 걸려서 이미 스프링 컨테이너에 등록되어있기 때문에 해당의 것을 반환하여 사용해서 , 기존로직(실제 AppConfig의 memberRepository()) 이 호출되지 않았기 때문이다.
즉, 위와같은 상황이 @Bean이 붙은 메소드마다 발생하여, 싱글톤을 보장해주는 것이다.
참고 AppConfig@CGLIB는 AppConfig의 자식 타입이므로, AppConfig 타입으로 조회 할 수 있다.
@Configuration을 사용하지 않을 경우
appConfig의 @Configuration을 제거하고 아래의 테스트를 실행해보면, 다음과 같이 순수한 클래스가 나온다.
@Test
void configurationDeep(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
AppConfig bean = ac.getBean(AppConfig.class);
System.out.println("bean = " + bean.getClass());
}
AppConfig@CGLIB 기술을 사용하지 않은 것!
그렇다면, 다음의 테스트를 진행하면, 당연하게도 싱글톤이 깨져, memberRepository 가 3번 호출되어 3번의 객체 생성이 발생된다.
인스턴스가 같은 지 돌려보면,
모두 다른 인스턴스 객체(memberRepository)를 사용하며,
심지어, orderService 와 memberSerivce에서 호출하면, new로 생성되었기 때문에 스프링 컨테이너가 관리하는 스프링 빈이 아니기 때문에, 스프링 컨테이너가 제공하는 기능을 사용할 수도 없다!
정리
- @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
- memberRepository() 처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.
- 크게 고민할 것이 없다. 스프링 설정 정보는 항상 @Configuration 을 사용하자.
'Backend > Spring' 카테고리의 다른 글
[Spring] 컴포넌트 스캔 2) 기타 사항 (0) | 2021.09.30 |
---|---|
[Spring] 컴포넌트 스캔 1) 컴포넌트 스캔과 의존관계 주입 (0) | 2021.09.30 |
[Spring] 싱글톤 컨테이너 2) 싱글톤 방식의 주의점 (0) | 2021.09.30 |
[Spring] 싱글톤 컨테이너 1) 싱글톤 컨테이너의 필요성 (0) | 2021.09.30 |
[Spring] 스프링 컨테이너와 빈 3) 다향한 설정 형식 지원(xml) (0) | 2021.09.30 |