[Spring] 스프링 싱글톤에서 상태를 유지하면 안 되는 이유

2025. 2. 13. 11:56코딩 도구/백엔드 개발 (Backend Development)

반응형

스프링 싱글톤에서 상태를 유지하면 안 되는 이유

스프링은 기본적으로 싱글톤 컨테이너를 사용하여 객체를 관리한다. 따라서 하나의 객체 인스턴스를 모든 클라이언트가 공유하게 된다. 이러한 특성 때문에 싱글톤 객체는 반드시 무상태(stateless)로 설계해야 한다.

1. 싱글톤에서 상태를 유지하면 발생하는 문제

싱글톤 객체에서 인스턴스 변수를 사용하여 상태를 유지하면, 여러 사용자가 동시에 접근할 때 데이터가 꼬일 수 있다.

예제 코드: 상태를 유지하는 서비스 객체

public class StatefulService {
    private int price; // 상태를 유지하는 필드

    public void order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);
        this.price = price; // 공유되는 필드 <- 이부분이 문제가 된다.
    }

    public int getPrice() {
        return price;
    }
}

위 코드에서 StatefulService 객체는 price 값을 필드에 저장한다. 여러 사용자가 이 객체를 공유하는 상황에서 문제가 발생할 수 있다.

테스트 코드: 문제 발생 확인

@Test
void statefulServiceSingleton() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
    
    StatefulService service1 = ac.getBean(StatefulService.class);
    StatefulService service2 = ac.getBean(StatefulService.class);

    // ThreadA: A 사용자가 10000원 주문
    service1.order("userA", 10000);
    
    // ThreadB: B 사용자가 20000원 주문
    service2.order("userB", 20000);

    // ThreadA: 사용자 A의 주문 금액 조회
    int price = service1.getPrice();
    System.out.println("price = " + price); // 기대값: 10000, 실제값: 20000
}

위 코드에서 service1service2는 같은 객체를 참조하므로, 마지막 주문한 사용자(userB)의 금액이 userA에게도 영향을 주게 된다. 결과적으로 userA는 본인이 주문한 10000원이 아닌 20000원이 저장된 것을 확인하게 된다.

 

실무에서는 이러한 문제가 결제 오류, 잘못된 데이터 저장, 동시성 문제로 이어질 수 있다고 한다.

2. 해결 방법: 무상태(stateless)로 설계

싱글톤 객체는 공유 필드를 사용하지 않도록 설계해야 한다. 필드에 데이터를 저장하는 대신, 메서드 호출 시 파라미터를 사용하여 데이터를 전달해야 한다.

상태를 유지하지 않는 설계

public class StatelessService {
    public int order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);
        return price; // 필드 대신 반환값 사용
    }
}

테스트 코드에서 order 메서드가 price 값을 반환하도록 변경하면, 각 호출이 독립적으로 동작하게 된다.

@Test
void statelessServiceSingleton() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
    StatelessService service = ac.getBean(StatelessService.class);

    int priceA = service.order("userA", 10000);
    int priceB = service.order("userB", 20000);

    System.out.println("userA price = " + priceA); // 10000
    System.out.println("userB price = " + priceB); // 20000
}

이렇게 하면 각 요청이 독립적으로 동작하며, 서로 영향을 주지 않게 된다.

3. 실무에서 주의할 점

  1. 싱글톤 객체에서는 인스턴스 변수를 사용하지 않는다.
    • 상태를 가지면 동시성 문제가 발생할 가능성이 높다.
  2. 객체의 상태를 유지해야 한다면 별도의 객체를 사용한다.
    • 상태 정보가 필요한 경우, 요청 단위 객체(Request Scope) 또는 DTO를 사용한다.
  3. 멀티스레드 환경에서는 ThreadLocal 등을 활용할 수 있다.
    • 사용자별로 데이터를 저장할 필요가 있다면 ThreadLocal을 사용할 수도 있다.

4. 쉽게 이해하는 예시(?)

예제 1: 식당에서 주문하는 방식

  • 잘못된 방식 (싱글톤 상태 유지) :
    • 한 식당에서 모든 손님의 주문을 하나의 종이에 적는다.
    • 마지막 손님의 주문이 종이에 덮어씌워져 이전 손님의 주문이 사라진다.
  • 올바른 방식 (무상태) :
    • 각 손님에게 개별적으로 주문서를 나눠준다.
    • 각 손님의 주문은 독립적으로 처리된다.

예제 2: ATM에서 출금하는 방식

  • 잘못된 방식 (싱글톤 상태 유지) :
    • ATM 기계가 최근 출금 금액을 저장한다.
    • 다음 사람이 출금할 때, 이전 사람의 출금 금액이 영향을 준다.
  • 올바른 방식 (무상태) :
    • ATM은 출금 요청을 처리할 때마다 개별적으로 데이터를 관리한다.
    • 각 요청이 독립적으로 처리되므로 문제 발생 가능성이 없다.

5. 결론

스프링에서 싱글톤 빈을 사용할 때 항상 무상태(stateless)로 설계해야 한다. 공유 필드를 사용하면 동시성 문제와 데이터 불일치 오류가 발생할 가능성이 크다. 이를 방지하려면 필드 대신 지역 변수, 파라미터, DTO를 활용하는 것이 중요하다.

실무에서도 싱글톤 상태 유지 문제는 자주 발생하며, 결제 시스템이나 사용자 데이터 처리에서 치명적인 오류를 유발할 수 있다. 따라서 싱글톤 객체를 설계할 때는 항상 상태를 유지하지 않는 방식으로 개발하는 것이 핵심 원칙이다.

 

***

이 글의 코드와 이론은 인프런 김영한님의 "스프링 핵심 원리 - 기본편" 을 수강하고 작성하게 되었다. 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

 

스프링 핵심 원리 - 기본편 강의 | 김영한 - 인프런

김영한 | , 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보세요! 📢 수강 전 확인해주세요! 본 강의는 자바 스프링 완전 정복 시리즈의 두 번째 강의입니다. 우아한형제들 최연

www.inflearn.com

***

반응형