[스프링 Core] @Configuration를 사용하면 싱글톤을 유지할 수 있는 이유(CGLIB)
미리 보는 결론
@Configuration
은 싱글톤을 위해서 존재합니다.@Bean
만 사용해도 스프링 빈으로 등록이 가능@Configuration
을 사용하면 바이트 코드를 조작하는CGLIB
를 사용하여 클래스를 상속받는 프록시 객체를 만들어서 스프링 빈을 싱글톤으로 관리할 수 있게 해준다.- 스프링 설정 정보는 반드시
@Configuration
을 사용하자!
@Configuration 예시
스프링 컨테이너는 기본값으로 모든 인스턴스를 싱글톤으로 관리합니다.
그런데 아래와 같은 코드를 보면 좀 이상하지 않나요?
ApplicationConfig
@Configuration
public class ApplicationConfig {
@Bean
public MemberRepository memberRepository() {
return new MemberRepository();
}
@Bean
public PencilRepository pencilRepository() {
return new PencilRepository();
}
@Bean
public MemoService memoService() {
// memberRepository()는 new MemberRepository()을 반환합니다.
return new MemoServiceImpl(memberRepository(), pencilRepository());
}
@Bean
public UserService userService() {
// memberRepository()는 new MemberRepository()을 반환합니다.
return new UserServiceImpl(memberRepository());
}
}
memoService
빈을 생성하는 코드를 보면 memberRepository()
를 호출 합니다.memberRepository()
는 new MemberRepository()
를 반환합니다.
userService
빈을 생성하는 코드를 보면 memberRepository()
를 호출 합니다.memberRepository()
는 new MemberRepository()
를 반환합니다.
각각 다른 2개의 new MemberRepository()
를 반환하여 싱글톤이 아닌 것처럼 보입니다…..?
이러면 싱글톤이 깨져야 정상 아닌가요?
싱글톤 유지 테스트
직접 테스트 해봅시다.
memberRepository
를 반환하는 getMemberRepository()
메소드를 작성했습니다.
MemoServiceImpl
public class MemoServiceImpl implements MemoService {
private final MemberRepository memberRepository;
private final PencilRepository pencilRepository;
public MemoServiceImpl(MemberRepository memberRepository, PencilRepository pencilRepository) {
this.memberRepository = memberRepository;
this.pencilRepository = pencilRepository;
}
public MemberRepository getMemberRepository() {
return memberRepository;
}
public PencilRepository getPencilRepository() {
return pencilRepository;
}
}
UserServiceImpl
public class UserServiceImpl implements UserService {
private final MemberRepository memberRepository;
public UserServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
ApplicationConfigTest
public class ApplicationConfigTest {
@Test
@DisplayName("싱글톤이 깨지는가?")
void applicationConfigTest() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
UserServiceImpl userService = ac.getBean("userService", UserServiceImpl.class);
MemoServiceImpl memoService = ac.getBean("memoService", MemoServiceImpl.class);
MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
System.out.println("userService 의 memberRepository="+ userService.getMemberRepository());
System.out.println("memoService 의 memberRepository="+ memoService.getMemberRepository());
System.out.println("memberRepository 의 memberRepository="+ memberRepository);
Assertions.assertEquals(userService.getMemberRepository(), memberRepository);
Assertions.assertEquals(memoService.getMemberRepository(), memberRepository);
}
}
결과
결과를 보면 memberRepository
는 모두 같은 인스턴스가 공유되고 있다는 것을 확인할 수 있습니다.
(싱글톤이 유지 되고 있습니다!!)
ApplicationConfig
를 보면 분명히 두번 memberRepository()
를 호출하여 다른 인스턴스가 생성되어야할 것 같은데….
어떻게 이러한 결과가 발생할 걸까요? 🤔
ApplicationConfig 에서 호출 안되는거 아냐? 🫨
정말로 두 번 호출되는지ApplicationConfig
에 로그를 남겨봅시다.
ApplicationConfig
@Configuration
public class ApplicationConfig {
@Bean
public MemberRepository memberRepository() {
System.out.println("memberRepository()");
return new MemberRepository();
}
@Bean
public PencilRepository pencilRepository() {
return new PencilRepository();
}
@Bean
public MemoService memoService() {
System.out.println("memoService()");
// new MemberRepository()
return new MemoServiceImpl(memberRepository(), pencilRepository());
}
@Bean
public UserService userService() {
System.out.println("userService()");
// new MemberRepository()
return new UserServiceImpl(memberRepository());
}
}
ApplicationConfigTest
를 실행하면 다음과 같은 결과가 출력 됩니다.
엇...?memberRepository()
가 1번만 호출됩니다…?
예상대로라면 memoService()
, userService()
를 호출할 때 memberRepository()
가 호출되어야 합니다..
무슨일이 발생한 걸까요? 🤔
@Configuration과 바이트코드 조작 마법
스프링 컨테이너는 싱글톤 레지스트리 입니다.
이말은 스프링 빈이 싱글톤이 유지되도록 보장해줘야 한다는 의미 입니다.
그런데 스프링이 자바 코드까지 컨트롤하기는 어렵습니다.
ApplicationConfig
코드를 보면 분명 memberRepository
가 3번 호출 되어야 합니다.
그래서 스프링은 클래스의 바이트 코드를 조작하는 라이브러리를 사용하여 이러한 문제를 해결합니다.
참고
바이트 코드(.class)란 자바 클래스 파일(.java)가 자바 컴파일러(javac)를 통해 컴파일된 코드입니다.
이 바이트 코드가 런타임 환경을 통해 실행됩니다.
ApplicationConfig
를 출력해봅시다.
ConfigurationDeep
public class ConfigurationDeep {
@Test
void configurationDeep() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
ApplicationConfig bean = ac.getBean(ApplicationConfig.class);
System.out.println("bean = " + bean);
}
}
결과
AnnotationConfigApplicationContext
에서 넘긴 값은 스프링 빈으로 등록 됩니다.
그래서 ApplicationConfig
도 스프링 빈으로 등록 됩니다.
순수한 클래스라면 seaung.springstudy.config.ApplicationConfig
가 출력되어야 합니다.
그런데 출력 결과를 보면 CGLIB
가 붙어 있는 것을 확인할 수 있습니다.
이것 저희가 작성한 클래스가 아니고 스프링이 바이트코드 조작을 위해 사용한 라이브러리 입니다.ApplicationConfig
를 상속받은 ApplicationConfig@CGLIB
를 생성하고 이를 스프링 빈으로 등록한 것 입니다.
ApplicationConfig@CGLIB
가 하는 역할은@Bean
이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고,
스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어 스프링 빈을 싱글톤으로 관리하게 됩니다.
참고CGLIB
는 클래스 기반으로 바이트 코드를 조작하여 프록시를 생성하는 방식 입니다.
프록시 객체를 생성할 때 일반적으로 인터페이스를 상속 받거나 클래스를 상속 받거나 하는 2가지 방식을 사용합니다.CGLIB
는 클래스를 상속받아 바이트 코드를 조작하여 프록시 객체를 생성합니다.
@Configuration이 없으면? 🤔
@Configuration
을 붙이면 바이트 코드를 조작하는 CGLIB
(프록시 객체)를 생성하여 스프링 빈을 싱글톤으로 관리할 수 있게 해줍니다.
정말로 그런지 @Configuration
을 제거해봅시다.
ApplicationConfig@Configuration
을 주석처리
// @Configuration
public class ApplicationConfig {
@Bean
public MemberRepository memberRepository() {
System.out.println("memberRepository()");
return new MemberRepository();
}
@Bean
public PencilRepository pencilRepository() {
return new PencilRepository();
}
@Bean
public MemoService memoService() {
System.out.println("memoService()");
// new MemberRepository()
return new MemoServiceImpl(memberRepository(), pencilRepository());
}
@Bean
public UserService userService() {
System.out.println("userService()");
// new MemberRepository()
return new UserServiceImpl(memberRepository());
}
}
ConfigurationDeep
public class ConfigurationDeep {
@Test
void configurationDeep() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationConfig.class);
ApplicationConfig bean = ac.getBean(ApplicationConfig.class);
System.out.println("bean = " + bean);
}
}
결과
먼저 @Bean
만 사용해도 ApplicationConfig
가 스프링 빈으로 등록되는 것을 확인할 수 있습니다.
또한, memberRepository
가 3번 호출되는 것을 확인할 수 있고, CGLIB
가 생성되지 않은 것을 확인할 수 있습니다!!
정리
@Bean
만 사용해도 스프링 빈으로 등록이 가능@Configuration
을 사용하면 바이트 코드를 조작하는 CGLIB를 사용하여 클래스를 상속받는 프록시 객체를 만들어서 스프링 빈을 싱글톤으로 관리할 수 있게 해준다.- 스프링 설정 정보는 반드시
@Configuration
을 사용하자!
싱글톤 주의사항 🚨
싱글톤을 사용할 때 주의해야하는 사항이 있습니다.
따라서 싱글톤 객체를 사용하는 경우 상태를 유지(stateful
)하게 설계하면 안됩니다.
무상태(stateless
)로 설계해야 합니다.
싱글톤 패턴은 객체 인스턴스를 하나만 생성해서 공유하는 방식 이기 때문입니다.
상태를 유지하게 설계할 경우 어떤 문제가 발생할까요?
OrderServiceOrderService
인스턴스가 price
라는 변수를 소유하고 있습니다.
public class OrderService {
private int price;
public void order(String name, int price) {
System.out.println("name = " + name + ", price = " + price);
this.price = price; // 상태 저장
}
public int getPrice() {
return price;
}
}
AppConfig
빈 등록
@Configuration
public class AppConfig {
@Bean
public OrderService orderService() {
return new OrderService();
}
}
OrderServiceTest
public class OrderServiceTest {
@Test
void orderServiceServiceSingleton() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 빈 조회
OrderService orderService1 = ac.getBean("orderService", OrderService.class);
OrderService orderService2 = ac.getBean("orderService", OrderService.class);
orderService1.order("user1", 1000);
orderService2.order("user2", 2000);
int price1 = orderService1.getPrice();
int price2 = orderService2.getPrice();
System.out.println("user1의 price = " + price1);
System.out.println("user2의 price = " + price2);
assertAll(
() -> assertEquals(1000, price1),
() -> assertEquals(2000, price2)
);
}
}
결과
uesr1
의 price
가 2000
으로 변경된것을 확인할 수 있습니다.user2
가 price
를 2000
으로 변경했기 때문에 발생한 문제 입니다.
이처럼 공유 필드는 싱글톤을 사용할 때는 사용해서는 안됩니다.
항상 무상태(stateless
)로 설계 해야합니다.
참고
'0+ 스프링 > 0+ 스프링 Core' 카테고리의 다른 글
[스프링 Core] 도대체 DI(Dependency Injection) 란 무엇인가? (0) | 2023.06.17 |
---|---|
[스프링 Core] 스프링 빈 초기화(@PostConstruct, @PreDestroy) (0) | 2023.06.08 |