본문 바로가기
Spring/Common

[Spring] 스프링 싱글톤 컨테이너: 왜 모든 빈은 '하나'여야 할까?

by coding_whale 2026. 5. 9.
반응형

웹 애플리케이션은 보통 여러 고객이 동시에 요청을 보내는 구조다. 만약 고객이 요청을 보낼 때마다 서비스 객체를 새로 생성한다면 어떤 일이 벌어질까?
오늘은 자바의 싱글톤 패턴과 스프링이 제공하는 싱글톤 컨테이너, 그리고 실무 주의사항에 대해 깊이 있게 알아본다.

 

1. 웹 애플리케이션과 객체 생성의 딜레마

기업용 웹 애플리케이션은 수많은 사용자의 요청을 동시에 처리해야 한다. 우리가 이전에 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청이 올 때마다 새로운 객체를 생성한다.

결과적으로 메모리 낭비가 심해진다. 이를 해결하기 위해 해당 객체를 딱 1개만 생성하고 공유하도록 설계하는 것이 바로 싱글톤 패턴(Singleton Pattern)이다.

 

2. 싱글톤 패턴 (Singleton Pattern)

싱글톤 패턴은 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다. 이를 위해 외부에서 new 키워드를 사용하지 못하도록 생성자를 private으로 막아야 한다.

🔍 싱글톤 서비스 구현 예시

public class SingletonService {
    // 1. static 영역에 객체를 딱 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();
    
    // 2. public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용한다.
    public static SingletonService getInstance() {
        return instance;
    }
    
    // 3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 막는다.
    private SingletonService() {
    }
    
    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }
}

이렇게 설계하면 아무리 많은 요청이 와도 이미 생성된 하나의 인스턴스를 효율적으로 재사용할 수 있다. 하지만 순수 자바의 싱글톤 패턴에는 치명적인 단점들이 존재한다.

⚠️ 싱글톤 패턴의 문제점

  1. 코드 복잡성: 패턴을 구현하기 위한 보일러플레이트 코드가 많이 들어간다.
  2. DIP 위반: 클라이언트가 구체 클래스의 getInstance()를 호출해야 하므로 추상화가 아닌 구체 클래스에 의존하게 된다.
  3. 유연성 부족: 내부 속성을 변경하거나 테스트하기가 어렵고, private 생성자 때문에 자식 클래스를 만들기 어렵다. 결론적으로 유연성이 떨어져 '안티 패턴'이라 불리기도 한다.

 

 

3. 스프링 싱글톤 컨테이너의 마법

스프링 컨테이너는 위의 지저분한 싱글톤 패턴 코드를 작성하지 않아도 객체를 싱글톤으로 관리해준다. 스프링 빈(Bean)으로 등록되는 순간, 컨테이너는 이를 딱 하나만 생성해서 '싱글톤 레지스트리'에 보관한다.

✅ 스프링 컨테이너 테스트

@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    
    // 1. 조회: 호출할 때마다 같은 객체를 반환하는지 확인
    MemberService memberService1 = ac.getBean("memberService", MemberService.class);
    MemberService memberService2 = ac.getBean("memberService", MemberService.class);
    
    // 참조값이 같은 것을 확인
    System.out.println("memberService1 = " + memberService1);
    System.out.println("memberService2 = " + memberService2);
    
    // memberService1 == memberService2
    assertThat(memberService1).isSameAs(memberService2);
}

스프링 컨테이너 덕분에 DIP, OCP 위반이나 private 생성자로부터 자유롭게 싱글톤을 사용할 수 있게 되었다.

 

 

4. 실무 필수 주의사항: 무상태(Stateless) 설계

싱글톤 객체는 여러 클라이언트가 하나의 인스턴스를 공유하기 때문에, 절대로 상태를 유지(Stateful)하게 설계하면 안 된다. 만약 특정 클라이언트에 의존적인 필드가 있다면 큰 장애가 발생한다.

❌ 상태 유지(Stateful) 시 발생하는 사고 예시

public class StatefulService {
    private int price; // 상태를 유지하는 필드 (위험!)
    
    public void order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);
        this.price = price; // 여기서 다른 사용자(Thread)가 값을 덮어쓸 수 있음!
    }
    
    public int getPrice() {
        return price;
    }
}
  • 내용: 사용자 A가 10,000원을 주문했는데, 사용자 B가 중간에 20,000원을 주문하여 사용자 A의 결제 금액이 20,000원으로 바뀌어버리는 과정.
  • 상세: Thread-A와 Thread-B가 공유 필드 price에 접근하는 시점을 타임라인으로 구성하여 정합성 문제가 발생하는 순간을 강조.

 

💡 해결책: 무상태(Stateless) 설계 원칙

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

 

 

5. 요약 및 결론

  1. 웹 환경은 동시 요청이 많다: 메모리 낭비를 막기 위해 객체 재사용(싱글톤)은 필수다.
  2. 스프링은 편리하다: 싱글톤 패턴의 단점을 해결하면서 빈을 싱글톤으로 안전하게 관리해준다.
  3. 무상태 설계가 핵심이다: 싱글톤 객체에서 필드에 값을 저장하는 행위는 멀티스레드 환경에서 시한폭탄과 같다. 반드시 지역변수 등을 활용해 무상태로 설계하자.
반응형