[스프링 빈과 싱글톤 패턴: 무상태로 설계해야 하는 이유??]
1. 싱글톤 패턴과 스프링 빈
스프링 프레임워크는 객체를 싱글톤 패턴 방식으로 관리한다. 싱글톤은 객체 인스턴스를 단 하나만 생성하고, 이를 여러 클라이언트와 공유하기 때문에 메모리 사용을 절약할 수 있다는 장점이 있다.
그러나 싱글톤 방식에서 공유되는 객체를 상태를 유지(stateful) 하게 설계하면 큰 문제가 발생할 수 있다.
2. 상태를 유지하는 객체의 문제점
싱글톤 객체는 하나의 인스턴스를 여러 클라이언트가 공유한다. 이때 상태를 유지하는 필드가 있다면 다음과 같은 문제가 발생한다.
- 특정 클라이언트에 의존적인 필드가 생긴다.
- 필드를 변경할 수 있는 클라이언트가 값을 덮어쓰게 된다.
- 여러 클라이언트가 동시에 객체를 사용할 경우 데이터 불일치 현상이 발생한다.
따라서 싱글톤 객체는 무상태(stateless) 로 설계해야 한다. 상태를 저장하지 않고 읽기만 가능하게 설계하거나 지역변수, 파라미터, ThreadLocal 등을 활용해야 한다.
3. 상태를 유지한 경우의 예시
다음은 상태를 유지하는 객체의 문제점을 보여주는 예시 코드다.
StatefulService.java 예시
package hello.core.singleton;
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라는 필드를 통해 상태를 저장한다. 이제 이 객체를 스프링 빈으로 등록하고 테스트해보자.
4. 상태 유지로 인한 문제 테스트
StatefulServiceTest.java 예시
package hello.core.singleton;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
public class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
// ThreadA: A사용자 10000원 주문
statefulService1.order("userA", 10000);
// ThreadB: B사용자 20000원 주문
statefulService2.order("userB", 20000);
// 사용자A의 주문 금액 조회
int price = statefulService1.getPrice();
System.out.println("price = " + price);
// 예상 결과는 10000원이지만, 실제 결과는 20000원
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
5. 문제 상황 설명
- ThreadA는 userA의 주문으로 10,000원을 입력했다.
- ThreadB는 userB의 주문으로 20,000원을 입력했다.
하지만 두 클라이언트가 같은 StatefulService 객체를 공유하기 때문에 price 필드가 덮어쓰기 된다. 결과적으로 ThreadA가 조회한 가격은 10,000원이 아닌 20,000원이 출력된다.
이런 문제가 실무에서 발생하면 매우 해결하기 어렵다. 특히 다수의 사용자와 동시 요청이 많은 환경에서는 치명적인 장애로 이어질 수 있다.
6. 해결 방법: 무상태(stateless) 설계
싱글톤 객체는 무상태(stateless) 로 설계해야 한다.
- 필드에 값을 저장하지 않는다.
- 지역변수나 파라미터를 사용해 상태를 유지한다.
무상태 예시:
public class StatelessService {
public int order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
return price; // 값을 반환한다.
}
}
무상태 설계 특징:
- 필드가 없다: 더 이상 price와 같은 필드를 사용하지 않는다.
- 메서드 내부에서 상태를 저장하지 않는다: 모든 값은 메서드 호출 시 파라미터로 처리된다.
- 여러 클라이언트가 동시에 호출해도 안전: 필드 공유가 없기 때문에 동시성 문제나 데이터 불일치가 발생하지 않는다.
테스트 코드도 수정필요.
@Test
void statefulServiceSingleton(){
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// ThreadA: A사용자가 10000원을 주문
int userAPrice = statefulService1.order("userA", 10000);
// ThreadB: B사용자가 20000원 주문
int userBPrice = statefulService2.order("userB", 20000);
//ThreadA: 사용자A 주문 금액 조회
// int price = statefulService1.getPrice();
System.out.println("price = " + userAPrice);
// Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
총 정리: 스프링 빈은 항상 무상태로 설계하자
스프링의 싱글톤 빈은 여러 클라이언트가 하나의 객체를 공유하기 때문에 상태를 유지하는 설계는 절대 금물이다.
필드를 통해 상태를 저장하면 데이터 불일치, 동시성 문제 등 해결하기 어려운 문제가 발생한다.
스프링 빈은 항상 무상태(stateless)로 설계해야 한다.
안정적이고 확장 가능한 시스템을 만들기 위해 반드시 이를 준수하자!
'JAVA > Spring' 카테고리의 다른 글
Spring에서 생성자에 @Autowired 어노테이션을 사용하는 이유와 장점 (0) | 2024.12.20 |
---|---|
[스프링 싱글톤] : 왜 @Bean은 한 번만 호출될까? (0) | 2024.12.20 |
AssertThat의 isEqualTo와 isSameAs의 차이점 알아보기 (0) | 2024.12.17 |
스프링 BeanDefinition 완벽 정리: 빈 설정 메타정보 탐구 (0) | 2024.12.16 |
스프링 빈 조회: 부모 타입과 Object 타입의 차이점 (0) | 2024.12.16 |
댓글