[Spring] 웹 애플리케이션에서 사용하는 싱글톤과 이를 기반하여 객체를 관리하는 스프링 컨테이너 및 무상태성 (stateless)
웹 애플리케이션과 싱글톤
웹 애플리케이션은 보통 여러 고객이 동시에 요청을 할 수 있는 가능성이 있다.
요청이 올 때마다, 새로운 객체를 생성한다면 메모리 낭비가 심하게 된다.
예를 들어, 고객 트래픽이 초당 200이 나오면 초당 200개의 객체가 생성되고 소멸된다.
싱글톤 패턴으로 객체를 생성하면 해당 객체가 딱 한개만 생성되고, 생성된 객체 인스턴스를 공유한다.
스프링 애플리케이션은 대부분 웹 애플리케이션이다.
즉, 스프링은 객체를 싱글톤으로 생성하도록 설계되어 있다. 스프링 빈
싱글톤 패턴
(1) private 생성자를 만들어, 외부에서 객체를 마음대로 생성하지 못하게 막는다.
(2) 멤버필드를 static으로 하나만 선언하고, 내부에서 객체를 생성한다.
(3) 이 멤버필드를 가져다 쓸 수 있는 public 메서드를 만든다.
객체 생성없이 접근 가능한 멤버이며, 애플리케이션 실행 시, 메모리에 무조건 올라간다.
즉, 클래스 level 당, 딱 하나만 만들어서 이후 생성될 인스턴스끼리 공유할 수 있다.
⚠️instance 멤버는 객체 level에 소속된 멤버로써, 객체를 생성해야만 사용할 수 있는 멤버이고, 객체 간에 공유되지 않는다.
한번 final 키워드로 선언되면, 초기화가 필수이며 이후의 값변경이 불가능하다. (즉, 일종의 상수역할)
메서드에 final 키워드를 사용하면 상속될 수 없고, 오버라이딩(재정의)될 수 없다.
☄️ 진정한 상수란
static final int CONSTANT = 100;
static과 final이 같이 붙은 변수가 마치 진정한 상수처럼 동작한다고 할 수 있다.
상수란 누가 써도 같아야 진정한 상수이다.
static을 붙이지 않으면 사용자마다 다른값으로 초기화해서 그 값이 다를 수 있다.
즉, static을 붙여 하나만 만들어 공유하고, final을 붙여 무조건 초기화하여 변경할 수 없게 만든다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SingletonService {
private static final SingletonService instance = new SingletonService();
// 클래스 level에 올라가기 때문에 딱 하나만 존재하게 된다
// static 영역에 객체 인스턴스를 미리 하나 생성해서 올려둔다
private SingletonService() {
// private 생성자로 외부에서 객체 생성을 막는다
}
public static SingletonService getInstance() {
return instance; // 항상 같은 인스턴스를 반환
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
스프링 컨테이너🥥
가 기본적으로 객체를 싱글톤으로 만들어 관리한다.
때문에, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다.
스프링은 CGLIB를 사용해서 AppConfig 클래스를 상속받은 * 임의의 클래스를 만들고, 해당 클래스를 스프링 빈으로 등록한 것이다.
이 * 임의의 클래스의 내부로직으로 싱글톤을 보장해준다.
⚠️ 스프링 기본 등록방식은 싱글톤이지만, 빈 스코프를 설정하여 요청 시마다 새로운 객체를 생성해서 반환할 수도 있다.
자바 바이트코드 조작 라이브러리로써, 주로 프록시 객체를 만드는데 사용된다.
즉, 런타임에 클래스를 생성하고 변경할 수 있다.
이는, 상속을 통해 클래스의 기능을 확장할 수 있도록 한다.
⚠️ 단, 자바 바이트 코드를 생성하고 조작하기 때문에, 런타임시 성능 오버헤드가 발생할 수 있다.
🍪 싱글톤의 문제점
- 클라이언트가 구체 클래스에 의존한다. DIP 위반
- 때문에 OCP 원칙을 위반할 가능성도 높다.
- 스프링 컨테이너가 관리하기 때문에 테스트하기 어렵다.
- 내부 속성을 변경하거나 초기화하기 어렵다.
private 생성자
로 자식 클래스를 만들기 어렵다.- 유연성이 떨어진다.
스프링 컨테이너🥥
는 이러한 싱글톤 패턴의 문제점을 내부적으로 해결하면서,
객체 인스턴스를 싱글톤으로 관리한다.
싱글톤으로 객체를 생성하고 관리하는 작업을 싱글톤 레지스트리에서 하고, 이 싱글톤 레지스트리를 스프링 컨테이너🥥
라 한다.
싱글톤과 무상태성(stateless)
즉, 무상태(stateless)로 설계해야 한다.
⛔ 객체가 특정 클라이언트에 의존적인 필드가 있으면 안된다. 있더라도 가급적 읽기만 가능해야 한다.
필드 대신 자바에서 공유되지 않는 지역변수 파라미터 ThreadLocal 등을 사용해야 한다.
🍪 if, 스프링 빈의 필드에 공유값을 설정하면 큰 장애가 발생할 수 있다.
☕ StatefulService.java
1
2
3
4
5
6
7
8
9
10
11
12
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;
}
}
🦕 StatefulServiceTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class StatefulServiceTest {
@Test
@DisplayName("상태를 유지하도록 클래스를 설계하면 데이터 오류가 생길 수 있다")
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
// 웹에서 요청이 오면, Thread가 할당된다
// ThreadA: 사용자A 10000 주문
statefulService1.order("userA", 10000);
// ThreadB: 사용자B 20000 주문
statefulService2.order("userB", 20000);
int price = statefulService1.getPrice();
// statefulService1 과 statefulService2는 같은 객체다
assertThat(price).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
사용자A가 주문한 금액은 10000원임에도,
getPrice()
를 호출하면 20000원으로 변경되어 있음을 확인할 수 있고,
이는 스프링 컨테이너🥥
에 의해 싱글톤으로 관리되는 객체가 상태를 유지하도록 잘못 설계되었기 때문이다.
따라서, 스프링의 클래스는 항상 무상태(stateless)로 설계한다.
1
2
3
4
5
6
7
public class StatelessService {
public int order(String name, int price) {
System.out.println("name= " + name + " price= " + price);
this.price = price;
return price;
}
}
@Configuration과 싱글톤 보장
@Configuration
을 붙이면 CGLIB 기술을 이용해서 싱글톤을 보장한다.
즉, @Bean
만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
⚠️ 이 때, 의존관계 주입을 위해 메서드를 직접 호출할 때, ⛔ 싱글톤을 보장하지 않는다.