멀티 쓰레드 환경에서 스프링빈 주의사항
얼마전 “CBT Live” 테스트(실제 서비스 장비에서 성능/기능 통합 테스트)를 하면서
발생한 문제를 공유하고자 합니다.
(1) 증상
외부 시스템에 “A“라는 사용자 정보를 요청 했는데, 확인을 해보니 “B” 사용자
정보가 등록이 되었습니다.
“Local” 개발 환경에서는 전혀 그런 증상이 없었고, 실제로 서비스를 하려는
“CBT” 개발 환경에서 “성능/기능 테스트“를 하면서 해당 이슈가 발생을 하였습니다.
그래서 “log“를 확인했습니다.
그런데 이상한 점이 “요청을 위임하는 모듈“에서는 정확히 “A” 사용자 정보
가 있었고, “실제 서버와 통신하는 모듈“에서 “B” 사용자 정보를 호출 했습니다.
확인 결과 “Spring Bean“에 대한 개발자의 이해 부족으로 발생된 에러 였습니다.
해결 방법을 말씀 드리기전에 “Spring Application Context“에 대해서
간략하게 설명을 드리겠습니다.
(2) Spring Big Picture
아래의 그림은 “멀티 쓰레드 환경에서 스프링 빈” 관계를 직접 그려봤습니다.
“SpringFramework” 기반으로 만든 “어플리케이션”은 기동시 “ApplicationContext“라는
“Static Sharing Pool“를 생성 합니다.
좀더 쉽게 설명을 하면 하나의 “싱글톤” 패턴 방식의 구현된 오브젝트가 생성 됩니다.
(정확히는 “싱글톤 패턴”은 아닙니다. 이해를 돕기 위해서)
이렇게 생성된 “ApplicationContext” 영역에 “POJO(Plain Old Java Object) 클래스“
들의 오브젝트들이 등록이 됩니다.
“POJO” 클래스는 그냥 “new 하면 스스로 생성”이 가능한 클래스의 형태를 말합니다.
이렇게 등록된 오브젝트를 “Spring Bean“이라고 합니다.
비록 “POJO” 클래스에 “static“으로 선언을 하지 않더라도, “ApplicationContext“
가 이미 “Sharing“이 되어 있기때문에 “당연히 등록된 Bean”로
“멀티 Thread 환경“에서 서로 공유를 하게 됩니다.
(물론 prototype일 경우는 매번 생성을 합니다.)
위에 그림을 보면 “JVM”에서 하나의 공유 “ApplicationContext”가 생성이 되며
“Spring Bean #1″, “Spring Bean #2″, “Spring Bean #3″ 등
3개의 “Bean”이 등록이 되어 있고, “1번부터 10번 Thread 모두
동일한 Bean 오브젝트“를 사용을 합니다.
여기까지는 “SpringFramework“를 개발하신 분들은 알고 계실 겁니다.
문제는 개발자들이 Spring Bean의 멤버변수 또한 멀티쓰레드 환경에서
공유가 된다는 것을 간과한다는 것입니다.
이러한 실수는 “Thread Safe” 한 프레임웍(Servlet, Netty Handler, Camel Router, Spring Controller)들에 대한 오해 때문입니다.
문서를 보면 당연히 “Thread Safe“하다고 명시된 부분만을 확인하지, 실제 어디까지
“Thread Safe“하지 않는지 확인을 잘 안한다는 것입니다.
대부분 public 메서드”안에서 선언된 로컬 변수가 “Thread Safe”한거지 “멤버변수”
까지 “Thread Safe” 하다라는 것입니다. 즉 정확한 확인이 필요 합니다.
(3) 해결 방법 및 코딩 가이드
아래는 개발자가 문제를 일으켰단 소스 입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @Service public class MemberController{ @Autowired private UserRepository userRepository; private Member member = new Member(); public void createUser(String id, String name) throws Exception { member.setID(id); member.setName(name); userRepository.insertUser(member); } } |
7 Line : 멤버변수 “Member” 오브젝트를 생성 합니다.
11 Line : 파라미터 id를 member 오브젝트의 id 속성에 값 설정을 합니다
12 Line : 파라미터 name를 member 오브젝트의 name 속성에 값 설정을 합니다
소스를 보면 위에서 언급한것 처럼 “멤버변수“를 “Bean” 최초 생성시 초기화를 합니다.
그 다음 부터는 “createUser” 메서드에서 계속 재사용을 합니다.
당연히 “createUser” 메서드는 “Thread Safe” 합니다. 하지만 문제는 “멤버변수” 입니다.
해당 코드를 아래와 같이 “리팩토링” 해야 합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Service public class MemberController{ @Autowired private UserRepository userRepository; public void createUser(String id, String name) throws Exception { Member member = new Member(); member.setID(id); member.setName(name); userRepository.insertUser(member); } } |
8 Line : 멤버변수 부분을 삭제하고, 대신 “createUser” 메서드에서 생성을 합니다.
코드는 상당히 간단 합니다. 간혹 물어 보시는 분들이 “저렇게 매번 생성하면” 메모리에
부담이 있는것 아니냐 하시는 분들이 있습니다.
“createUser” 메서드가 종료되면 자연스럽게 “member” 오브젝트는 “GC“
대상이 됩니다.
(4) Conclusion
“멀티 쓰레드 환경에서 스프링빈 주의사항“에 대해서 말씀 드렸습니다.
사실 별거 아닐수 있지만 상당히 주의를 요하며, 개발시에 팀원들에게 숙지를
해야 하며, 팀장 또는 선임 개발자들은 “코드 Inspection“시 반드시
이 부분에 대한 점검이 필요 합니다.
왠만하면 “스프링”에서 “멤버변수”는 “Injection“에 사용하는 “bean“일 경우만
사용하도록 권고 드립니다.
제가 왜 이렇게 강조를 하는 이유는
디버깅이 하기가 너무 어렵다는 이유 입니다. 대부분 개발시에는
개발자 환경은 멀티 쓰레드 환경이 아닙니다. 그렇기 때문에
발견이 거의 되지를 않습니다. 대부분 개발 후반에 성능 테스트
할때 발견이 됩니다.특정 서버가 CPU를 많이 차지 하거나, 메모리 사용률을 많이 점유하면
thread dump를 떠서 확인을 하지만 이렇게 thread간 race condition
깨진 경우는 dump떠서 확인이 힘들고, 특히나 여러 클래스에 그렇게
사용을 하면 더 찾기가 어렵습니다.최악의 경우는 프로젝트 막바지는 “멘붕” 상태이기 때문에 “선무당”이 사람 잡는다고
전체 서버에 JDK를 다른 버전을 설치 하거나, WAS를 다시 설치 하거나, 심지어
OS 커널 또는 버전을 바꿔야 한다든지 더 큰 상황으로 치닫을수 있습니다.또한 수십, 수백명이 이런 코드를 양산될 경우는 더더욱 트러블 슈팅이 어렵습니다.
이점 유념하시고 개발 하셨으면 합니다.