본문 바로가기

Spring

스프링 빈은 Thread-safe 할까?

초기에 스프링을 공부할 때 스프링 빈의 기본 Scope는 싱글톤이고, 스프링 환경은 멀티 쓰레드이라는 것을 알았습니다. 그런데 '왜 하나의 공유자원(싱글톤 객체)을 여러 쓰레드에서 다루는데 문제가 되지 않을까?'란 생각을 했습니다. 한편으로는 '스프링이 마법을 부려서 쓰레드에 안전한 건가?'란 생각도 했고요.



차근차근 싱글톤과 불/가변 객체에 대해 알아보고 무엇이 착각을 일으켰는지 알아보겠습니다. 


먼저, 위키를 통하여 싱글톤의 정의에 대해서 알아보겠습니다.


소프트웨어 디자인 패턴에서 싱글턴 패턴(Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이와 같은 디자인 유형을 싱글턴 패턴이라고 한다. 주로 공통된 객체를 여러 개 생성해서 사용하는 DBCP(DataBase Connection Pool)와 같은 상황에서 많이 사용된다.


마찬가지로 주로 스프링 빈도 공통된 객체를 여러 개 생성해서 메모리를 쓰는 것보다는 하나의 객체를 동해서 로직을 수행하는 것을 선호합니다.



간단하게 싱글톤 객체를 통해 덧셈을 하는 프로그램을 만들어보겠습니다.

public class Singleton {

private static Singleton singleton = new Singleton();

private Singleton(){}

public static Singleton getInstance(){
return singleton;
}

public int add(int num){
return ++ num;
}

}

class AddTest{
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();

int[] array = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };

for(int i : array) {

new Thread(() -> {
System.out.println(singleton.add(i));
}).start();

}
}
}

쓰레드를 통해 가지고 있는 값을 변경시키고, 출력을 합니다.


비동기의 특성상 순서는 보장되지 못하지만, 1 ~ 20까지의 각각의 결과는 다 보장되었습니다.

다시 말해서, Thread-safe 합니다.


이번에는 상태를 가지는(전역변수가 있는) 싱글톤 객체를 통해 덧셈을 하는 프로그램을 만들어보겠습니다.

public class Singleton {

int num;


private static Singleton singleton = new Singleton();

private Singleton(){}

public static Singleton getInstance(){
return singleton;
}

public int add(){
return ++ num;
}

}

class AddTest{
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();

int[] array = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };

for(int i : array) {

new Thread(() -> {
System.out.println(singleton.add());
}).start();

}
}
}

쓰레드를 통해 객체의 내부 상태를 변경시키고, 출력을 합니다.


여기서 주목할 점은 데이터에 문제가 발생했습니다. 1 ~ 20이 나와야 하는데 1이 두번 나오고 20이 빠졌습니다.

하나의 공유자원을 놓고 여러 개의 쓰레드가 읽기/쓰기를 하면서 데이터 조작 중 문제가 발생한 거죠. (Race Condition)

이 경우는 Thread-safe 하지 못합니다.


위 두 가지의 싱글톤 객체를 비교해보자면 전자의 경우(불변 객체)는 값을 외부에서 전달받아 변경 시켰고 , 후자(가변객체)의 경우는 내부에서 가지고 있는 값을 변경 시켰습니다. 좀 더 정확하게 말하자면, JVM에서 각각의 쓰레드는 자신의 stack 영역을 가지고 있지만 heap 영역은 쓰레드간에 공유를 하고 있습니다. (간단하게 stack은 지역변수, heap은 전역변수로 생각하시면 됩니다.) 그래서 상태를 가지는 가변 객체의 경우 문제가 발생한 것입니다.


(싱글톤 스코프의)스프링 빈도 결국 위의 싱글톤 예제와 원리는 동일하기 때문에 멀티 쓰레드 환경에서의 가변 객체일 경우에는 Thread-safe 하지 못합니다.


본인이 짠 코드를 한번 생각해보면 스프링 빈(@Controller, @Service, @Repository, @Component 어노테이션이 달린 객체 등)의 전역변수에는 주로 스프링 빈과 같은 불변 객체들이 있지 VO, DTO, Map 같은 가변 객체가 존재하지 않을 겁니다. 만약 있다면 synchronized 키워드나 concurrent 패키지의 클래스들을 사용하여 동시성 문제를 해결했을 것입니다. 그리고 스프링 빈 사이의 데이터를 주고받을 때에는 스프링빈의 상태를 변경 시키는 것이 아니라 메소드의 파라미터를 이용했을 것이고요. 자신도 모르게 관행에 따라 개발을 하다 보니 Thread-safe 하게 개발 한 것 같습니다.


결론은 스프링 빈을 상태를 변경할 수 있게 만든다면 Thread-safe 하지 않습니다.