최근 프로젝트에 Spring Actuator, Prometheus, Grafana를 도입해 컨테이너 자원과 애플리케이션 상태를 모니터링하게 되면서, 하나의 의문이 생겼습니다.
"스프링 부트는 동시에 들어오는 여러 요청을 어떻게 처리할까?"
평소에는 당연하게 느끼던 부분이지만, 막상 모니터링 지표에서 다양한 스레드 관련 메트릭이 표시되자 호기심이 생겼다. 그래서 이참에 Java의 스레드 개념을 정리하고, 동시성과 병렬성에 대한 기본기를 다지고자 학습을 시작했다.
이 글은 그 학습 과정과 내용을 정리한 글입니다. 또한 여러 시리즈로 학습하며 포스팅할 예정입니다.
프로세스와 스레드
스레드를 공부하기 위해서 가장 기본적인 용어를 정리한다.
멀티 태스킹과 멀티 프로세싱
멀티 스레드에 대해서 제대로 이해하려면 먼저 멀티 태스킹과 프로세스 같은 운영체제의 기본 개념들에 대해서 알아야 한다. 여기서 멀티 스레드를 이해하기 위한 목적으로 최대한 단순하게 핵심 내용을 알아본다.
🐻단일 프로그램 실행
그림을 설명하면, 두 개의 프로그램 (A, B)을 동시에 실행한다고 가정한다. 예를 들어
- 음악 프로그램을 통해 음악을 듣는다.
- 워드 프로그램을 통해 문서를 작성한다.
여기서 연산을 처리할 수 있는 CPU 코어 1개만 있다고 가정한다.
이처럼 CPU 코어가 한 가지의 프로그램을 모두 실행한 후 다음 프로그램을 실행하는 것을 단일 프로그램 실행이라고 한다.
- 프로그램 실행 : 프로그램을 구성하는 코드를 순서대로 CPU에서 연산하는 일
- CPU 코어는 하나로 가정하므로, 한 번에 하나의 프로그램 코드만 실행할 수 있다.
- 하나의 프로그램 안에 있는 코드를 모두 실행한 후에야 다른 프로그램의 코드를 실행할 수 있다.
- 실제 초창기 컴퓨터는 이처럼 한 번에 하나의 프로그램만 실행했다.
- 이를 해결하기 위해 하나의 CPU 코어로 여러 프로그램을 동시에 실행하는 멀티 태스킹 기술이 등장했다.
🐻멀티 태스킹
실세계에서 영화 필름을 생각하면 매우 빠르게 필름들이 지나가며 사람은 하나의 영상처럼 인지하게 된다. 이처럼 CPU는 초당 수십억 번의 연산을 빠르게 프로그램 A, B를 번갈아가며 수행한다.
CPU가 매우 빠르게 두 프로그램 코드를 번갈아 수행한다면, 사람이 느낄 때 두 프로그램이 동시에 실행되는 것처럼 느껴진다. 이 방식은 CPU 코어가 프로그램 A의 코드를 0.01초 정도 수행하다가 잠시 멈추고, 프로그램 B의 코드를 0.01초 정도 수행한다. 그리고 다시 프로그램 A의 이전에 실행 중이던 코드로 돌아가서 0.01초 정도 코드를 수행하는 방식으로 동작한다.
이렇게 하나의 컴퓨터 시스템이 동시에 여러 작업을 수행하는 능력을 멀티 태스킹이라고 한다.
CPU의 어떤 프로그램이 얼마만큼 실행될지는 운영체제가 결정하는데 이것을 스케줄링이라고 한다.
🐻 멀티 프로세싱
CPU 코어가 둘 이상으로 함께 작업을 나누어 수행한다. 즉 일꾼이 두 명 이상이 있는 것을 말한다.
CPU 안에는 실제 연산을 처리할 수 있는 코어라는 것이 있다. 과거에는 하나의 CPU 안에 보통 하나의 코어만 들어있었다. 최근에는 하나의 CPU 안에 보통 2개 이상의 코어가 들어있다.
멀티 프로세싱은 컴퓨터 시스템에서 둘 이상의 프로세서 (CPU 코어)를 사용하여 여러 작업을 동시에 처리하는 기술을 의미한다. 멀티 프로세싱 시스템은 하나의 CPU 코어만을 사용하는 시스템보다 동시에 더 많은 작업을 수행한다.
🐻정리
- 멀티 프로세싱
- 여러 CPU (여러 코어)를 사용하여 동시에 여러 작업을 수행하는 것을 의미한다.
- 하드웨어 기반의 성능을 향상한다.
- 예 : 다중 코어 프로세서를 사용하는 현대 컴퓨터 시스템
- 멀티태스킹
- 단일 CPU가 여러 작업을 동시에 수행하는 것처럼 보이게 하는 것을 의미한다.
- 소프트웨어 기반으로 CPU 시간을 분할하여 각 작업을 할당한다.
- 예 : 현대 운영 체제에서 여러 애플리케이션이 동시에 실행되는 환경
프로세스와 스레드
🐻프로세스
- 프로그램은 실제 실행되기 전까지는 단순한 파일에 불과하다.
- 프로그램을 실행하면 프로세스가 만들어지고, 프로그램이 실행된다.
- 이렇게 운영체제 안에서 실행 중인 프로그램을 프로세스라고 한다.
- 프로세스는 실행 중인 프로그램의 인스턴스이다.
- 자바 언어로 비유를 하자면 클래스는 프로그램 파일이고, 인스턴스는 프로세스이다.
프로세스는 실행 중인 프로그램의 인스턴스이다. 각 프로세스는 독립적인 메모리 공간을 갖고 있으며, 운영체제에서 별도의 작업 단위로 분리해서 관리된다. 각 프로세스는 별도의 메모리 공간을 갖고 있기 때문에 서로 간섭하지 않는다. 그리고 프로세스가 서로의 메모리에 직접 접근할 수 없다.
프로세스는 이렇듯 서로 격리되어 관리되기 때문에, 하나의 프로세스가 충돌해도 다른 프로세스에는 영향을 미치지 않는다. 쉽게 이야기해서 특정 프로세스(프로그램)에 심각한 문제가 발생하면 해당 프로세스만 종료되고, 다른 프로세스에는 영향을 미치지 않는다.
프로세스의 메모리 구성
- 코드 섹션 : 실행할 프로그램의 코드가 저장되는 부분
- 데이터 섹션 : 전역 변수 및 정적 변수가 저장되는 부분
- 힙 : 동적으로 할당되는 메모리 영역으로 객체의 주소 저장
- 스택 : 메서드(함수) 호출 시 생성되는 지역 변수와 반환 주소가 저장되는 영역 (스레드에 포함)
🐻스레드 (Thread)
프로세스는 하나 이상의 스레드를 반드시 포함한다.
스레드는 프로세스 내에서 실행되는 작업의 단위이다. 한 프로세스 내에서 여러 스레드가 존재할 수 있으며, 이들은 프로세스가 제공하는 동일한 메모리 공간을 공유한다. 스레드는 프로세스보다 단순하므로 생성 및 관리가 단순하고 가볍다.
메모리 구성
- 공유 메모리 : 같은 프로세스의 코드 섹션, 데이터 섹션, 힙(메모리)은 프로세스 안의 모든 스레드가 공유한다.
- 개별 스택 : 각 스레드는 자신의 스택을 갖고 있다.
프로그램이 실행된다는 것은 운영체제가 먼저 디스크에 있는 파일 덩어리인 프로그램을 메모리로 불러오면서 프로세스를 만든다. 프로그램이 실행된다는 것은 사실 프로세스 안에 있는 코드가 한 줄씩 실행되는 것이다. 코드는 보통 main()부터 시작해서 하나씩 순서대로 내려가면서 실행된다.
생각해 보면 어떤 무언가가 코드를 하나씩 순서대로 실행하기 때문에 프로그램이 작동하고 계산도 하고, 출력도 할 수 있다. 이 코드를 하나씩 실행하면서 내려가는 것의 정체가 무엇일까?
마치 실(thread) 같은 것이 코드를 위에서 아래로 하나씩 꿰면서 내려가는 것 같다. 이렇듯 프로세스의 코드를 실행하는 흐름을 스레드라 한다.
스레드는 프로세스 내에서 실행되는 작업의 단위다. 한 프로세스 내에 하나의 스레드가 존재할 수 있고, 한 프로세스 내에 여러 스레드가 존재할 수 있다. 그리고 스레드는 프로세스가 제공하는 동일한 메모리 공간을 공유한다.
- 단일 스레드 : 한 프로세스 내에 하나의 스레드만 존재한다.
- 멀티 스레드 : 한 프로세스 내에 여러 스레드가 존재한다.
하나의 프로세스 안에는 최소한 하나 이상의 스레드가 존재한다. 그래야 프로그램이 실행될 수 있다. 참고로 지금까지 작성한 자바 코드들은 모두 한 프로세스 내에서 하나의 스레드만 사용하는 단일 스레드였다.
🐻정리
정리하면 프로세스는 실행 환경과 자원을 제공하는 컨테이너 역할을 하고, 스레드는 CPU를 사용해서 코드를 하나하나 실행한다.
멀티 스레드가 필요한 이유
하나의 프로그램도 그 안에서 동시에 여러 작업이 필요하다.
- 워드 프로그램을 문서로 편집하면서, 문서가 자동으로 저장되고, 맞춤법 검사도 함께 수행한다.
- 유튜브는 영상을 보는 동안, 댓글도 달 수 있다.
운영체제 관점에서 보면 다음과 같이 구분할 수 있다. 2개의 프로그램
- 워드 프로그램 - 프로세스 A
- 스레드 1 : 문서 편집
- 스레드 2 : 자동 저장
- 스레드 3 : 맞춤법 검사
- 유튜브 - 프로세스 B
- 스레드 1 : 영상 재생
- 스레드 2 : 댓글 작성
스레드와 스케줄링
앞서 멀티태스킹에서 설명한 운영체제의 스케줄링을 더 자세히 알아보자.
CPU 코어는 1개이고, 프로세스는 2개이다. 프로세스 A는 스레드 1개, 프로세스 B는 스레드가 2개 있다. 프로세스는 실행 환경과 자원을 제공하는 컨테이너 역할을 하고, 실제 CPU를 사용해서 코드를 하나하나 실행하는 것은 스레드이다.
- 프로세스 A에 있는 스레드 A1을 CPU가 실행한다.
- 프로세스 A에 있는 스레드 A1의 실행을 잠시 멈추고 프로세스 B에 있는 스레드 B1을 실행한다.
- 프로세스 B에 있는 스레드 B1의 실행을 잠시 멈추고 같은 프로세스의 스레드 B2를 실행한다.
- 이후에 프로세스 A에 있는 스레드 A1을 실행한다.
- 이 과정을 프로세스가 끝날 때까지 반복한다.
🐻단일 코어 스케줄링
운영체제가 스레드를 어떻게 스케줄링하는지, 스케줄링 관점으로 알아보자.
운영체제는 내부에 스케줄링 큐를 가지고 있고, 각각의 스레드는 스케줄링 큐에서 대기한다.
🐻멀티 코어 스케줄링
CPU 코어가 2개 이상이면 한 번에 더 많은 스레드를 물리적으로 진짜 동시에 실행할 수 있다.
참고
CPU에 어떤 프로그램이 얼마만큼 실행될지는 운영체제가 결정하는데 이것을 스케줄링이라 한다. 이때 단순히 시간으로만 작업을 분리하지 않고, CPU를 최대한 활용할 수 있는 다양한 우선순위와 최적화 기법을 사용한다. 우리는 운영체제가 스케줄링을 수행하고, CPU를 최대한 사용하면서 작업이 골고루 수행될 수 있게 최적화된다는 정도만 이해하면 충분하다.
로빈 후드 스케줄링, RR 등등 정보처리기사 시험을 볼 때 스케줄링 기법에 대해서 학습했었다. 그런 방식으로 최적화된 스케줄링을 통해서 프로그램을 실행한다.
🐻정리
프로세스, 스레드, 스케줄링
멀티 태스킹과 스케줄링
- 멀티 태스킹이란 동시에 여러 작업을 수행하는 것을 말한다. 이를 위해 운영체제는 스케줄링이라는 기법을 사용한다. 스케줄링은 CPU 시간을 여러 작업에 나누어 배분하는 방식이다.
프로세스와 스레드
- 프로세스는 실행 중인 프로그램의 인스턴스이다. 각 프로세스는 독립적인 메모리 공간을 가지며, 운영체제에서는 독립된 실행 단위로 취급된다.
- 스레드는 프로세스 내에서 실행되는 작은 단위이다. 여러 스레드는 하나의 프로세스 내에서 자원을 공유하며, 프로세스의 코드, 데이터, 시스템 자원 등을 공유한다. 실제로 CPU에 의해 실행되는 단위는 스레드이다.
프로세스의 역할
- 프로세스는 실행 환경을 제공한다. 여기에는 메모리 공간, 파일 핸들, 시스템 자원(네트워크 연결) 등이 포함된다. 이는 프로세스가 컨테이너 역할을 하다는 의미이다.
- 프로세스 자체는 운영체제의 스케줄러에 의해 직접 실행되지 않으며, 프로세스 내의 스레드가 실행된다. 참고로 1개의 프로세스 안에는 하나의 스레드만 실행되는 경우도 있고, 1개 이상의 스레드가 실행되는 경우가 있다.
콘텍스트 스위칭
멀티 태스킹이 반드시 효율적인 것만은 아니다.
🐻컴퓨터의 멀티 태스킹
운영체제의 멀티 태스킹을 생각해 보자. CPU 코어는 하나만 있다고 가정하자.
스레드 A, 스레드 B 가 있다.
운영체제는 먼저 스레드 A를 실행한다. 멀티태스킹을 해야 하기 때문에 스레드 A를 계속 실행할 수 없다. 스레드 A를 잠시 멈추고 스레드 B를 실행한다. 이후에 스레드 A로 그냥 돌아올 수 없다. CPU에서 스레드를 실행하는데, 스레드 A의 코드가 어디까지 수행되었는지 위치를 찾아야 한다. 그리고 계산하던 변수들의 값을 CPU에 다시 불러들여야 한다.
따라서 스레드 A를 멈추는 시점에 CPU에서 사용하던 이런 값들을 메모리에 저장해두어야 한다. 그리고 이후에 스레드 A를 다시 실행할 때 메모리에서 이 값들을 CPU에 다시 불러와야 한다.
이러한 과정을 콘텍스트 스위칭이라고 한다. 결과적으로 콘텍스트 스위칭 과정에는 약간의 비용이 발생한다.
멀티스레드는 대부분 효율적이지만, 콘텍스트 스위칭 과정이 필요하므로 항상 효율적인 것은 아니다. 만약 운영체제가 엄청 많은 스레드를 처리하기 위해서 스케줄링을 하였는데, 이전 계산하던 값들을 메모리에 올리는 시간이 길어 다른 작업으로 바로 넘어가게 되면 스레싱이라는 현상이 발생한다. 그럼 처리되는 작업이 없이 페이지 교체만 끊임없이 반복하게 된다.
스레싱이란 CPU 작업 시간보다 메모리와 스왑 영역 간 페이지 교체에 시간을 많이 소비하는 것을 말한다
🐻CPU 바운드 작업 vs I/O 바운드 작업
각각의 스레드가 하는 작업은 크게 2가지로 구분할 수 있다.
- CPU - 바운드 작업
- CPU의 연산 능력을 많이 요구하는 작업을 의미한다.
- 이러한 작업은 주로 계산, 데이터 처리, 알고리즘 실행 등 CPU의 처리 속도가 작업 완료 시간을 결정하는 경우다.
- 예시 : 복잡한 수식 계산, 데이터 분석, 비디오 인코딩, 과학적 시뮬레이션 등
- I/O 바운드 작업
- 디스크, 네트워크, 파일 시스템 등과 같은 입출력 (I/O) 작업을 많이 요구하는 작업을 의미한다.
- 이러한 작업은 I/O 작업이 완료될 때까지 대기 시간이 많이 발생하며, CPU는 상대적으로 유후(대기) 상태에 있는 경우가 많다. 쉽게 이야기해서 스레드가 CPU를 사용하지 않고 I/O 작업이 완료될 때까지 대기한다.
- 예시 : 데이터베이스 쿼리 처리, 파일 읽기/쓰기, 네트워크 통신, 사용자 입력 처리 등.
🐻웹 애플리케이션 서버
분야마다 다르겠지만, 실무에서 CPU-바운드 작업보다는 I/O 바운드 작업이 많다.
예를 들어서 백엔드 개발자의 경우 주로 웹 애플리케이션 서버를 개발하는데, 스레드가 1에서 1,000,000,000까지 더하는 CPU의 연산이 필요한 작업보다는, 대부분 사용자의 입력을 기다리거나, 데이터베이스를 호출하고 그 결과를 기다리는 일이 많다. 쉽게 이야기해서 스레드가 CPU를 많이 사용하지 않는 I/O 바운드 작업이 많다는 뜻이다.
일반적으로 자바 웹 애플리케이션 서버의 경우 사용자의 요청 하나를 처리하는데 1개의 스레드가 필요하다. 사용자 4명이 동시에 요청하면 4개의 스레드가 작동하는 것이다. 그래야 4명의 사용자의 요청을 동시에 처리할 수 있다. 사용자의 요청을 처리하는데, 스레드는 CPU 1% 정도 사용하고, 대부분 데이터베이스 서버에 어떤 결과를 조회하면서 기다린다고 가정하자.
이때는 CPU를 거의 사용하지 않고 (유후 상태) 대기한다. 바로 I/O 바운드 작업이 많다.
🐻정리
정리하면 스레드의 숫자는 CPU - 바운드 작업이 많은가, 아니면 I/O 바운드 작업이 많은가에 따라 다르게 설정해야 한다.
- CPU-바운드 작업 : CPU 코어 수 + 1
- CPU를 거의 100% 사용하는 작업이므로 스레드를 CPU 숫자에 최적화
- I/O 바운드 작업 : CPU 코어 수보다 많은 스레드 생성, CPU를 최대한 사용할 수 있는 숫자까지 스레드 생성
- CPU를 많이 사용하지 않으므로 성능 테스트를 통해 CPU를 최대한 활용하는 숫자까지 스레드 생성
- 단 너무 많은 스레드를 생성하면 콘텍스트 스위칭 비용도 함께 증가 - 적절한 성능 테스트
참고로 웹 애플리케이션 서버라도 상황에 따라 CPU 바운드 작업이 많을 수 있다. 이 경우 CPU 바운드 작업에 최적화된 CPU 숫자를 고려하면 된다.