싱글톤 패턴이란 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다. 예를 들어 사용자가 100명이라면 서버에 100번 요청을 보내면 서버에서는 100개의 인스턴스를 만들었다가 응답을 보낸 후 100개의 인스턴스를 삭제하는 과정을 거쳐야 한다.
그러나 인스턴스를 1개만 만들고 이를 공유한다면 이러한 리소스를 낭비하는 일이 없을 것이다. 이를 싱글톤 패턴이라 한다.
이러한 싱글톤에도 여러 문제점이 존재한다.
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리한다.
이러한 스프링 컨테이너 때문에 고객이 100번 요청을 해도 이미 만들어진 객체를 공유하기 때문에 효율적으로 재사용할 수 있다.
싱글톤 패턴은 인스턴스를 공유하기 때문에 특정 클라이언트에 의존적이 필드가 있어서는 안된다. 즉, 유지(stateful)하게 설계해서는 안된다.
따라서 무상태(stateless)로 설계를 해야 한다. 특정 클라이언트에 의존적인 필드가 존재해서는 안되며, 클라이언트가 값을 변경할 수 있는 필드가 존재해서는 안된다. 또한 가급적 읽기만 가능해야 한다.
만약 공유하거나 변경이 가능한 필드가 있다면 큰 장애로 이어질 수 있다.
이러한 일을 방지하기 위해서는 지역변수를 사용하거나 ThreadLocal, 파라미터를 필드로 사용하여 변경, 공유로부터 방어해야 한다.
만약 인스턴스가 2개가 생성되면 스프링 컨테이너는 이를 어떻게 처리할까?
@Configuration
public class Config{
@Bean
public CallService callService(){
return new CallServiceImpl(callRepository());
}
@Bean
public MessageService messageService(){
return new MessageService(callRepository());
}
@Bean
public CallRepository callRepository(){
return new CallRepository();
}
}
이처럼 CallRepository는 2개의 각각 다른 인스턴스가 생성되는 것처럼 보인다. 그러나 스프링 컨테이너는 각각의 인스턴스를 생성하는 것이 아닌 하나의 인스턴스를 공유하게 해준다.
그러면 빈으로 등록된 CallRepository, MessageService의 callRepository, CallService의 callRepository 총 3번 호출되는 것일까?
스프링 컨테이너는 @Configuration을 붙인 클래스를 등록할 때 바이트코드 조작 라이브러리인 CGLIB을 이용하여 @Configuration이 붙은 클래스를 상속받은 임의의 다른 클래스를 생성하여 이 클래스를 스프링 빈으로 등록한다.
위의 예를 보면 Config 클래스에는 @Configuration 어노테이션이 붙어있다. 스프링 컨테이너는 컴포넌트 스캔으로 통해 이를 스프링 컨테이너에 등록할때 CGLIB을 통해 Config를 상속받은 클래스 Config@CGLIB을 등록한다.
이 클래스 안에는 @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.
//Config@CGLIB 코드 예시
@Bean
public CallService callService() {
if(callRepository가 이미 스프링이 들어있으면?){
return 스프링 컨테이너에서 찾아서 반환;
} else(없으면?) {
기존 로직을 통해 callRepository 스프링에 등록
return 반환
}
}
이처럼 스프링 컨테이너가 작동하여 싱글톤이 유지된다.
그러나 @Bean만을 통해서 스프링 컨테이너에 인스턴스를 등록하면 총 3번이 호출되기 때문에 각각 다른 인스턴스를 생성하게 되어 싱글톤이 깨지게 된다.
따라서 @Bean만으로 스프링 컨테이너에 등록한다는 것은 싱글톤이 깨질 위험성이 높기 때문에 스프링 설정 정보는 항상 @Configuration 사용하면 될 것이다.
참고
김영한의 스프링 핵심원리 - 기본편