들어가며
- 스프링의 멀티스레드는 어떻게 작동하는지 항상 궁금했다. 서버에서 돌아가는 웹 어플리케이션은 하나인데 어떻게 여러 명이 동시에 사용할 수 있는 것일까?
- 이전에도 여러 블로그를 보면서 이해해보려고 노력했지만 쉽지 않았다. 이번에 운영체제의 cpu의 사용과 대비하여 생각하니까 쉽게 풀렸다. 이런 경험을 공유하고자 블로그를 작성한다.
- 한편, 이 글의 신뢰성에 대해서는 확신이 없기에 적당하게 참고 바란다. 부족한 부분은 차차 보완하고자 한다.
하나의 프로세스와 여러 개의 멀티 스레드
- 운영체제의 가장 중요한 목표는 자원을 최대한 활용하는 것이다. 메모리든, cpu든 가능한 100퍼센트의 효율을 통해 요청된 작업을 최대한 빨리 끝내는 것이 목표다. 이런 조건 속에서 멀티스레드의 장점은 무엇일까? 먼저 cpu를 최대한 활용한다. 하나의 스레드가 blocked된 상황이라 하더라도 다른 스레드가 cpu를 사용한다. 메모리 입장에서도 좋다. 프로세스가 여러 개라서 메모리를 각 각 차지하는 것 보다, 하나의 프로세스의 데이타를 여러 스레드가 공유하는 것이 유리하다.
- 스프링도 이와 크게 다를까? 서버에 올라간 웹 어플리케이션은 하나의 프로세스이고, 그 프로세스에는 멀티 스레드가 존재한다. 하나의 프로세스에 여러 멀티스레드가 cpu를 점유하여 작업하는 것처럼, 스프링 역시 다수의 클라이언트가 하나의 웹을 사용할 수 있다. 이렇게 생각하니까 스프링의 멀티스레드가 좀 더 쉽게 다가왔다.
- IoC와 DI의 과정은 무엇인가? 여러 개의 부품(component, bean)을 결합하여 완성된 어플리케이션을 만드는 과정이다. 다수의 사용자에 대응하기 위하여 어플리케이션을 여러 개 만들어야 할까? 그렇지 않다. 완성된 어플리케이션은 하나만 있으면 된다. 어차피 한 순간에 cpu를 점유하는 것은 프로세스가 여러개라 하더라도 단 하나의 스레드만 점유한다. 같은 코드와 힙을 사용하는데, 여러 개의 프로세스(웹)을 사용할 이유가 없다. 또, cpu를 선점하기 위하여 웹 어플리케이션 간 문맥교환이 발생하는 것도 효율적인 선택이 전혀 아니다. 이렇게 생각하니까 웹 어플리케이션이 하나이며, 멀티 스레드로 스프링이 작동한다는 말은, 정말로 당연한 이야기였을 뿐이다.
- 그럼 하나의 웹 어플리케이션으로 작동하는 범위는 어디까지인가? IoC, DI로 생산되는 어플리케이션은 당연히 포함된다. request, response 객체 또한 그러하다. 어플리케이션에 최초로 생성되고, 첫 번째 클라이언트가 서버에 요청을 보내면, 그 때 request와 response가 생성된다. 그리고 그 이후 계속 재사용된다. 그러므로 스프링의 성능을 체크 할 때 반드시 서버와의 접속을 여러번 한 후에 해야 한다.
- 스프링(서블릿)에서 객체(빈)를 생성할 때 기본 스코프는 싱글톤이다. 스코프를 만약 프로토타입으로 변경할 경우, 빈이 계속 생성과 삭제를 반복한다고 한다. 스프링은 객체를 생성하는 것을 기본적으로 단 하나를 전제한다.
- 이러한 이해를 바탕으로 보면, 웹 어플리케이션이 하나여야만 하는 이유는 분명하다. 그 이후 논의해야 할 부분은 무엇일까?
- 멀티스레드 상황에서 트랜잭션과 임계 구역, 스레드 간 독자적인 데이타 관리, 스레드 간 cpu 및 자원의 분배를 위한 스케쥴링 및 알고리즘 기법이 논의되어야 할 것이다.
thread saftey
- thread saftey 하지 않은 상태는 무엇일까? 멀티 스레드 과정에서 여러 스레드가 같은 데이타를 동시에 접근하여 데이타의 완전성이 훼손되거나, 여러 스레드가 임계 구역에서 교착되어 멈춰버리는 현상을 의미할 것이다. 그리고 대체로 전자의 의미로 이해한다.
- 멀티스레드 기법이 자바와 스프링이 같을까? 톰캣도 스프링도 JVM을 사용하니까 그럴 것이다. 자바의 스레드 관리 기법은 많이 공유되어 있다. synchronized, Thread 클래스의 메서드인 notify(), wait() 등 메서드의 활용, Vector 등 스레드로부터 안전한 자료구조의 사용, ThreadLocal의 활용이 있을 것이다.
- 스프링과 관련하여 스레드 세이프티와 관련하여 정리된 자료를 보면 일관적으로 맴버에 대한 내용이 나온다. 아래의 코드를 보자.
@Requiredargconstructor
public class MemberService{
private final MemberRepository repository; ....(1)
private int studentId;
public int getGrade(){
return repository.getGrade(studentId);
} ..........(2)
public int getAge(String name){
return repository.getAge(name);
}...........(3)
}
- (1) 어플리케이션의 부품으로 사용되며 final로 불변하는 객체이기 때문에 멀티 스레드의 조건에서 안전하다.
- (2) 요청한 작업이 종료되더라도 어플리케이션은 재활용 된다. 그런데 만약 맴버의 값이 변할 수 있고 어플리케이션에 계속 남아 있다면, 이후 재활용 할 때 데이타의 신뢰성 문제가 발생한다. 그러므로 아래의 코드는 thread safety 하지 않다.
- (3) 매개변수로만 사용할 경우 결합도를 낮추는 효과를 주면서 동시에 멀티스레드로부터 안전하다. 왜냐하면 어플리케이션에 잔존하는 값이 없기 때문이다.
- 그러니까 결론적으로 위의 과정은, 웹 어플리케이션은 계속 재활용 되는데, 이전의 작업의 값이 남아있어서는 안된다는 의미로 이해할 수 있다. 운영체제에 학습하는 것처럼 같은 데이타에 동시에 접근하는 문제라기 보다, 스레드에게 영향을 받은 프로세스의 문제처럼 느껴진다.
스레드 간 메모리 관리
- JVM에는 운영체제와 동일하게 스택 영역이 있다. 스택 영역에는 멀티 스레드가 각자의 메모리 영역을 가진다. 이를 통해 스레드 간 배타적인 메모리를 사용할 수 있다.
- 스레드를 서버를 이용하는 고객, 클라이언트 단위로 한정할 수 있다. 그리고 HTTP 통신은 stateless 방식으로, request와 response가 한 차례 종료되면 모든 데이타는 사라진다. 이를 극복하기 위한 방법이 존재한다. 클라이언트는 세션아이디와 쿠키를 가질 수 있다. 세션아이디는 서버가 발급하며, 서버는 세션아이디에 따른 데이타를 (기본 설정으로) 30분 동안 가지고 있다. 세션은 공유하지만 세션 데이타 자체는 개인화 되어 있다.
효율적인 멀티 스레딩을 위한 기법
참고 :