backend

[CS] 프로세스와 쓰레드

co2plant

프로세스와 스레드

프로세스와 스레드는 운영체제가 프로그램을 실행하는 두 단위입니다.
이 글에서는 두 개념의 정의와 차이에서 시작해, 메모리를 어떻게 나눠 쓰는지, 그 구조가 전환 비용과 어떻게 연결되는지, 그리고 컨텍스트 스위칭이 왜 필요하고 어떻게 동작하는지를 순서대로 정리합니다.
마지막에는 자바 관점에서 스레드가 어떻게 다뤄지는지와, 자바 21에서 도입된 가상 스레드까지 이어서 살펴봅니다.

1. 정의와 차이

프로세스는 실행 중인 프로그램의 단위이며, 각자 독립된 메모리 공간을 가져서 서로 공유하지 않습니다.
스레드는 프로세스 안에서 실행되는 흐름의 단위이며, 코드·데이터·힙은 공유하지만 스택과 레지스터는 각자 가집니다.
이 차이가 두 단위의 성격을 결정합니다.
프로세스는 완전히 격리되어 있어 안전한 대신 전환 비용이 크고, 스레드는 많은 영역을 공유하는 덕분에 가볍고 빠르지만 대신 동기화 문제를 신경 써야 합니다.

2. 메모리 구조

스레드끼리 공유하는 영역은 코드 영역, 데이터 영역, 그리고 힙입니다. 반면 스택과 레지스터, 프로그램 카운터(PC)는 스레드마다 따로 가집니다. 스택을 따로 갖는 이유는 각 스레드가 자기만의 함수 호출 흐름과 지역 변수를 관리해야 하기 때문입니다.
이를 그림으로 나타내면 다음과 같습니다. 공유 영역은 모든 스레드가 함께 바라보고, 스택과 레지스터·PC는 스레드마다 하나씩 존재합니다.

프로세스는 이 모든 영역이 격리되어 있습니다. 그래서 프로세스끼리 데이터를 주고받으려면 IPC(Inter-Process Communication)라는 별도의 프로세스 간 통신 방법이 필요합니다.

3. 전환 비용과 동시성·병렬성

프로세스 전환은 메모리 주소 공간 자체를 통째로 바꿔야 합니다. 이 과정에서 캐시나 TLB(Translation Lookaside Buffer) 같은 것이 무효화되고 다시 채워져야 하므로 비용이 큽니다. 반면 스레드끼리는 같은 주소 공간을 공유하므로, 전환할 때 바꿀 것이 적어 상대적으로 가볍습니다.
여기서 함께 구분해 둘 개념이 동시성과 병렬성입니다. 싱글 코어에서 여러 스레드를 아주 빠르게 번갈아 실행하는 것은 동시성(concurrency)이고, 멀티 코어에서 여러 스레드를 실제로 한꺼번에 실행하는 것은 병렬성(parallelism)입니다. 동시성은 번갈아 실행해 동시에 도는 것처럼 보이게 하는 것이고, 병렬성은 물리적으로 동시에 실행하는 것입니다.

4. 컨텍스트 스위칭은 왜 필요한가

하나의 프로세스가 CPU를 계속 붙잡고 일하면 좋겠지만, 실제로는 그렇지 않습니다. 실행 도중에 사용자 입력을 기다리거나, 파일을 읽고 쓰거나, 네트워크 응답을 기다리는 일이 자주 생깁니다. 이럴 때 그 프로세스는 당장 할 일이 없어 놀게 됩니다.
CPU를 놀리는 것은 낭비입니다. 그래서 코어 수가 한정되어 있는데도 여러 프로그램이 동시에 도는 것처럼 보이게 하려고, 운영체제가 실행 대상을 빠르게 번갈아 바꿉니다. 이 착시를 만들기 위해 감수하는 대가가 바로 컨텍스트 스위칭입니다.

5. 컨텍스트 스위칭은 어떻게 동작하는가

실행 중이던 작업을 멈출 때는, 그 작업이 어디까지 실행했는지를 저장해야 합니다. 구체적으로는 프로그램 카운터와 레지스터 값들을 PCB(Process Control Block)에 저장합니다. 그리고 다음에 실행할 작업의 저장된 상태를 다시 불러와 이어서 실행합니다.

graph TB
    A["작업 A 실행 중"] --> B["작업 A의 PC · 레지스터 값을<br/>PCB에 저장"]
    B --> C["작업 B의 상태를<br/>PCB에서 복원"]
    C --> D["작업 B 실행 재개"]

여기서 중요한 점은, 이렇게 저장하고 복원하는 작업 자체는 아무런 생산적인 일을 하지 않는 순수한 오버헤드라는 것입니다. 그래서 전환이 지나치게 잦으면 오히려 전체 성능이 떨어질 수 있습니다.

6. 자바 스레드와 스레드 풀

자바 애플리케이션을 실행하면 운영체제 입장에서는 하나의 JVM 프로세스로 뜹니다. 우리가 만드는 스레드들은 모두 그 하나의 프로세스 안에서 동작합니다. 여기서 핵심은 HotSpot JVM에서 자바 스레드 하나가 운영체제 스레드 하나에 1:1로 매핑된다는 점입니다. 그래서 실제 스케줄링은 JVM이 아니라 OS 스케줄러가 담당하고, JVM은 우선순위 같은 힌트만 전달합니다.

graph TB
    subgraph JVMProc["JVM 프로세스"]
        JT1["자바 스레드 1"]
        JT2["자바 스레드 2"]
        JT3["자바 스레드 3"]
    end
    JT1 -->|1:1| OS1["OS 스레드 1"]
    JT2 -->|1:1| OS2["OS 스레드 2"]
    JT3 -->|1:1| OS3["OS 스레드 3"]
    OS1 --> K["OS 스케줄러"]
    OS2 --> K
    OS3 --> K

그런데 OS 스레드는 각각 스택 메모리를 차지합니다. 플랫폼에 따라 다르지만 대략 1MB 안팎입니다. 그래서 스레드를 수천 개씩 만들면 메모리도 문제가 되고 컨텍스트 스위칭 비용도 크게 늘어납니다. 이 때문에 실무에서는 스레드 풀을 만들어 정해진 개수만 재사용합니다. 스프링의 요청 처리도 이 방식을 따릅니다.

7. 가상 스레드

가상 스레드(Virtual Thread)는 자바 21에서 정식 도입되었으며, Project Loom의 결과물입니다. 웹 서버처럼 요청이 수만 개씩 들어오는 상황에서 1:1 매핑의 한계를 넘기 위해 나왔습니다. 기존 스레드와의 차이는 크게 두 가지입니다.
첫째, 전환 주체가 다릅니다. 기존 스레드는 커널이 전환을 담당하므로 커널까지 다녀오는 비용이 듭니다. 반면 가상 스레드는 JVM이 유저 영역에서 직접 전환하므로 훨씬 저렴합니다.
둘째, 스택을 가볍게 다룹니다. 가상 스레드가 입출력 대기처럼 놀고 있을 때는, 스택 내용을 힙에 접어서 보관하고 실제 OS 스레드(캐리어 스레드)를 반납합니다. 그리고 다시 실행할 때만 스택을 펼칩니다. 덕분에 수십만 개를 만들어도 부담이 적습니다.

graph TB
    subgraph V["가상 스레드 · 수십만 개까지 가능"]
        V1["VT 1"]
        V2["VT 2"]
        V3["VT 3"]
        V4["VT 4"]
        Vn["..."]
    end
    V1 --> C1["캐리어 스레드 1"]
    V2 --> C1
    V3 --> C2["캐리어 스레드 2"]
    V4 --> C2
    Vn --> C2
    C1 --> OS1["OS 스레드"]
    C2 --> OS2["OS 스레드"]

다만 가상 스레드가 항상 유리한 것은 아닙니다. CPU를 계속 사용하는 작업보다는, 입출력 대기가 많은 작업에서 이점이 큽니다. 대기 중에 캐리어 스레드를 반납하는 구조에서 이득이 나오기 때문입니다.

정리

프로세스와 스레드의 차이를 메모리의 관점에서 봤습니다. 스레드는 코드·데이터·힙을 공유하고 스택과 레지스터·PC를 각자 갖습니다. 공유하는 영역이 있어 동기화가 필요하고, 주소 공간을 공유하기 때문에 스레드 전환이 프로세스 전환보다 가볍습니다. 컨텍스트 스위칭은 CPU를 놀리지 않기 위한 전환이며, 그 자체는 순수한 오버헤드입니다. 자바에서는 스레드가 OS 스레드에 1:1로 매핑되어 스레드 풀로 재사용하는 방식이 일반적이었고, 자바 21의 가상 스레드는 이 1:1 매핑의 한계를 유저 영역 전환과 가벼운 스택 관리로 넘어섭니다.


참고 자료