Back to blog
Jan 05, 2024
6 min read

프로세스와 스레드

프로세스와 스레드의 개념, 메모리 구조, 생명주기, 실무 활용

프로세스(Process)와 스레드(Thread)는 프로그램 실행의 기본 단위다. 프로세스는 실행 중인 프로그램의 인스턴스로 독립적인 메모리 공간을 가지고, 스레드는 프로세스 내에서 실행되는 흐름의 단위로 같은 프로세스의 스레드들은 메모리를 공유한다.

백엔드 개발에서는 웹 서버의 동시성 모델, 데이터베이스 연결 관리, 비동기 처리 등에서 프로세스와 스레드를 직접 다룬다. 이 개념을 이해하면 성능 최적화와 아키텍처 설계에 도움이 된다.

프로세스 (Process)

프로세스는 운영체제로부터 자원을 할당받는 작업의 단위다. 프로그램을 실행하면 운영체제는 새로운 프로세스를 생성하고, 메모리, CPU 시간, 파일 디스크립터 등의 자원을 할당한다.

각 프로세스는 Code, Data, Heap, Stack 영역으로 구성된 독립적인 주소 공간을 가진다. 한 프로세스는 다른 프로세스의 메모리에 직접 접근할 수 없으며, 이는 프로세스 간 격리(isolation)를 보장한다.

Shell에서 명령어를 실행할 때마다 새로운 프로세스가 생성된다. ls 명령을 입력하면 Shell이 자식 프로세스를 생성하여 ls 프로그램을 실행하고, 완료되면 프로세스가 종료된다.

스레드 (Thread)

스레드는 프로세스 내에서 실행되는 여러 흐름의 단위다. 하나의 프로세스는 여러 스레드를 가질 수 있으며, 이를 멀티스레딩이라고 한다.

같은 프로세스의 모든 스레드는 Code, Data, Heap 영역의 메모리를 공유한다. 각 스레드는 독립적인 Stack만 가지며, 이는 함수 호출과 지역 변수를 위한 공간이다.

같은 프로세스의 스레드는 메모리를 공유하므로 데이터 교환이 빠르고 간단하다. 전역 변수나 Heap에 할당된 객체를 여러 스레드가 직접 접근하기 때문에, 동시 접근으로 인한 Race Condition 문제가 발생할 수 있다.

웹 서버가 HTTP 요청을 받으면 스레드 풀에서 스레드를 하나 가져와 처리한다. 요청 처리가 끝나면 스레드는 풀로 돌아가 다음 요청을 기다리며 재사용되므로 매번 생성/삭제 비용을 절약할 수 있다.

프로세스 주소 공간

프로세스는 물리 메모리를 직접 사용하지 않고 가상 주소 공간을 사용한다. 운영체제는 가상 주소를 물리 주소로 매핑하여 각 프로세스가 마치 전체 메모리를 독점하는 것처럼 동작하게 한다.

프로세스 주소 공간은 4개의 주요 영역으로 구성된다.

주소 (64-bit Linux)
─────────────────────
0x7fff_ffff_ffff     ← 최상위 주소 (커널 영역)
    ...
0x7fff_1234_5678     ← Stack 영역 (지역 변수, 함수 호출)
    ...               ← Stack ↓ 성장 방향
    ...               ← 여유 공간
    ...               ← Heap ↑ 성장 방향
0x0055_5555_8000     ← Heap 영역 (동적 할당)
0x0055_5555_4000     ← Data 영역 (.data, .bss)
0x0055_5555_0000     ← Code 영역 (실행 코드)
0x0000_0000_0000     ← 최하위 주소 (접근 불가)

Code 영역 (Text Segment)

Code 영역은 컴파일러가 소스 코드를 기계어로 변환한 명령어를 저장한다. 프로그램 실행 중 코드가 변경되면 예측 불가능한 동작이 발생하므로, 운영체제는 이 영역을 읽기 전용으로 보호한다.

같은 프로그램을 여러 프로세스가 실행할 때, Code 영역은 프로세스 간 안전하게 공유할 수 있어 메모리를 절약한다.

Data 영역

Data 영역은 전역 변수와 정적 변수를 저장하며 프로그램 시작부터 종료까지 메모리에 유지된다.

.data 섹션은 명시적으로 초기값이 지정된 전역/정적 변수를 저장한다. 실행 파일에 초기값이 포함되어 프로그램 로드 시 메모리에 복사된다.

.bss 섹션은 초기값이 없는 전역/정적 변수를 저장한다. 프로그램 로드 시 운영체제가 자동으로 0으로 초기화한다. 실행 파일에는 크기 정보만 저장되므로 파일 크기를 줄일 수 있다.

Heap 영역

Heap 영역은 동적 메모리 할당을 위한 공간이다. JavaScript의 new, [], {}, Python의 list, dict, Java의 new 등이 Heap에 메모리를 할당한다.

배열의 크기를 컴파일 타임에 결정할 수 없거나 사용자 입력에 따라 가변 크기의 데이터를 저장해야 할 때 동적 할당을 사용한다.

메모리 누수 예시:

// ❌ 메모리 누수
const cache = {};
app.get("/api/users/:id", (req, res) => {
  const user = db.getUser(req.params.id);
  cache[req.params.id] = user; // 계속 증가
  res.json(user);
});
// 24시간 후 서버 메모리 부족으로 크래시

// ✅ LRU 캐시로 해결
const LRU = require("lru-cache");
const cache = new LRU({ max: 500 }); // 최대 500개

Stack 영역

Stack 영역은 함수 호출과 지역 변수를 관리한다. 각 함수 호출마다 스택 프레임이 생성되고, 함수가 반환되면 자동으로 제거된다.

스택 프레임은 함수 호출에 필요한 정보를 담는다:

  • 반환 주소
  • 매개변수
  • 지역 변수
  • 이전 프레임 포인터

함수가 반환되면 스택 프레임이 자동으로 제거되므로, 지역 변수를 명시적으로 해제할 필요가 없다. 이는 메모리 누수를 방지하는 안전한 방식이다.

Stack 크기는 제한되어 있어, 너무 많은 지역 변수를 선언하거나 깊은 재귀 호출을 하면 Stack Overflow가 발생한다.

Heap vs Stack

특성StackHeap
할당 속도매우 빠름 (포인터 이동)느림 (빈 공간 탐색)
크기제한적 (수 MB)큰 편 (물리 메모리까지)
수명함수 종료 시 자동 해제명시적 해제 필요
멀티스레드스레드마다 독립모든 스레드가 공유
사용 용도지역 변수, 함수 호출큰 객체, 동적 자료구조

프로세스 생명주기

프로세스는 생성, 준비, 실행, 대기, 종료 등 여러 상태를 거친다.

프로세스의 상태

  • New (생성): 프로세스가 막 생성된 초기 상태
  • Ready (준비): CPU만 할당받으면 즉시 실행 가능
  • Running (실행): CPU를 할당받아 명령어 실행 중
  • Waiting (대기): I/O 완료나 이벤트를 기다림
  • Terminated (종료): 실행 완료
# Linux에서 프로세스 상태 확인
$ ps aux
USER  PID %CPU %MEM STAT ...
root  1   0.0  0.1  S    ...  # S = Sleeping (Ready/Waiting)
root  100 2.5  1.2  R    ...  # R = Running
user  200 0.0  0.0  Z    ...  # Z = Zombie (Terminated)

좀비 프로세스와 고아 프로세스

좀비 프로세스는 종료되었지만 부모가 종료 상태를 확인하지 않은 프로세스다. ps에서 Z 상태로 표시되며, PCB가 메모리를 차지한다.

고아 프로세스는 부모가 먼저 종료된 자식 프로세스다. init/systemd가 자동으로 입양하여 정상 종료된다.

컨텍스트 스위칭

CPU가 한 프로세스에서 다른 프로세스로 전환하는 과정을 Context Switching이라고 한다. 멀티태스킹의 핵심이지만, 비용이 크다는 문제가 있다.

Context Switch 과정

  1. 타이머 인터럽트 발생
  2. 현재 프로세스 상태 저장 (레지스터, PC, SP 등)
  3. 현재 프로세스를 Ready 큐에 추가
  4. 스케줄러가 다음 프로세스 선택
  5. 선택된 프로세스 상태 복원
  6. 페이지 테이블 교체 → TLB 플러시
  7. 실행 재개

멀티프로세싱 vs 멀티스레딩

멀티 프로세싱은 여러 프로세스를 사용하여 작업을 병렬로 처리한다. 프로세스 간 완전하게 격리되며, 한 프로세스의 문제가 다른 프로세스에 영향을 주지 않는다. 메모리 샤용량이 크고 생성 비용이 크다는 단점이 있다.

멀티 스레딩은 하나의 프로세스 내에서 여러 스레드를 사용한다. 메모리를 공유해 효율적이며 빠른 데이터 교환하기 때문에 생성 비용이 작다. Race Condition 문제가 생길 수 있고 한 스레드의 문제로 인해 전체 프로세스가 다운될 수 있다.

프로세스와 스레드는 운영체제의 핵심 개념이며, 백엔드 개발에서 성능과 안정성을 결정하는 중요한 요소다. 적절한 선택과 최적화로 효율적인 시스템을 만들 수 있다.