프로세스 · 스레드 · 운영체제
프로세스와 스레드 구분하기
독립된 메모리를 갖는 프로세스와 메모리를 공유하는 스레드의 차이를 이해하고 동시성과 병렬성을 구분합니다.
무료 공개 · 최근 수정
한 지붕 아래 주방과 요리사의 분업
도심의 번화가에 위치한 대형 음식점의 구조를 떠올려 봅니다. 건물 안에는 서로 완전히 분리된 독립된 주방들이 입점해 있습니다. 각 주방은 자체적인 수도와 가스 배관, 냉장고, 조리대를 독점적으로 사용하며 다른 주방의 기구에 마음대로 손을 댈 수 없습니다. 한쪽 주방에 화재나 누수가 발생해 문을 닫더라도, 벽으로 격리된 옆 주방은 아무런 방해 없이 요리를 이어갈 수 있습니다.
이 독립된 주방 하나가 운영체제에서의 프로세스(Process)에 해당합니다. 그리고 그 주방 안에서 동일한 조리대와 냉장고를 함께 쓰며 각자의 도마에서 양파를 썰고 스테이크를 굽는 복수의 요리사들이 바로 스레드(Thread)입니다. 요리사들은 주방 안의 식재료를 자유롭게 공유하므로 협업 속도가 매우 빠르지만, 두 요리사가 좁은 도마 하나를 두고 동시에 칼질을 하려 들면 손을 다치는 사고가 일어날 수 있습니다.
컴퓨터의 운영체제가 한정된 CPU와 메모리 자원을 효율적으로 쪼개어 수많은 프로그램을 매끄럽게 구동하는 밑바탕에도 이 주방과 요리사의 관계와 동일한 설계 철학이 자리 잡고 있습니다.
독립된 방을 가진 프로세스와 메모리 격리
디스크에 저장되어 있는 실행 파일은 정적인 기계어 바이너리 묶음일 뿐입니다. 사용자가 아이콘을 더블클릭하거나 터미널에서 명령을 입력하면, 운영체제는 이 파일을 메인 메모리에 적재하고 가상 메모리 관리자를 통해 독립적인 4GB(32비트) 또는 수백 테라바이트(64비트) 규모의 가상 주소 공간을 할당합니다. 이 순간 정적 프로그램은 생명력을 얻어 실행 중인 인스턴스가 됩니다.
프로세스의 주소 공간은 기계어 명령어가 적재되는 코드(Code), 전역 변수가 저장되는 데이터(Data), 런타임에 동적으로 크기가 늘어나는 힙(Heap), 그리고 함수 호출 이력을 추적하는 스택(Stack)의 4개 영역으로 엄격히 구획됩니다.
운영체제의 하드웨어 메모리 관리 장치(MMU)는 프로세스 A가 프로세스 B의 메모리 주소를 절대 직접 읽거나 쓰지 못하도록 하드웨어 수준에서 차단합니다. 프로세스 하나가 치명적인 세그멘테이션 오류를 일으켜 강제 종료되더라도 전체 시스템이 뻗지 않고 다른 프로그램들이 정상 동작을 유지하는 비결이 바로 이 격리성에 있습니다. 만약 서로 다른 프로세스끼리 협력하여 데이터를 주고받아야 한다면 소켓이나 파이프, 공유 메모리 같은 프로세스 간 통신(IPC; Inter-Process Communication) 창구를 명시적으로 열어야 합니다.
힙은 함께 쓰고 스택은 따로 쓰는 스레드 구조
프로세스가 안전한 격리를 제공하는 대신 새로운 프로세스를 복제하거나 생성하는 작업은 운영체제 입장에서 무거운 페이지 테이블 복사와 자원 할당을 수반합니다. 하나의 작업 단위 안에서 복수의 연산을 가볍게 쪼개어 동시 실행하기 위해 고안된 경량 단위가 스레드입니다.
독립된 두 프로세스의 메모리 경계와 단일 프로세스 안에서 복수의 스레드가 자원을 나누어 쓰는 방식을 도식으로 대조해 봅니다.
그림: 독립 프로세스의 주소 공간 격리와 스레드 간 자원 공유 구조
구조도를 살펴보면 프로세스 A 내부의 스레드들은 부모 프로세스의 코드와 전역 데이터, 그리고 동적 힙 공간을 완전히 공유하고 있습니다. 스레드 1이 힙에 객체를 생성하면 스레드 2는 별도의 네트워크 통신이나 복사 과정 없이 그 객체의 메모리 주소 포인터를 즉시 읽을 수 있습니다.
반면 스레드마다 독자적으로 주어지는 자원은 현재 실행 중인 CPU 명령어 위치를 가리키는 프로그램 카운터(PC)와 함수 호출 시 로컬 변수를 적재하는 스택 프레임뿐입니다. 이 덕분에 스레드는 생성과 소멸에 드는 메모리 오버헤드가 프로세스의 수십 분의 일에 불과합니다.
전환 비용과 동기화의 딜레마
CPU 코어 하나가 여러 작업을 번갈아 수행할 때 작업 대상을 교체하는 과정을 컨텍스트 전환(Context Switching)이라고 부릅니다. 전환의 주체가 프로세스냐 스레드냐에 따라 시스템이 치러야 하는 대가는 완전히 다릅니다.
프로세스 간 전환이 일어날 때는 CPU 레지스터를 백업하는 것은 물론, 페이지 테이블 기준 주소를 교체하고 CPU의 가상 메모리 변환 캐시인 TLB(Translation Lookaside Buffer)를 완전히 비워내야 합니다. 캐시가 증발하면서 전환 직후 심각한 캐시 미스가 발생합니다. 반면 동일 프로세스 내부의 스레드 간 전환은 가상 주소 공간이 동일하므로 TLB를 비울 필요 없이 CPU 레지스터와 스택 포인터만 바꿔 치기하면 되므로 전환 비용이 저렴합니다.
하지만 자원을 공유한다는 장점은 필연적으로 동기화 문제를 낳습니다. 두 스레드가 힙 영역의 전역 카운터 변수를 동시에 증가시키려 할 때 읽기와 쓰기 타이밍이 겹치면 값이 유실되는 경쟁 상태(Race Condition)가 발생합니다. 이를 막기 위해 뮤텍스(Mutex)나 세마포어(Semaphore) 같은 상호 배제 잠금을 걸어야 하며, 잠금을 잘못 설계하면 스레드들이 서로의 잠금이 풀리기만을 무한히 기다리는 교착 상태(Deadlock)에 빠질 위험이 따릅니다.
[참고] 동시성과 병렬성의 차이
동시성은 단일 코어가 작업들을 아주 잘게 쪼개어 번갈아 실행함으로써 사용자에게 마치 동시에 진행되는 것처럼 보이게 만드는 논리적 개념입니다. 병렬성은 여러 개의 물리적 CPU 코어가 동일한 순간에 서로 다른 명령어 스트림을 실제로 동시에 실행하는 물리적 상태를 뜻합니다.
asyncio로 네트워크 I/O 병목 돌파하기
Python 표준 런타임(CPython)에는 단 하나의 스레드만 Python 바이트코드를 실행할 수 있도록 제약하는 전역 인터프리터 락(GIL)이 존재합니다. 따라서 CPU 연산이 주를 이루는 무거운 작업은 스레드를 늘리기보다 multiprocessing을 통해 독립 프로세스로 분산해야 코어를 온전히 활용할 수 있습니다.
그러나 외부 웹 API 호출이나 데이터베이스 질의처럼 네트워크 응답을 마냥 기다려야 하는 I/O 바운드 작업에서는 CPU가 연산을 하지 않고 대기합니다. 이때는 스레드를 무한정 늘리기보다 단일 스레드 안에서 협력적 멀티태스킹을 지원하는 asyncio를 도입해 자원 낭비를 줄이고 높은 동시성(Concurrency)을 확보하는 것이 표준 해법입니다.
import asyncio
import time
async def fetch_remote_resource(task_id: int, latency: float):
# 네트워크 입출력 대기 시뮬레이션
await asyncio.sleep(latency)
return f"도구 {task_id} 수신 완료 ({latency}초)"
async def main():
start_time = time.perf_counter()
# 세 개의 독립적인 I/O 작업을 단일 스레드 이벤트 루프에 동시 예약
responses = await asyncio.gather(
fetch_remote_resource(1, 0.35),
fetch_remote_resource(2, 0.20),
fetch_remote_resource(3, 0.15),
)
total_elapsed = time.perf_counter() - start_time
for item in responses:
print(item)
print(f"전체 소요 시간: {total_elapsed:.2f}초")
asyncio.run(main())위 비동기 코드를 실행하면 다음과 같은 수순으로 결과가 정리됩니다.
예시 출력:
도구 1 수신 완료 (0.35초)
도구 2 수신 완료 (0.20초)
도구 3 수신 완료 (0.15초)
전체 소요 시간: 0.35초동기 방식으로 세 작업을 순차 실행했다면 0.35 + 0.20 + 0.15 = 0.70초가 걸렸을 것입니다. 반면 비동기 방식에서는 세 작업의 네트워크 대기 구간이 시간 축 위에서 포개어집니다.
그림: asyncio.gather의 작업 대기 시간 중첩과 이벤트 루프 제어권 전환
이벤트 루프는 도구 1이 await sleep으로 제어권을 내려놓자마자 대기하지 않고 도구 2와 도구 3을 연달아 깨웁니다. 세 작업이 동시에 응답을 기다리므로 지연 시간이 가장 짧은 도구 3(0.15초), 도구 2(0.20초), 도구 1(0.35초) 순으로 완료되며, 전체 실행 시간은 가장 긴 단일 작업 시간인 0.35초에 수렴합니다.
복수 도구를 호출하는 에이전트의 동시성 설계
프로세스와 스레드, 비동기 동시성의 차이는 AI 에이전트 시스템을 프로덕션 수준으로 구현할 때 결정적인 성능 차이를 만듭니다.
복합적인 사용자 요청을 처리하는 에이전트는 한 번의 추론 단계에서 뉴스 검색, 사내 위키 질의, 환율 계산기 등 세 개 이상의 외부 도구를 실행하도록 판단할 수 있습니다. 각 도구가 서로의 출력에 의존하지 않는 단순 조회 작업이라면, 순차 루프로 도구를 하나씩 호출하는 것은 심각한 사용자 경험 저하를 유발합니다. 도구당 1초가 소요된다면 순차 방식은 3초를 기다려야 하지만, 비동기 동시성이나 스레드 풀을 적용해 병렬 요청하면 1초대에 처리가 끝납니다.
반면 파일 수정이나 계좌 이체처럼 상태를 변경하는 도구는 스레드의 공유 메모리 경쟁 상태와 마찬가지로 실행 순서와 트랜잭션 무결성을 지켜야 합니다. 격리가 필요한 무거운 연산은 별도 프로세스로 격리하고, 수많은 I/O 도구는 비동기 동시성으로 묶어 내는 하이브리드 아키텍처를 설계할 때 빠르고 안정적인 AI 에이전트를 구축할 수 있습니다.
참고 문서
설명이 어렵거나 잘못된 부분을 발견하셨나요?
문서 수정 의견 보내기