싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든,
객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은
여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에
싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안 된다.
즉 무상태(stateless)로 설계해야 한다.
특정 클라이언트에 의존적인 필드가 있으면 안 된다.
(특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안 된다)
가급적 읽기만 가능해야 한다.
필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다.
상태를 유지할 경우 발생하는 문제점을 테스트 코드를 통해 예시로 살펴보자.
● 상태를 유지할 경우 발생하는 문제점 예시
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;
}
}
가격(price)을 필드로 삼는 클래스를 생성했다.
이제 테스트 코드를 통해 무슨 문제점이 있는지 확인해보자.
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;
import javax.swing.plaf.nimbus.State;
import static org.junit.jupiter.api.Assertions.*;
class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
statefulService1.order("userA", 10000);
statefulService2.order("userB", 20000);
int price= statefulService1.getPrice();
System.out.println("price = " + price);
// Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
Assertions.assertThat(userAPrice).isNotEqualTo(userBPrice);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
임시 스프링 컨테이너(TestConfig)를 생성하여
하나의 statefulService 빈을 공유하는 객체를 두 개 생성하고
각각 다른 price를 넣어 order 메서드를 실행했다.
테스트 코드를 실행해보면..
userA의 주문금액을 출력했더니 10000 이 아닌 20000이 출력되었다.
왜냐하면 생성된 2개의 statefulService는 같은 빈 객체를 공유하기에
한 곳에서 price를 수정하면 다른 곳에도 영향을 끼치기 때문이다.
이러한 문제를 코드로 간단히 해결할 수 있다.
작성했던 코드들을 아래와 같이 수정한다.
StatefulService.java
package hello.core.singleton;
public class StatefulService {
/*
service에서 사용자가 공유하는 변수등을 수정하면
실무에서 엄청난 문제가 생길 수 있다.(ex. 다른 사용자의 정보를 볼 수 있다.)
해결책으로는 사용자가 공유하는 변수는 건드리지 못하게 한다.
(이 클래스에선 들어오는 값을 바로 return해서 해결)
*/
//private int price; // 상태를 유지하는 필드
public int order(String name, int price){
//this.price = price; // 여기가 문제
System.out.println("name = "+ name + " price = " + price);
return price;
}
// public int getPrice(){
// return price;
// }
}
공유되는 price 필드를 생성하지 않고,
order 메서드가 실행 시 바로 그 가격을 반환해주도록 한다.
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;
import javax.swing.plaf.nimbus.State;
import static org.junit.jupiter.api.Assertions.*;
class StatefulServiceTest {
@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);
Assertions.assertThat(userAPrice).isNotEqualTo(userBPrice);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
StatefulSerivce 객체를 두개 생성하고
각각 order 메서드를 실행하면서 반환되는 값을 변수에 저장한다.
그리고 출력을 해보면,,
각각의 가격이 따로 저장됨을 알 수 있다.
스프링 빈은 항상 무상태(stateless)로 설계하자.
'Java > Spring' 카테고리의 다른 글
[Spring] 스프링 필터(Spring Filter) | FilterType 옵션 (0) | 2022.01.22 |
---|---|
[Spring] 컴포넌트 스캔 (@Component) | 의존관계 자동 주입(@Autowired) (0) | 2022.01.22 |
[Spring] 싱글톤 패턴(Singleton Pattern)의 문제점과 스프링 컨테이너 (0) | 2022.01.21 |
[Spring] 싱글톤(Singleton) 이란? | 싱글톤 패턴 | 싱글톤 컨테이너 | 웹 애플리케이션과 싱글톤 |Singleton Pattern | Singleton Container (0) | 2022.01.21 |
[Spring] XML로 스프링 빈 설정 사용 | 애노테이션 기반 자바 코드 설정 VS XML 설정 (0) | 2022.01.20 |