스프링/핵심원리-기본
싱글톤 컨테이너
eunkyung
2022. 10. 2. 02:36
<싱글톤 컨테이너>
// 지금까지 쭉 사용해왔던 방식
- 스프링은 싱글톤 패턴의 단점 해결하면서도 객체 인스턴스를 싱글톤으로 관리한다. (->코드 깔끔(@annotation사용), Solid위반 X, 코드 유연, private 생성자 X)
- 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리하는데 기존 컨테이너 생성과정을 생각해보면 객체가 하나만 생성된다.
- 스프링 빈이 바로 싱글톤으로 관리되는 빈 ->@Bean을 호출해 빈 객체를 미리 등록해(객체 미리 생성해) 관리해줌. 조회해도 1개만 생성된거 확인 가능.
- 스프링 컨테이너는 싱글톤 컨테이너의 역할을 하고, 싱글톤 객체를 생성/관리하는 기능을 싱글톤 레지스트리라고 함.
- 위에서 사용한 싱글톤 패턴 코드가 하나도 없음.
클라이언트가 요청시마다 동일한 객체 반환. 이미 만들어진 객체 공유함.
+)99%는 싱글톤 기능 사용하고, 스프링의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아님. 요청마다 새로운 객체 생성해 반환하는 기능도 가능.
📌정말 중요
<싱글톤 방식의 주의점>
여러 클라이언트가 공유해서 사용하는 객체이기에 싱글톤 객체는 상태를 유지하게 설계하면 안됨.
무상태로 설계해야 함.
- 특정 클라이언트에 의존적인 필드가 있으면 안된다.
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
- 가급적 읽기만 가능해야 한다.
- 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있음.
-상태를 유지할 경우 발생하는 문제점 예시
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;
}
}
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);
//ThreadA: 사용자A 주문 금액 조회
int price = statefulService1.getPrice();
//ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
System.out.println("price = " + price);
Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean - 빈으로 스프링 컨테이너에 등록
public StatefulService statefulService() {
return new StatefulService();
}
}
}
사용자 A가 10000원을 주문하고 조회하려고 할 때 그 사이에 사용자 B가 20000원을 주문한 상황
사용자 A는 10000원을 주문했기에 10000원이 출력되어야 하지만 상태를 유지했기에 20000원이 출력됨.
- ThreadA가 사용자A 코드를 호출하고 ThreadB가 사용자B 코드를 호출한다. 웹 요청 시 각각 따로 쓰레드 할당.
- StatefulService 의 price 필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경할 수 있기에 문제가 발생. ->공유필드는 조심해서 설계 =>항상 무상태로 설계해야 한다.
<@Configuration과 싱글톤>
+)왜 구체 클래스 말고 인터페이스를 참조할까? - 구현체를 참조하지 않기 위해서. 구현체 의존 X위해.