프로세스 : 실행중인 프로그램
멀티 프로세스 : 둘 이상의 쓰레드를 가진 프로세스
쓰레드는 작업을 수행하는데 개별적인 메모리 공간(호출 스택)을 필요로 한다.
즉 프로세스의 메모리 한게에 따라서 생성할 수 있는 쓰레드가 결정된다.
실제로 프로세스 메모리 한계에 다다를 정도로 많은 쓰레드를 생성하는 경우는 없다.
대부분의 OS는 멀티 태스킹을 지원하기 때문에 여러개의 프로세스가 동시에 실행될 수 있다.
멀티쓰레딩 : 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것
CPU 코어가 한 번에 단 하나의 작업만 수행할 수 있다. 따라서 실제로 동시에 처리되는 작업의 개수는 코어의 개수와 일치한다.
메신저로 채팅하면서 음악을 들을 수 있는 이유는 멀티쓰레딩이 가능하기 때문이다.
싱글쓰레드로 작성되어 있다면 파일을 다운받는 동안에 다른 일을 할 수 없다.
여러 사용자에게 서비스를 해주는 서버 프로그램의 경우 멀티쓰레드로 작성하는 것이 필수적이다.
하나의 서버 프로세스가 여러 개의 쓰레드를 생성해서 쓰레드와 사용자의 요청이 1대1로 처리되도록 프로그래밍 해야한다.
싱글쓰레드로 처리하게 된다면, 사용자의 요청마다 새로운 프로세스를 생성해야하는데, 프로세스를 생성하는 것은 쓰레드를 생성하는 것에 비해 더 많은 시간과 메모리 공간이 필요하다.
자바는 스레드 풀을 이용해서 자바 프로그래머가 태스트 제출과 실행을 분리할 수 있는 기능을 제공했다.
자바 스레드는 직접 운영체제 스레드에 접근했고, 운영체제가 지원하는 스레드 수를 초과해서 사용하면, 자바 애플리케이션이 예상치 못한 방식으로 크래시 될 수 있다.
따라서 기존 스레드가 실행되는 상태에서 계속 새로운 스레드를 만드는 상황이 일어나지 않도록 주의해야한다.
스레드 풀은 자바에서 논리적인 스레드를 만들고 사용하는 것이다. 만약 스레드풀이 5개이고 실제 하드웨어 코어는 4코어라면
실제 태스크 수행은 하드웨어 코어가 진행하는 것이다.
스레드 풀의 장점은 하드 웨어에 맞는 수의 태스크를 유지하고 수 천개의 태스크를 스레드 풀에 아무 오버헤드 없이 제출할수 있다는 점이다.
하지만 잠을 자거나 I/O를 기다리거나, 네트워크를 사용하는 상황에서는 주의해야한다.
논리적 스레드풀 5개
그 중에서 20개의 태스크 요청이 들어왔다고 가정하자
5개의 수행중인 스레드에서 3개가 I/O지연 작업이 이루어지면 2개의 스레드가 나머지 태스크를 수행할 수 있다.
그럼 총 15개의 큐에 들어있는 남은 태스크를 두 개의 스레드가 처리해야하는 상황이다.
그렇다면 기존에 예측했던 5개의 스레드풀이 나눠서 작업을 처리하는 것과 효율이 달라지게 된다.
**이벤트를 기다리거나, 잠을 자는 태스크는 스레드 풀에 제출하지 말아야하지만 이를 지킬 수 있지는 않다.
따라서 프로그램을 종료하기 전 모든 스레드 풀을 종료하는 습관을 가져야한다.
특정한 스레드로 접근이 가능하게 하는 API
스레드별로 독립적인 데이터를 저장하고 관리하기 위한 클래스이다.
멀티스레드 환경에서, 여러 스레드가 동일한 변수를 공유하면 데이터 경합(Race Condition)문제가 발생할 수 있기 때문에, ThreadLocal을 사용해 각 스레드가 고유한 데이터 복사본을 가지도록해 문제를 방지한다.
CustomRequestContext
@Component
public class CustomRequestContext {
private static final ThreadLocal<String> userContext = new ThreadLocal<>();
public static String getUserId() {
return userContext.get();
}
public static void setUserId(String userId) {
userContext.set(userId);
}
public static void clear() {
userContext.remove();
}
}
CustomRequestContextFilter
@Component
public class CustomRequestContextFilter extends OncePerRequestFilter {
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws
ServletException,
IOException {
String userId = request.getHeader("Authorization");
MDC.put("requestId", userId);
CustomRequestContext.setUserId(userId);
MDC.put("userId", userId);
filterChain.doFilter(request, response);
}
}
ThreadLocalController
@RestController
@RequestMapping("threadLocal")
@Slf4j
public class ThreadLocalController {
@GetMapping public String threadLocalRequest() {
log.info("ThreadLocalController Hello!");
return "hello!";
}
}
RequestContextFilterTest
@WebMvcTest(controllers = ThreadLocalController.class)
@Import(CustomRequestContextFilter.class)
@AutoConfigureMockMvc
class RequestContextFilterTest {
private final CustomRequestContextFilter customRequestContextFilter = new CustomRequestContextFilter();
private MockMvc mockMvc;
@BeforeEach
void setup() {
this.mockMvc = MockMvcBuilders.standaloneSetup(new ThreadLocalController())
.addFilters(customRequestContextFilter)
.build();
}
@Test
@DisplayName("MDC with threadLocal")
public void testRequestContextFilter() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/threadLocal")
.header("Authorization", "user123"))
.andExpect(status().isOk())
.andDo(request -> {
String userId = CustomRequestContext.getUserId();
assertEquals("user123", userId);
});
CustomRequestContext.clear();
assertNull(CustomRequestContext.getUserId());
}
}
ThreadLocal은 사용후에 데이터를 삭제해서, ThreadLocal이 다음 요청에서 잘못된 값을 반환하지 않도록 방지한다
간단하게 ThreadLocal을 사용하는 방법을 알아보았다.
이번엔 동시에 새로고침을 와다다다다 하면 쓰레드 로컬을 어떻게 사용하게 될까?
새로고침 테스트도 수행해보자!
2024-12-29 16:35:14 [http-nio-8080-exec-1] INFO o.e.s.t.ThreadLocalController -[requestId=,userId=]
ThreadLocalController Hello!
2024-12-29 16:35:14 [http-nio-8080-exec-3] INFO o.e.s.t.ThreadLocalController -[requestId=,userId=]
ThreadLocalController Hello!
2024-12-29 16:35:14 [http-nio-8080-exec-4] INFO o.e.s.t.ThreadLocalController -[requestId=,userId=]
ThreadLocalController Hello!
2024-12-29 16:35:14 [http-nio-8080-exec-5] INFO o.e.s.t.ThreadLocalController -[requestId=,userId=]
ThreadLocalController Hello!
2024-12-29 16:35:15 [http-nio-8080-exec-6] INFO o.e.s.t.ThreadLocalController -[requestId=,userId=]
ThreadLocalController Hello!
2024-12-29 16:35:15 [http-nio-8080-exec-7] INFO o.e.s.t.ThreadLocalController -[requestId=,userId=]
ThreadLocalController Hello!
2024-12-29 16:35:15 [http-nio-8080-exec-8] INFO o.e.s.t.ThreadLocalController -[requestId=,userId=]
ThreadLocalController Hello!
2024-12-29 16:35:15 [http-nio-8080-exec-9] INFO o.e.s.t.ThreadLocalController -[requestId=,userId=]
ThreadLocalController Hello!
http-nio-8080-exec-thread번호
위 처럼 쓰레드를 사용하는데 순서대로 사용하는 것이 아닌 마구잡이로 섞여서? 사용된 것을 알 수 있다.
대부분 오름차순으로 사용하고 있지만 위 로그는 2가 빠졌다.
위에서 활용 방법은 알아보았다. 하지만 정확히 어떤 때에 사용할지는 감이 좀 오지 않는다.
이제 그걸 알아보자