당니의 개발자 스토리
싱글톤 방식의 주의점 본문
싱글톤 방식의 주의점
이번 시간에는 여러분들께 꼭 알려드리고 싶은 싱글톤 방식의 주의점에 대해서 설명을 해드리겠습니다.


싱글톤 패턴이든, 아니면 스프링과 같은 싱글톤 컨테이너를 사용하든 간에 객체 인스턴스를 딱 하나만 생성해서 공유하는 싱글톤 방식은 조심해야 될 게 있어요. 왜냐하면 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유해서 쓰잖아요. 그렇기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안 돼요.
무상태로 최대한 설계를 해야 됩니다.
그래서 특정 클라이언트에 의존적인 필드가 있으면 안 돼요.
그리고 특정 클라이언트가 값을 바꾸게 두면 안 돼요. 그래서 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안 됩니다.
가급적이면은 읽기만 가능해야 돼요. 읽기만 가능하다는 게 가급적이면 값을 수정하면 안된다는 말이에요.
그리고 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해서 써야돼요.
스프링 빈의 필드에 공유 값을 설정하면, 정말 큰 장애가 발생할 수 있습니다. 물론, 싱글톤 빈을 말하는 거죠.
말로 설명해서는 좀 안 와닿을 수 있거든요. 실무에서 상당히 자주 발생하는 예시를 보여드릴게요.

test의 singleton에다가 StatefulService 라는 클래스를 만들게요.
무상태가 아닌, 상태를 유지할 경우에 발생하는 문제점에 대한 예시입니다.
어떤 서비스가 있는데 이름이 StatefulService 인 거에요.

이 서비스는 가격이라는 필드를 가지고 있어요. 상태를 유지하는 필드입니다.

그리고 주문과 관련된 메서드의 만듭니다. 주문할 때 가격을 저장해두는 메서드예요.

클라이언트의 의도는 이런 거예요. 주문을 해서 값을 원래 필드에다가 저장을 해놓고,

getPrice()를 통해서 가격을 꺼내고 싶었던 거죠. 이제 이거에 대한 test를 만들어야 하는데, cmd + shift + T 하면,

new test를 만들 수 있습니다.

이렇게 만들면, 편리하게 만들어집니다.

그런 다음에 statefulServiceTest 전용으로 Config를 하나 만들게요.

이렇게 만들고요. 그런 다음에 스프링 컨테이너를 만들겠습니다.

이러면, ac 라는 스프링 컨테이너는 이 TestConfig 하나만 생성해서 쓰는 거에요. 그리고 나서 빈을 조회할 겁니다.
그냥 타입으로 꺼낼게요.

2번 꺼내서 조회하겠습니다.
그리고 나서, 고객 요청에서 A라는 사용자가 10,000원을 주문했어요. 그리고 B 사용자는 20,000을 주문했습니다.

원래는 멀티 쓰레드 해가지고 복잡하게 해야되는데, 단순하게 했습니다.

사용자 A가 주문 금액을 조회하면, price는 얼마 나와야 돼요?
지금 두 개의 고객이 요청을 한 거죠. 첫 번째 요청이 오면서,

order를 호출했어요. A는 10000원을 주문했고, 내 주문한 금액을 getPrice()로 꺼내고 싶은 거죠.
그런데 그 금액을 꺼내기 전에! 사용자 B가 끼어든 거에요.
A가 주문하고 금액을 조회하려는 사이에 B가 중간에 끼어들어서, 주문을 넣어버린 겁니다.
B가 20,000원을 주문하고나서, 사용자 A가 주문 금액을 조회하면 얼마가 나올까요?

20000원이 나오겠죠. ac를 쓴 순간부터 빈으로 등록되는 모든 객체가 싱글톤이므로, StatefulService가 싱글톤이고,

StatefulService를 호출하면 빈으로 올려뒀던 동일한 애를 계속 반환하므로, 걔의 상태가 바뀌겠죠.

그래서 A의 금액을 조회하는 코드로 돌려보면,

2만원이 나옵니다. 우리는 만원이 나오길 기대했어요.

왜냐면 사용자 A가 조회를 한 거란 말이에요. 근데도 중간에 사용자 B가 바꿔버리는 바람에 2만원이 나왔습니다.
사실 statefulService1이든, statefulService2든 이름은 중요하지 않습니다. 직접 참조 값을 들어가보면, 인스턴스는 같은 애를 쓰잖아요. == 비교하면 똑같습니다.
검증을 해보면,

돌리면,

테스트가 성공했습니다. 그럼 사용자 A는 당황스럽겠죠. A는 만원을 넣었는데 주문 결과 화면에 2만원으로 보이는 거에요.
정리해보면,

최대한 단순히 설명하기 위해, 실제 쓰레드는 사용하지 않았구요. 실제 쓰레드는 더 복잡해요.

실제로는 order 안에서 getPrice()로 꺼낼 때 문제가 생깁니다. 메서드를 안 쪼개도 한 메서드 안에서 문제가 생깁니다. 그런데 이건 멀티쓰레드에 대한 내용이니까 넘어갈게요.
ThreadA가 사용자A 코드를 호출하고 ThreadB가 사용자B 코드를 호출한다 가정합시다. 이게 무슨 얘기냐면, 웹에서 요청이 오잖아요. 그러면 요청별로 쓰레드가 각각 따로 할당이 돼요.
근데 StatefulService의 price 필드는 공유되는 필드란 말이에요. 왜냐면 싱글톤이기 때문이에요. 그런데 특정 클라이언트가 공유되고 있는 필드의 값을 변경한 게 문제예요.
위에서 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다고 했죠.
사용자A의 주문금액은 10000원이 되어야 하는데, 20000원이라는 결과가 나와버린 거에요. 딱 주문서를 열어봤는데, 20000원이 긁힌 거에요.
실무에서 이런 경우를 종종 보는데, 이로 인해 정말 해결하기 어려운 큰 문제들이 터집니다. 돈과 관련된 건 진짜 큰일 납니다.
진짜 공유 필드는 조심해야돼요. 스프링 빈은 항상 무상태(stateless)로 설계해야 됩니다.

이거를 무상태로 바꿔보겠습니다. 위에서 무상태로 하기 위해선, 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다고 했죠.

이렇게 하면,

사용자 A가 order 하고 나면, 금액이 바로 튀어나오겠죠. 이건 지역변수이기 때문에 사용자 A와 사용자 B의 가격이 다르거든요.

이렇게 하고 돌려보면,

만원이 나옵니다. 유저 B를 호출하든, 말든 지역변수는 공유 되는 게 아니니까요. 이런 식으로 문제를 해결하라는 거예요.
아무튼 진짜 공유필드는 조심해야 하고, 스프링 빈은 항상 무상태(stateless)로 설계하는 걸 기억하셔야 합니다.
이렇게 해서 싱글톤 방식의 주의점은 여기서 마무리 하겠습니다.
다음 시간에는 @Configuration과 싱글톤에 대해서 알아보겠습니다.
'스프링 > 스프링 핵심 원리 - 기본편' 카테고리의 다른 글
| @Configuration과 바이트코드 조작의 마법 (0) | 2024.01.23 |
|---|---|
| @Configuration과 싱글톤 (0) | 2024.01.23 |
| 싱글톤 컨테이너 (0) | 2024.01.23 |
| 싱글톤 패턴 (0) | 2024.01.23 |
| 웹 애플리케이션과 싱글톤 (0) | 2024.01.23 |