프로세스와 스레드는 운영체제에서 프로그램을 실행할 때 가장 자주 등장하는 개념이다. 면접에서는 보통 “프로세스와 스레드의 차이가 무엇인가요?”처럼 묻지만, 핵심은 단순한 정의보다 무엇을 독립시키고 무엇을 공유하는가에 있다.

프로세스는 격리된 실행 단위다

프로세스는 실행 중인 프로그램의 인스턴스다. 운영체제는 프로세스마다 독립된 메모리 공간을 할당한다. 그래서 한 프로세스가 다른 프로세스의 메모리에 직접 접근할 수 없다. 이 격리 덕분에 안정성은 높아지지만, 프로세스끼리 데이터를 주고받으려면 IPC 같은 별도의 통신 방식이 필요하다.

프로세스의 메모리 공간은 보통 Code, Data, Heap, Stack으로 나뉜다. Code 영역에는 실행할 기계어 코드가 있고, Data 영역에는 전역 변수와 정적 변수가 있다. Heap은 실행 중 동적으로 할당되는 메모리이고, Stack은 함수 호출 정보와 지역 변수를 저장한다.

스레드는 같은 프로세스 안의 실행 흐름이다

스레드는 프로세스 내부에서 실행되는 흐름의 단위다. 하나의 프로세스는 최소 하나의 스레드를 가진다. 같은 프로세스에 속한 스레드들은 Code, Data, Heap을 공유한다. 그래서 스레드끼리는 데이터를 주고받기 쉽고, 프로세스보다 생성과 전환 비용도 작다.

다만 Stack은 스레드마다 독립적으로 가진다. Stack에는 함수 호출 순서, 지역 변수, 매개변수, 돌아갈 주소가 들어 있다. 만약 Stack까지 공유한다면 한 스레드의 함수 호출이 다른 스레드의 실행 흐름을 덮어쓸 수 있다. 스레드가 독립적인 실행 흐름을 유지하려면 Stack은 분리되어야 한다.

정리하면 프로세스는 서로 격리된 실행 단위이고, 스레드는 같은 프로세스의 자원을 공유하는 실행 흐름이다. 이 차이가 안정성, 통신 비용, 컨텍스트 스위칭 비용의 차이로 이어진다.

  • 메모리: 프로세스는 독립된 주소 공간을 가지고, 스레드는 같은 프로세스의 Code, Data, Heap을 공유한다.
  • Stack: 프로세스와 스레드 모두 실행 흐름별로 독립적인 Stack을 가진다.
  • 생성과 전환 비용: 프로세스가 더 크고, 스레드가 상대적으로 작다.
  • 통신 방식: 프로세스 간에는 IPC가 필요하고, 스레드는 공유 메모리를 통해 더 쉽게 데이터를 주고받을 수 있다.
  • 안정성: 프로세스는 격리되어 안정성이 높지만, 스레드는 한 스레드의 문제가 프로세스 전체에 영향을 줄 수 있다.

멀티 프로세스와 멀티 스레드

멀티 프로세스는 하나의 작업을 여러 프로세스로 나눠 처리하는 방식이다. 각 프로세스가 독립되어 있으므로 한 프로세스가 죽어도 다른 프로세스에는 영향이 적다. 크롬 브라우저가 탭마다 프로세스를 분리하는 것도 이런 이유다. 한 탭이 멈추거나 죽어도 브라우저 전체가 같이 죽지 않고, 탭 간 메모리 접근도 제한할 수 있다.

대신 프로세스 간 통신은 비용이 크다. 메모리 공간이 분리되어 있기 때문에 데이터를 주고받으려면 파이프, 소켓, 공유 메모리 같은 IPC가 필요하다. 컨텍스트 스위칭 비용과 메모리 사용량도 스레드보다 크다.

멀티 스레드는 하나의 프로세스 안에서 여러 스레드가 작업을 나눠 처리하는 방식이다. 메모리를 공유하므로 통신이 빠르고, 컨텍스트 스위칭 비용도 상대적으로 작다. 웹 서버에서 여러 요청을 동시에 처리하거나, 백그라운드 작업을 분리할 때 자주 사용한다.

하지만 공유한다는 것은 위험도 함께 공유한다는 뜻이다. 한 스레드에서 예외가 발생하거나 공유 자원을 잘못 변경하면 프로세스 전체에 영향을 줄 수 있다. 여러 스레드가 같은 데이터를 동시에 수정하면 Race Condition이 발생할 수 있고, 이를 막기 위해 락이나 동기화 장치가 필요하다.

스레드는 많을수록 좋은가

스레드를 많이 만든다고 항상 성능이 좋아지는 것은 아니다. CPU 바운드 작업은 실제로 계산을 수행할 코어가 필요하므로, 스레드 수가 코어 수를 크게 넘어서면 컨텍스트 스위칭 비용만 늘어날 수 있다. 반대로 I/O 바운드 작업은 네트워크나 디스크 응답을 기다리는 시간이 많기 때문에 코어 수보다 많은 스레드를 두는 것이 도움이 될 수 있다.

그래서 실무에서는 매 요청마다 스레드를 새로 만들기보다 스레드 풀을 사용한다. 미리 만들어둔 스레드를 재사용하면 생성 비용을 줄이고, 동시에 처리할 작업 수를 제한할 수 있다.

ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> {
  // 작업 처리
});

스레드마다 독립적인 값이 필요할 때는 ThreadLocal을 사용하기도 한다. 예를 들어 현재 사용자 정보나 요청 컨텍스트처럼 같은 코드 안에서도 스레드별로 다른 값을 가져야 하는 경우다.

ThreadLocal<User> currentUser = new ThreadLocal<>();
currentUser.set(user);
currentUser.get();

프로세스와 스레드를 선택할 때는 안정성과 통신 비용을 같이 봐야 한다. 장애 격리와 보안이 중요하면 멀티 프로세스가 유리하고, 빠른 데이터 공유와 낮은 전환 비용이 중요하면 멀티 스레드가 유리하다. 결국 핵심 질문은 “이 작업들이 서로 얼마나 독립적이어야 하는가”와 “얼마나 자주 데이터를 공유해야 하는가”다.