본문 바로가기

SPRING/Spring

[스프링| 스프링 핵심 원리 | 기본편 | 싱글톤 컨테이너] 싱글톤 컨테이너, 싱글톤 방식의 주의점

@Test // JUnit5에서 테스트 메서드임을 나타내는 어노테이션
@DisplayName("스프링 컨테이너와 싱글톤") // 테스트 메서드의 이름 또는 설명을 지정
void springContainer() { // 테스트 메서드 선언
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); // 스프링 컨테이너를 초기화하고, AppConfig 클래스를 설정 정보로 사용

    //1. 조회: 호출할 때 마다 같은 객체를 반환
    MemberService memberService1 = ac.getBean("memberService", MemberService.class); // 스프링 컨테이너에서 "memberService" 이름으로 MemberService 타입의 빈(bean) 객체를 조회
    //2. 조회: 호출할 때 마다 같은 객체를 반환
    MemberService memberService2 = ac.getBean("memberService", MemberService.class); // 동일한 이름과 타입으로 빈 객체를 다시 조회

    //참조값이 같은 것을 확인
    System.out.println("memberService1 = " + memberService1); // 첫 번째 조회한 객체의 참조값 출력
    System.out.println("memberService2 = " + memberService2); // 두 번째 조회한 객체의 참조값 출력
    //memberService1 == memberService2
    assertThat(memberService1).isSameAs(memberService2); // 두 객체가 동일한 참조(싱글톤)인지 확인
}

스프링 프레임워크는 기본적으로 '싱글톤 컨테이너'로 작동하여, 애플리케이션 내에서 빈(bean) 인스턴스를 싱글톤으로 관리합니다. 이는 각각의 스프링 빈이 기본적으로 각각 하나씩만 생성되고, 이 인스턴스가 공유되어 사용되는 것을 의미합니다.

싱글톤 컨테이너의 주요 특징은 다음과 같습니다:

  • 메모리 효율성: 각 빈에 대해 오직 하나의 인스턴스만 생성되므로 메모리 사용이 효율적입니다.
  • 공유 인스턴스: 모든 클라이언트가 같은 인스턴스를 공유하기 때문에 애플리케이션 내에서 상태 정보를 공유하기 쉽습니다.
  • 설정과 관리의 용이성: 스프링 컨테이너가 빈의 생명 주기를 관리하므로 개발자는 객체 생성과 의존성 주입 등에 대한 부담을 덜 수 있습니다.

싱글톤 패턴이 가지는 몇 가지 단점(예: 상태 관리의 복잡성)도 있지만, 스프링은 이러한 문제를 해결하기 위한 다양한 방법(예: 스코프 지정, 프록시 패턴 등)을 제공합니다.

public class StatefulService { // StatefulService 클래스 선언

    private int price; // 상태를 유지하는 필드, 싱글톤 사용 시 문제를 일으킬 수 있음

    public void order(String name, int price) { // 주문을 받는 메서드, 이름과 가격을 인자로 받음
        System.out.println("name = " + name + " price = " + price); // 주문 정보를 콘솔에 출력
        this.price = price; // 클래스 필드 price에 주문 가격을 저장, 여러 요청에서 상태 공유 문제 발생 가능
    }

    public int getPrice() { // 저장된 가격을 반환하는 메서드
        return price; // 현재 저장된 가격을 반환
    }
}

문제점 및 주의사항

이 코드에서 price 필드는 객체의 상태를 나타내며, 여러 클라이언트가 동일한 서비스 인스턴스를 사용할 경우 price 필드 값이 예측하지 못한 방식으로 변경될 수 있습니다. 싱글톤 패턴에서는 하나의 인스턴스만 생성되므로, 여러 클라이언트가 동일한 인스턴스의 price를 공유하게 됩니다. 따라서 한 클라이언트의 데이터 변경이 다른 클라이언트에 영향을 줄 수 있어, 싱글톤 서비스에서는 상태를 유지하는 필드 사용을 피해야 합니다.

해결 방법

상태 정보를 가지고 있지 않은 무상태(stateless) 설계로 변경하는 것이 바람직합니다. 예를 들어, price 필드 대신에 메서드 로컬 변수를 사용하거나, 계산 결과를 바로 반환하는 방식으로 코드를 구조화할 수 있습니다. 이렇게 하면 각 클라이언트 요청마다 독립적인 상태를 유지하게 되어, 서로 영향을 주지 않게 됩니다.

public class StatefulServiceTest { // StatefulServiceTest 클래스 선언

    @Test // JUnit 테스트 메서드를 나타내는 어노테이션
    void statefulServiceSingleton() { // 테스트 메서드 이름
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class); // 스프링 컨텍스트 초기화 및 TestConfig 클래스를 사용하여 빈 설정
        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class); // StatefulService 빈을 가져옴
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class); // 동일한 StatefulService 빈을 다시 가져옴

        //ThreadA: A사용자 10000원 주문
        statefulService1.order("userA", 10000); // 첫 번째 서비스 인스턴스로 사용자 A의 주문 처리
        // ThreadB: B사용자 20000원 주문
        statefulService2.order("userB", 20000); // 두 번째 서비스 인스턴스(실제로는 같은 인스턴스)로 사용자 B의 주문 처리

        //ThreadA: 사용자A 주문 금액 조회
        int price = statefulService1.getPrice(); // 사용자 A가 주문한 금액을 조회
        //ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
        System.out.println("price = " + price); // 실제 출력되는 금액을 콘솔에 표시
        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000); // 주문 금액이 20000원인지 확인
    }

    static class TestConfig { // 테스트를 위한 설정 클래스
        @Bean // 스프링 빈으로 등록하기 위한 어노테이션
        public StatefulService statefulService() { // StatefulService 빈을 생성하는 메서드
            return new StatefulService(); // StatefulService 인스턴스 생성 및 반환
        }
    }
}

싱글톤에서의 상태 공유 문제

이 코드는 StatefulService 인스턴스가 싱글톤으로 관리되고 있기 때문에, 여러 스레드(사용자)가 동일 인스턴스의 상태(price)를 공유하게 되어 문제가 발생합니다. 이 테스트는 스프링 컨테이너에서 싱글톤 빈이 어떻게 동작하는지를 보여주고, 상태를 가진 싱글톤 빈의 사용이 왜 문제가 될 수 있는지를 시연합니다. 사용자 A가 주문한 후에 사용자 B가 주문하면, A의 결과가 B의 주문 값으로 덮어쓰여지게 되는 것을 확인할 수 있습니다. 이는 싱글톤 패턴을 사용할 때 상태를 공유하지 않도록 설계해야 하는 이유를 잘 보여줍니다.