본문 바로가기

back-end/spring

spring/ 싱글톤 컨테이너

 

싱글톤 Singleton

전체 프로그램에서 단 하나의 객체만 생성되도록 하는 경우, 생성된 단 하나의 객체를 싱글톤이라고 한다.

 

//자바 코드로 구현하는 싱글톤

 

public class Singleton {
    //static 영역에 객체를 1개 생성한다 (private)
    private static Singleton singleton = new Singleton;
    
    //생성자의 외부 호출을 막기 위한 private 생성자 선언
    private Singleton() {}
    
    //외부에서 호출 가능한 getInstance() 메서드 선언    
    static Singleton getInstance() {
    	return singleton;
    }
}

 

싱글톤이 필요한 이유

웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다. 이 경우, 싱글톤이 보장되지 않으면 고객의 요청 수에 따라 무수히 많은 객체가 생성된다. 이렇게 낭비되는 메모리를 막기 위해 객체 생성을 단 한 개만 보장하는 싱글톤 패턴을 사용하고, 싱글톤 패턴을 공유하도록 설계한다.

 

*  스프링에서는 이러한 싱글톤 패턴을 따로 적용하지 않아도 자체적으로 객체 인스턴스를 싱글톤으로 관리한다.

 


싱글톤 컨테이너

스프링 컨테이너는 스프링 빈을 등록할 때 @Bean이 적용된 항목을 따라 객체를 생성한다. 객체를 생성할 때 이미 생성이 되었다면 다시 생성하지 않고 싱글톤을 유지한다. 뿐만 아니라 기존 자바 코드와 같이 추가적인 설계 과정이 필요하지 않고 스프링 컨테이너 자체에서 DIP, OCP를 지키면서 테스트도 용이하다. 자바 코드로 작성되는 싱글톤이 private 접근 제한자를 사용해 외부의 접근과 상속이 불가능했던 점 역시 스프링 컨테이너를 사용하면 걱정하지 않아도 된다.

스프링 컨테이너에서 이렇듯 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다.

 


싱글톤 사용 시 주의점

@Bean을 통해 스프링 빈으로 등록된 객체들은 무상태Stateless로 설계되어야 한다.

스프링 컨테이너를 사용하는 경우, 모든 객체는 유일하게 존재한다. 이는 곧 모든 클라이언트가 하나의 싱글톤 객체를 공유한다는 것을 말한다. 만약 빈으로 등록하는 객체를 Stateful하게 설계한다면 클라이언트1이 한 필드를 사용하고, 이어서 클라이언트2가 또 같은 필드를 사용한다면 원하는 값을 반환하지 못하는 문제를 마주할 수 있다.

 

따라서 다음을 항상 주의하며 스프링 빈을 설계한다.

  • 특정 클라이언트에 의존적인 필드가 있으면 안 된다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안 된다.
  • 가급적 읽기만 가능해야 한다.
  • 필드 대신에 자바에서 공유되지 않는(Stateless한) 지역변수, 파라미터, ThreadLocal 등을 사용한다.

스프링 컨테이너가 싱글톤을 보장하는 방법

스프링 컨테이너는 싱글톤 레지스트리이다. 즉, 자체적으로 싱글톤 객체를 생성하고 관리한다. 

 

@Configuration
public class AppConfig {
    @Bean //(1)
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean //(2)
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), pointPolicy());
    }

    @Bean //(3)
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean //(4)
    public PointPolicy pointPolicy() {
        return new FixPointPolicy();
    }
}

 

위와 같은 AppConfig 클래스가 있다고 할 때, 코드 상으로 memberRepository()는 총 3번 호출된다. 하지만 스프링 컨테이너는 memberRepository()를 3번 호출하지 않는다.

 

이는 @Configuration으로 등록된 클래스를 바이트 코드 조작을 통해 사용하기 때문에 가능하다.

 

스프링은 싱글톤을 유지하기 위해 CGLIB라는 바이트코드 조작 라이브러리를 사용한다.

 

@Test
void configurationDeep() {
    //AppConfig를 구성 정보로 하는 스프링 컨테이너 생성
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    
    AppConfig been = ac.getBean(AppConfig.class);
    
    //스프링 빈 AppConfig가 참조하는 클래스 출력
    System.out.println(bean.getClass());
}

따라서 다음과 같이 작성된 코드의 출력은 다음과 같이 class review.blog.AppConfig가 아닌 EnhancerBySpringCGLIB가 붙는 것을 볼 수 있다. 

class review.blog.AppConfig$$EnhancerBySpringCGLIB$$dd98cc83

 

즉, 스프링 컨테이너는 기존의 AppConfig 클래스를 사용하는 것이 아니라 바이트 코드 조작 라이브러리를 사용해 AppConfig 클래스를 상속받는 임의의 다른 클래스를 만들고, 그 클래스를 원본 AppConfig 클래스 대신 스프링 빈으로 등록한다.

 

스프링 컨테이너는 @Configuration이 붙은 클래스를 스프링 빈으로 등록 후, @Bean이 붙은 메서드를 순회하면서 스프링 컨테이너에 등록되지 않은 것들만 골라 스프링 빈으로 생성해 등록한다. 이 과정에서 싱글톤이 보장된다.