배움 __IL/addtionalBackEnd

스프링과 싱글톤: 효율성과 안정성의 조화

Mo_bi!e 2025. 1. 12. 20:17

I. 들어가며

스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다(유일하게 하나만 등록해서 공유한다) 따라서 같은 스프링 빈이면 모두 같은 인스턴스. 설정으로 싱글톤이 아니게 설정할 수 있지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용한다.

 

스프링 기본기를 공부하던 중 다음 문장을 보고 왜 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록하는지 의문이 있었다. 스프링과 싱글톤의 관계를 탐색하고, 웹 어플리케이션 관점에서 특히 싱글톤이 왜 필요한지 그리고 싱글톤 패턴 그 자체와 주의사항에 대해서 알아보겠다.

 

아울러 시작 전 질문을 한다. 웹 애플리케이션은 수많은 요청을 처리하기 위해 다수의 객체를 필요로 한다. 하지만 매 요청마다 객체를 새로이 생성한다면 어떤 문제가 발생할까?

 

II. 싱글톤 컨테이너

1. 정의

스프링 컨테이너의 bean 관리방식 중 하나이다. bean을 기본적으로 싱글톤 스코프로 관리하여 애플리케이션 전반에서 하나의 인스턴스만 생성되록 보장한다.

 

2. 주요 역할

- 동일한 빈 요청 시, 동일한 인스턴스를 반환하여 리소스를 절약하고 성능을 최적화

어떤환경에서 리소스를 절약하고 성능을 최적화 하는것이 필요할까 웹 어플리케이션 관점에서 자세히 살펴보겠다.

 

- 스프링 컨테이너가 내부적으로 싱글톤을 구현하고 관리하는 방식

왜 내부적으로 구현하고 관리할까? 그 이유는 싱글톤 패턴의 기존 한계점인 OCP, DIP, 테스트, private 생성자관련 문제가 있기 때문이다. 이후 싱글톤 패턴 자체에서 자세히 살펴보겠다.

 

3. 상황 소개

bean1, 2, 3, 4 각각은 단일한 Singleton Bean 을 요청하여 스프링 컨테이너가 동일한 bean을 재사용하는 것으로 이해할 수있다. 이렇게 된다면 위 주요 역할처럼 리소스를 절약하고 성능을 최적화 할 수있다.

 

앞서 언급한 것 처럼 싱글턴 컨테이너는 특히 웹 어플리케이션 환경에서 더 큰 이점을 제공한다. 어떤 점 때문일까?

 

III. 웹 어플리케이션과 싱글톤

웹 어플리케이션은 보편적으로 다수의 사용자가 동시에 요청을 한다. 요청 마다 새로운 객체를 생성한다면 리소스 낭비가 크다.

void pureContainer() {
	AppConfig appConfig = new AppConfig();
    
    //1. 조회: 호출할 때 마다 객체를 생성
    MemberService memberService1 = appConfig.memberService();
    //2. 조회: 호출할 때 마다 객체를 생성
    MemberService memberService2 = appConfig.memberService();
    
    //참조값이 다른 것을 확인
    System.out.println("memberService1 = " + memberService1); 
    System.out.println("memberService2 = " + memberService2);
    
    // 만약 1000개라면은 리소스 낭비가 어떨까?
    
}

위 상황처럼 '호출할 때 마다 객체를 생성' 한다면 순수 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로이 생성한다.

고객 트래픽이 초당 1,000이면 초당 1,000개의 객체가 생성되고 소멸이 된다. 메모리 낭비는 매우 심하다.

 

하지만 싱글톤 컨테이너는 이러한 다중 요청(100개, 1000개, 10000개 상관없이)을 처리하면서도 동일한 객체를 재사용함으로써 성능과 리소스 사용을 최적화 하여 효율적으로 이용이 가능하다. 즉 요청이 많아도 하나의 객체를 공유하여서 안정적으로 애플리케이션 운영이 가능하다.

 

즉 정리하면 HTTP 요청마다 새로운 객체를 생성하면 GC(가비지 컬렉션)가 자주 발생하고, CPU 및 메모리 사용량이 급증하여 서버 성능이 저하된다. 싱글톤 컨테이너는 이러한 문제를 해결한다.

 

그런데 싱글톤이라는 개념은 스프링안의 아이디어가 아니고, 디자인 패턴인 싱글톤 패턴을 스프링이 활용한 것이다. 싱글톤 패턴이 무엇인지 살펴보겠다.

 

IV. 싱글톤 패턴

1. 정의

클래스의 인스턴스가 1개만 생성되는 것을 보장하고 전역적으로 접근 가능하게 만드는 디자인 패턴이다. 

2. 싱글톤 패턴 구현

public class Singleton {

    // 1. 클래스 내부에 유일한 인스턴스를 정적으로 생성
    private static Singleton instance;

    // 2. 외부에서 인스턴스를 생성하지 못하도록 private 생성자
    private Singleton() {
        // 생성자를 private으로 선언해 외부에서 접근하지 못하게 막음
    }

    // 3. 정적 메서드로 인스턴스를 반환
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton(); // 처음 호출 시 인스턴스 생성
        }
        return instance; // 이후에는 동일한 인스턴스를 반환
    }

    // 4. 싱글톤 클래스의 예제 메서드
    public void showMessage() {
        System.out.println("Hello, Singleton!");
    }
}
public class Main {
    public static void main(String[] args) {
        // Singleton 인스턴스 가져오기
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();

        // 메서드 호출
        singleton1.showMessage();

        // 두 인스턴스 비교
        if (singleton1 == singleton2) {
            System.out.println("singleton1과 singleton2는 같은 인스턴스입니다.");
        } else {
            System.out.println("singleton1과 singleton2는 다른 인스턴스입니다.");
        }
    }
}
Hello, Singleton!
singleton1과 singleton2는 같은 인스턴스입니다.

이렇게 구현이 번거롭다.

 

3. 싱글톤 패턴의 문제점

싱글톤 패턴을 직접 구현하기에는 번거롭다. 또한 앞서 언급한것 처럼 'OCP, DIP, 테스트, private 생성자관련 문제'가 있다. 즉 의존관계상 클라이언트가 구체 클래스를 의존하는 DIP원칙에 위반하는 상황이 발생한다. 더욱이 클라이언트가 구체 클래스에 의존해서 OCP원칙을 위반할 가능성이 높은 등이 있다.

 

- 즉 싱글톤 패턴은 클라이언트 코드가 구체 클래스에 의존하므로 DIP(Dependency Inversion Principle)를 위반하고

- 또한 기존 코드를 변경하지 않고 확장할 수 있어야 하는 OCP(Open-Closed Principle)에도 어긋난다.

 

결국 이러한 방식은 유연성이 떨어지며 안티패턴이라고도 부른다

 

이러한 부분을 개선하기 위한 방식이 결국 싱글턴 컨테이너이다. 스프링 컨테이너가 DI를 하면서 DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용이 가능해졌다.

void springContainer() {
     ApplicationContext ac = new
	 AnnotationConfigApplicationContext(AppConfig.class);
	//1. 조회: 호출할 때 마다 같은 객체를 반환
	MemberService memberService1 = ac.getBean("memberService", MemberService.class);
	//2. 조회: 호출할 때 마다 같은 객체를 반환
	MemberService memberService2 = ac.getBean("memberService", MemberService.class);
	
	//참조값이 같은 것을 확인
	System.out.println("memberService1 = " + memberService1); 
	System.out.println("memberService2 = " + memberService2);
	
	//memberService1 == memberService2
	assertThat(memberService1).isSameAs(memberService2);
 }

 

V. 실무관점에서 싱글톤의 주의점

1. 문제점

싱글톤 패턴, 싱글톤 컨테이터 무엇이든 객체 인스턴스를 하나만 생성해서 공유하기 때문에 싱글톤 객체는 상태를 stateful하게 설계하면 안된다. 즉 무상태(stateless)로 설계해야한다.

2. stateless란

객체가 특정 요청이나 특정 사용자에 종속적인 상태 정보를 저장하지 않는 설계를 의미한다. 즉 주요 목적은 여러 스레드가 동시에 하나의 객체를 사용해도 안전하게 동작해야한다.

 

3. 왜 필요한가?

하나의 인스턴스를 모든 요청에 공유하기 때문에 상태를 가진 필드가 있다면 , 요청 간 데이터 충돌 발생이 가능하다.  주요한 해결책으로는 상태는 지역변수, 매개변수로 처리해야한다.

 

이러한 방식으로 한다면 여러 스레드가 한번에 접근하여 발생한는 동시성문제, 락없이 동작하여 가능한 성능최적화 문제 등이 해결이된다.