4. 싱글톤 컨테이너

2025. 4. 10. 16:21Spring/Core

1. 웹 애플리케이션과 싱글톤 패턴

1) 싱글톤 패턴의 필요성

  • 웹 어플리케이션은 보통 동시에 여러 클라이언트로부터 요청이 들어온다. 
  • 이때, 만약 이전에 우리가 직접 구현한 AppConfig.class를 DI 컨테이너로 사용하면 모든 memberService 요청시마다 새로운 객체를 생성해서 사용한다. 
  • 이렇게 매 요청마다 객체를 새로 생성하는 것은 메모리 낭비가 심해 효율적인 설계라 할 수 없다. 
  • 해결방안은 하나의 클래스마다 객체를 1개만 생성하도록 하고 요청마다 해당 객체를 공유하도록 설계하면 된다. → 이것이 싱글톤 패턴

2) 싱글톤 패턴 구현하기 

public class SingletonService {
    // 1. 클래스가 만들어질 때 객체도 static하게 만들어 놓는다
    // 그리고 해당 클래스의 객체가 필요할 때마다 객체를 계속 만드는게 아니라
    // 처음 만들어놓은 static 객체 하나를 getInstance() 메서드로 불러와서 사용함
    private static final SingletonService instance = new SingletonService();
    public static SingletonService getInstance(){
        return instance;
    }
    //2. 그리고 해당 클래스의 생성자를 private으로 만들어서
    // 다른 클래스에서 생성자를 호출 할 수 없도록, 즉 객체를 생성할 수 없도록 한다.
    private SingletonService(){}

    public void logic(){
        System.out.println("싱글톤 객체 로직 호출");
    }
}

@Test
@DisplayName("직접 구현한 싱글톤 패턴을 적용한 객체 사용")
void singletonServiceTest() {
    // 1. private하게 만든 생성자는 외부에서 호출할 수 없음
    //SingletonService singletonService = new SingletonService();

    // 2. getInstance() 메서드를 통해서 미리 만들어 놓은 static 인스턴스 가져와서 사용하기
    SingletonService singletonService1 = SingletonService.getInstance();
    SingletonService singletonService2 = SingletonService.getInstance();

    // 같은 객체 인스턴스 반환되는 것을 확인할 수 있음
    System.out.println("singletonService1 = " + singletonService1);
    System.out.println("singletonService2 = " + singletonService2);
    
    singletonService1.logic();
}

 

3) 위와 같은 싱글톤 패턴 구현의 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. → DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다. → 안티패턴으로 불리기도 한다! 
 

2.  싱글톤 컨테이너

  • 스프링 컨테이너는 위와 같은 싱글톤 패턴의 문제점을 해결하기 위해서 다른 방식으로 객체 인스턴스를 싱글톤으로 관리한다.
    • HOW?! 스프링 컨테이너가 생성될 때 매개변수로 들어간 구성정보(AppConfig)를 활용하여
      스프링 빈 저장소에 객체를 스프링 빈으로 등록하고 객체를 각각 하나씩만 생성해서 관리한다! 
    • 따라서 요청이 올 때마다 새로 객체를 만들지 않고 스프링 컨테이너가 빈으로 등록해서 가지고 있던 객체를 반환해준다.  
  • 스프링 컨테이너 생성 과정 복습
    (1) 스프링 컨테이너 생성

    → (2) 구성 정보(AppConfig.class) 활용하여 스프링 빈 등록
    → (3) 스프링 빈 의존관계 주입 
  • 위처럼 스프링 컨테이너는 싱글톤 컨테이너 역할을 하고
    스프링 컨테이너가 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다.
  • 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
    • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
    • DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다. 
  • 스프링의 싱글톤 컨테이너 
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void SpringContainer(){
    // 직접 만든 순수한 DI가 아니라 객체를 싱글톤으로 관리해주는 스프링 컨테이너 사용
    //AppConfig appConfig = new AppConfig();
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    // 요청이 올 때마다 새로운 객체를 생성하는 것이 아니라,
    // ac가 등록해서 가지고 있던 객체를 반환해서 사용한다. 
    //MemberService memberService1 = appConfig.memberService();
    //MemberService memberService2 = appConfig.memberService();
    MemberService memberService1 = ac.getBean("memberService", MemberService.class);
    MemberService memberService2 = ac.getBean("memberService", MemberService.class);

    // 같은 객체 인스턴스 반환되는 것을 확인할 수 있음
    System.out.println("memberService1 = "+memberService1);
    System.out.println("memberService2 = "+memberService2);
}

 

3. 싱글톤 방식의 주의점

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은
    여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에,
    스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다!!!
  • 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다. 즉 무상태(stateless)로 설계해야 한다!
    • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다!!! 
    • 가급적 읽기만 가능해야 한다.
    • 필드 대신에 자바에서 공유되지 않는지역변수파라미터, ThreadLocal 등을 사용해야 한다.

 

4. @Configuration과 싱글톤 

1) AppConfig에 대한 의문점 

@Configuration
public class AppConfig {
    static int cnt = 0;
    @Bean
    public MemberService memberService() {
        System.out.println(++cnt + " AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public OrderService orderService() {
        System.out.println(++cnt + " AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    @Bean
    public MemberRepository memberRepository() {
        System.out.println(++cnt + " AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        System.out.println(++cnt + " AppConfig.DiscountPolicy");
        return new FixDiscountPolicy();
    }
}
  • 스프링 컨테이너가 싱글톤 컨테이너라는 것을 배우다 보니 AppConfig 클래스에 의문이 생긴다. 
    스프링 컨테이너 AppConfig의 구성정보를 바탕으로 빈을 생성하고 등록하는데, AppConfig는 자바코드로 new MemoryMemberRepository()를 3번이나 호출한다. 결과적으로 각각 다른 3개의 MemoryMemberRepository가 생성되면서 싱글톤이 깨지는 것처럼 보이는데 과연 어떨까?
  • 스프링 컨테이너를 생성하는 테스트문을 실행시키고, AppConfig의 각각의 메서드에 에 추가해 놓은 프린트문을 확인해보면 아래와 같다. 각 메서드가 여러번 호출 되더라도 실제로 호출되는 것은 단 한번 뿐이고, 결과적으로 생성되는 객체도 1개이므로 싱글톤이 유지되는 것을 확인할 수 있다.
1 AppConfig.memberService
2 AppConfig.memberRepository
3 AppConfig.orderService
4 AppConfig.DiscountPolicy
  • 그렇다면, 도대체 스프링 컨테이너는 어떻게 이런 싱글톤 패턴을 구현한 것일까? 
    → 정답은 바로 AppConfign위에 적용한 @Configuration 이다. 

2) @Configuration의 비밀 

@Test
void configurationDeep(){
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    //ac 생성시 파라미터에 넘긴 값(AppConfig)도 스프링 빈으로 등록 됨
    AppConfig bean = ac.getBean(AppConfig.class);
    System.out.println("bean = " + bean.getClass());
    //bean = class spring.core.AppConfig$$SpringCGLIB$$0
}
  • 스프링 컨테이너인 ApplicationContext를 생성할 때는 파라미터에 구성정보(AppConfig)를 반드시 넘겨줘야한다. 
    그리고 스프링 컨테이너는 넘어온 구성정보도 스프링 빈으로 등록한다. 
  • 이때 구성정보에 @Configuration이 붙어있는 경우,
    스프링 컨테이너는 이 구성정보를 바로 빈으로 등록하지 않고,

    바이트코드 조작 라이브러리인 CGLIB를 사용해서
    해당 클래스를 상속받은 다른 임의의 클래스를 만든다. 

    이 클래스를 출력해보면 클래스 이름 뒤에 $$SpringCGLIB라는 문구가 추가로 붙어있다. 
  • 이렇게 새로 만들어진 클래스의 객체가 스프링 컨테이너의 스프링 빈으로 등록되는데,
    이는 싱글톤 패턴이 구현되도록  바이트코드 단위로 다시 조작되어있다. 
    따라서 파라미터로 넘긴 구성정보에 작성된 자바 코드와 다르게 싱글톤이 지켜지도록 작동하는 것이다!!

  • 만약  AppConfig 폴더에 @Configuration 어노테이션을 사용하지 않고 @Bean만 붙이고
    위의 테스트 코드를 그대로 실행하면 어떻게 출력 될까?
1 AppConfig.memberService
2 AppConfig.memberRepository
3 AppConfig.orderService
4 AppConfig.memberRepository
5 AppConfig.DiscountPolicy
6 AppConfig.memberRepository
7 AppConfig.DiscountPolicy
bean = class spring.core.AppConfig
  • AppConfig의 클래스 이름은 spring.core.AppConfig로 뒤에 $$SpringCGLIB가 붙어있지 않음
    즉, 바이트 단위로 조작된 새로운 클래스가 아닌 진짜 AppConfig 클래스임
  • 생성자가 호출될 때마다 새로운 객체가 생성되어 싱글톤이 지켜지지 않음  
  • 따라서 스프링 컨테이너는 @Configuration 어노테이션을 통해 싱글톤 패턴을 구현하는 것을 확인할 수 있다.