개발자

mvc api 호출 시 thread 상태에 대해서 궁금한 점이 있습니다..

2023년 12월 06일조회 331

스프링에서 api를 호출할 때 어떤 일이 발생하는지 RestTemplate 사용하여 테스트를 해보았습니다. 제 예상으로는 api를 호출하게 되면 io 작업이 발생하여 컨텍스트 스위칭 작업이 발생하여 호출한 쓰레드는 응답이 올 때 까지 blocking 상태를 유지할 것이라 판단하였습니다. 하지만 VisualVM을 사용하여 모니터링 해본 결과 park 상태로 유지되던 쓰레드가 running상태로 바뀌며 해당 api의 응답이 올 때까지 running상태로 유지되고 있음을 확인하였습니다. (응답 속도가 빨라 5초간 sleep 후 응답하는 api를 호출) 실제로 block이 일어나지 않은 것인지 아니면 실제로 컨텍스트 스위칭이 발생한 것은 커널레벨 쓰레드 쪽이라 visualVM이 이를 캐치하지 못한것인지.. (하지만 자바에서는 유저레벨 쓰레드와 1대1 매칭이 된다고 알고 있는데.. 유저 레벨 쓰레드에도 영향이 있을 것이라 생각합니다.) 머리 속이 너무 혼란스럽네요ㅜㅜ 어떤 부분을 공부하면 될지 키워드라도 알려주실 수 있나요?

이 질문이 도움이 되었나요?
'추천해요' 버튼을 누르면 좋은 질문이 더 많은 사람에게 노출될 수 있어요. '보충이 필요해요' 버튼을 누르면 질문자에게 질문 내용 보충을 요청하는 알림이 가요.

답변 1

인기 답변

장성호님의 프로필 사진

- 기존 답변이 잘못 되어 댓글에 추가로 답변 남겨놓았습니다. 굉장히 흥미있는 내용이라 다음 환경에서 시도해봤습니다. 1. 8080 포트에서 실행 중인 Spring MVC 클라이언트 2. 8081 포트에서 실행 중인 Spring MVC 서버 3. M1 macOS 14.1 테스트 결과는 작성자님과 동일합니다. Park 상태에서 Runnable 상태로 변화하는 이유는, JVM에서 관리하는 User Thread는 실행 가능한 상태가 맞기 때문입니다. 대신 Blocking 상태에 있는 것은 OS Kernel Thread 입니다. 혼동이 오는 이유는 Runnable에 대한 정의가 제대로 되지 않아서 그런 것 같습니다. 오라클 공식 문서에 따르면 Runnable은 다음처럼 정의되어 있습니다. “실행 중인 Thread의 상태이다. 이 상태는 JVM에서 실행 중이지만, 운영 체제의 프로세서처럼 외부 자원을 기다리고 있을 수 있다.” 코드로 남겨놓은 로그를 보시면 SocketDispatcher.read0 같은 메소드를 호출하시는 것을 확인할 수 있습니다. 이는 운영체제 네트워크 I/O 시스템 콜을 호출한 겁니다. 이런 네이티브 메소드는 JVM 바깥에서 실행됩니다. Kernel Thread의 네트워크 작업이 끝날 때까지, User Thread는 Runnable 상태로 대기합니다. 그렇다면 왜 User Thread는 Kernel Thread처럼 동일하게 Blocking이 아니라 Runnable로 관리하는지 의문이 생길 수 있습니다. 이런 저런 자료를 찾아보며 추측한 내용은 다음과 같습니다. 1. 운영 체제와의 독립성 확보: 언어마다 Thread 관리 방법이 다른 것처럼, OS도 방법이 서로 다르다. 따라서 JVM에서 Kernel Thread 상태와 Tight Coupling 하게 구성한다면, JVM 철학과 정반대의 구현이다. JVM을 사용하는 개발자도 OS에 따라 Kernel Thread를 명확히 알아야하므로 이식성이 떨어진다. 2. 성능 최적화: Kernel Thread 작업이 끝나자마자 바로 작업을 처리할 수 있도록 우선순위를 높여둔다. OS별 Kernel Thread 상태를 신경 안 써도 돼서 스케쥴링이 간단해진다. 정리하자면 JVM 철학, 그리고 자원 투자 대비 성능이 안나와서 그런 것 같다는 생각입니다. Kernel Thread 같은 외부 자원을 기다리는 동안 내부적으로 무엇을 하고 있는지, 상태는 어떤지 정확히 관리하면서 싱크 맞추는 건 효율이 별로라는 거죠. 가능한지도 모르겠네요..? 사실상 2개 트랙이면서 별개 내용이라고 생각하시는게 편하실 겁니다. **park 상태는 그냥 HTTP 요청 이벤트를 기다리게끔 LockSupport로 설정해놓은거라 그런 겁니다. 참고자료 오라클 공식 문서: https://docs.oracle.com/javase/7/docs/api/java/lang/Thread.State.html

1"http-nio-8080-exec-10" #50 [38147] daemon prio=5 os_prio=31 cpu=3.58ms elapsed=442.25s tid=0x0000000144b75a00 nid=38147 runnable  [0x0000000174d27000]
2   java.lang.Thread.State: RUNNABLE
3        at sun.nio.ch.SocketDispatcher.read0(java.base@19.0.2/Native Method)
4        at sun.nio.ch.SocketDispatcher.read(java.base@19.0.2/SocketDispatcher.java:47)
5        at sun.nio.ch.NioSocketImpl.tryRead(java.base@19.0.2/NioSocketImpl.java:251)
장성호님의 프로필 사진

장성호

현대오토에버 백엔드2023년 12월 22일

기존 답변을 잘못 드렸습니다. 뭔가 찝찝해서 더 알아봤는데, Tomcat의 NIO Connector를 사용해서 그런거였습니다. RestTemplate의 동기 / 비동기 문제를, BIO / NIO 문제로 혼동했네요. RestTemplate의 호출은 동기더라도 Tomcat의 네트워크 작업은 NIO이기 때문에, Blocked 상황이 없는 겁니다. 그래서 Runnable 상황인 거였습니다.

지금 가입하면 모든 질문의 답변을 볼 수 있어요!

현직자들의 명쾌한 답변을 얻을 수 있어요.

또는

이미 회원이신가요?

AI 답변
커리어리 AI 봇 프로필 이미지

커리어리 AI 봇

GPT가 달아주는 답변입니다.

2023년 12월 06일

스프링의 RestTemplate는 기본적으로 Java의 HTTP Client API를 사용하여 HTTP 요청을 보내고, 이 과정에서 내부적으로 소켓 통신을 수행하게 됩니다. 이때 발생하는 네트워크 I/O 작업은 블록킹(blocking) 작업으로, 해당 작업이 완료될 때까지 현재 쓰레드가 대기 상태에 빠지게 됩니다. 하지만 여기에 혼돈을 주는 것이 `park` 상태와 `running` 상태의 변화인 것 같습니다. 이 두 가지 상태는 Java Concurrency에서 스레드 관리에 사용되는 상태입니다. - `park`: 스레드가 LockSupport.park() 메소드에 의해 일시 중지된 상태를 말합니다. 즉, 작업은 실행 가능한 상태이나 처리를 위해 다른 조건들이 충족되어야 합니다. - `running`: 실제로 스레드가 CPU의 자원을 점유하고 실제로 코드를 수행하고 있는 상태를 말합니다. 그런데 여기서 중요한 점은 한 쓰레드가 '실행 가능한(runnable)'상태라 하더라도 실제로 항상 CPU자원을 점유하는 것은 아니라는 점입니다. OS 레벨에서 여러 쓰레드(혹은 프로세스)들 사이에서 컨텍스트 스위칭이 발생하게 되면, 싱글 코어에서도 여러 스레드가 동시에 실행되는 것처럼 보일 수 있습니다. 따라서 RestTemplate을 호출하면서 발생하는 쓰레드의 상태 변화를 본다면, 실제로는 네트워크 I/O 작업 중에 `running` 상태에서 `park` 상태로 변경되어 대기하고 있는 것이 맞을 것 같습니다. VisualVM으로 보았을 때 이 과정이 잘 수집되지 않았다면, 표본 수집의 타이밍 문제일 수 있습니다. 결론적으로 말씀드리자면, 핵심 키워드는 Java의 Concurrency와 네트워크 I/O, 그리고 OS 레벨에서의 컨텍스트 스위칭 등을 깊게 이해하는 것입니다. 추가적으로 JVM 모니터링 도구나 Thread Dump 분석 등에 대한 지식도 필요할 것 같습니다.

목록으로

지금 가입하면 모든 질문의 답변을 볼 수 있어요!