1. 멀티스레딩 vs 병렬 처리
멀티스레딩: 많지만, 반드시 동시에일 필요는 없음
멀티스레딩은 프로그램 안에 여러 실행 스레드가 있는 상태를 말합니다. 각 스레드는 독립적인 실행 라인과 같습니다. 하나는 계산을 하고, 다른 하나는 사용자 입력을 기다리고, 세 번째는 데이터를 파일에 저장합니다. Java에서는 Thread 클래스로 스레드를 만들거나 Runnable 인터페이스를 구현하거나, ExecutorService 같은 고수준 도구를 사용할 수 있습니다(이에 대해서는 다음 강의에서).
하지만! 멀티스레딩이 있다고 해서 작업이 실제로 동시에 수행된다는 보장은 없습니다. 이는 프로세서에 코어가 몇 개 있는지에 달려 있습니다. 코어가 하나뿐이라면 스레드들은 서로 빠르게 “전환”될 뿐입니다. 사람 눈에는 동시에 일어나는 것처럼 보일 정도로 빠르지만, 실제로 프로세서는 매 순간 하나의 스레드만 실행하고 나머지는 차례를 기다립니다.
병렬 처리: 작업이 실제로 동시에 진행될 때
병렬 처리는 코드가 여러 프로세서 코어에서 실제로 동시에 실행되는 것을 말합니다. 4, 8, 16 코어가 있는 최신 컴퓨터라면, 큰 작업을 독립적인 부분으로 나눠 코어에 분배함으로써 실제로 처리를 가속할 수 있습니다.
비유하자면, 멀티스레딩은 한 명의 요리사가 보르시, 커틀릿, 샐러드 준비 사이를 빠르게 오가는 상황이고, 병렬 처리는 여러 명의 요리사가 동시에 각자 맡은 요리를 담당하는 상황입니다.
실무에서의 차이는?
멀티스레딩은 편의성과 응답성에 관한 것입니다. 여러 스레드를 사용해 프로그램이 “멈추지” 않도록 합니다. 한 스레드는 네트워크를 기다리고, 다른 스레드는 UI를 그리며, 또 다른 스레드는 계산을 합니다. 체감상 동시에 동작하지만 실제로 반드시 동시에일 필요는 없습니다.
병렬 처리는 속도에 관한 것입니다. 여러 프로세서 코어가 작업의 다른 부분을 동시에 수행해 결과를 더 빨리 얻습니다.
다시 말해: 멀티스레딩은 작업을 잘 조직하도록 돕고, 병렬 처리는 그 작업을 빠르게 수행하도록 돕습니다.
중요:
멀티스레딩은 서로 독립적으로 처리할 수 있는 일이 있을 때 언제나 유용합니다.
병렬 처리는 실제로 일을 코어 간에 분산해 계산을 가속하고 싶을 때 필요합니다.
예시: 큰 배열 처리
천만 개의 숫자로 이루어진 배열이 있고, 모든 요소의 합을 구한다고 가정해 봅시다.
순차적으로:
하나의 스레드가 배열 전체를 순회하며 합을 계산합니다. 단순하고 안정적이지만 오래 걸립니다.
멀티스레딩(하지만 1코어):
배열을 4개 부분으로 나누고 4개의 스레드를 만들어 각자 자신의 부분을 계산합니다. 하지만 코어가 하나뿐이라면 스레드들은 교대로 실행될 뿐이어서 가속 효과가 없습니다. 스레드 전환에 드는 오버헤드 때문에 오히려 느려질 수도 있습니다.
병렬(여러 코어):
배열을 4개 부분으로 나누고 4개의 스레드를 실행하면, 각 스레드가 실제로 자신의 코어에서 동작합니다. 최종 합은 4개의 부분을 합쳐 얻습니다. 특히 대용량 데이터에서 정말 더 빠릅니다.
반면, 배열을 순차적으로 처리하는 코드는 매우 간단하며, 이런 종류의 프로그램은 여러 번 작성해 보셨을 겁니다:
// 예: 배열의 순차 처리
int[] arr = new int[10_000_000];
// ... 배열 채우기 ...
long sum = 0;
for (int x : arr) {
sum += x;
}
System.out.println(sum);
멀티스레딩 버전과 병렬 버전은 조금 더 복잡합니다. 최신 도구를 사용해 다음 강의에서 자세히 살펴보겠습니다.
2. 왜 병렬 처리가 필요한가
현대 프로세서는 오래전에 단일 코어의 한계를 넘어섰습니다. 여러분의 스마트폰도 대개 코어가 최소 네 개이며, 데스크톱과 서버는 여덟, 열여섯, 서른둘 이상일 수 있습니다. 애플리케이션이 이 모든 코어를 활용할 줄 안다면, 성능은 몇 배나 빨라질 수 있습니다.
예전에는 프로세서 성능이 클록 주파수 증가로 향상되었습니다. 2000년대 중반까지는 이것이 유효했지만, 주파수 증가는 물리적 한계에 부딪혔습니다. 그때부터 다중 프로세서·다중 코어 시대가 시작되었습니다. 이제는 작업을 코어 간에 효율적으로 분배할 수 있는 프로그램이 승리합니다.
어디에서 병렬 처리가 실제로 가속되는가?
- 대용량 데이터 처리: 로그 분석, 통계, 집계 등 독립적인 조각으로 나눌 수 있는 모든 것.
- 렌더링, 이미지·비디오 처리: 각 픽셀이나 조각을 따로 처리 가능.
- 과학 계산, 모델링: 수학적 문제, 시뮬레이션, 모델 학습.
- 서버 애플리케이션: 다수 클라이언트를 동시에 처리.
- 리액티브 애플리케이션: 기본 스레드를 막지 않으면서 다수의 이벤트에 빠르게 반응해야 할 때.
언제 병렬 처리가 도움이 되지 않는가?
- 작업이 아주 작다면 병렬화 오버헤드가 이득보다 클 수 있습니다.
- 작업을 독립적인 부분으로 나눌 수 없을 때(예: 각 단계가 이전 단계 결과에 의존).
- 공유 자원이 많아(예: 동일한 파일) 스레드들이 서로 간섭할 때.
3. 병렬 처리에 자주 배분되는 작업들
어떤 작업이 자주 코어에 “분산”되는지 살펴봅시다.
대량 계산
- 합계, 최댓값/최솟값, 큰 배열에 대한 통계 계산.
- 예: 백만 개 센서의 평균 온도 계산.
컬렉션 처리
- 대규모 리스트의 필터링, 정렬, 변환(예: 전자상거래 주문 처리).
- 예: 10,000루블보다 비싼 주문만 골라 날짜순으로 정렬.
렌더링과 그래픽 처리
- 이미지의 모든 픽셀에 필터 적용(예: 흑백으로 만들기).
- 각 픽셀을 독립적으로 처리 가능 — 병렬 처리의 이상적인 사례.
데이터 분석, big data
- MapReduce, 집계, 방대한 데이터에 대한 통계 계산.
- 예: 1년치 로그를 처리해 이상 징후 찾기.
예시: 병렬 합계 계산
백만 개의 숫자로 된 배열이 있다고 합시다. 이를 4개의 부분으로 나누고 각 부분의 합을 별도의 스레드에서 계산한 뒤, 결과를 더할 수 있습니다.
4. 병렬 처리의 문제와 도전 과제
디버깅의 어려움
코드가 여러 스레드에서 동작하면, 스레드들이 “특정 방식으로 엇갈릴” 때만 나타나는 버그가 생길 수 있습니다. 어떤 오류는 1000번 중 한 번만 나타나기도 하며, 이런 버그를 잡기는 매우 어렵습니다.
데이터 레이스 (race condition)
여러 스레드가 동시에 같은 변수나 객체를 변경하면 결과가 잘못될 수 있습니다. 예를 들어 두 스레드가 동시에 카운터를 증가시키면 최종 값이 기대보다 작아질 수 있습니다.
동기화
레이스를 피하려면 공유 데이터에 대한 접근을 동기화해야 합니다. 이를 위해 synchronized 키워드, 락, 원자적 변수 등의 도구를 사용합니다. 이는 코드를 복잡하게 만들고 다른 문제(예: deadlock — 스레드 간 상호 교착 상태)를 초래할 수 있습니다.
부하 분산
작업을 4개로 나눴는데 그중 하나가 다른 것보다 훨씬 무겁다면, 세 스레드는 이미 끝나 놀고 있는데 네 번째 스레드는 여전히 일하고 있을 수 있습니다. 결국 가속 효과가 사라집니다.
오버헤드
스레드 생성, 전환, 동기화에는 시간이 듭니다. 작업이 작다면 병렬 처리가 오히려 실행을 늦춥니다.
표: 접근 방식 비교
| 접근 방식 | 빠른 경우 | 느려지는 경우 | 적용 예 |
|---|---|---|---|
| 순차(1 스레드) | 작은 작업, 단순한 로직 | 대용량 데이터 | 10줄 처리 |
| 멀티스레딩(1 코어에서) | 비동기 작업(대기 IO) | 1코어에서 CPU-bound(연산이 병목) 작업 | 파일 동시 다운로드 |
| 병렬 처리(다중 코어) | 크고, 서로 독립적인 작업 | 작은 작업, 강한 결합 | 대형 배열 처리 |
시각화: 이렇게 보입니다
// 순차 처리(1 스레드)
[작업 1][작업 2][작업 3][작업 4]
// 단일 코어에서의 멀티스레딩(컨텍스트 스위칭)
[작업 1] [작업 2] [작업 3] [작업 4]
(실제로는 한 번에 하나만 실행되고 나머지는 대기)
// 4코어에서의 병렬 처리
[작업 1] [작업 2] [작업 3] [작업 4]
(모두 동시에 실행됨)
5. 병렬 처리 시 흔한 실수
오류 1: 무조건 병렬화하기. 많은 초보자는 “스레드가 많을수록 더 빠르다!”고 생각합니다. 사실은 그렇지 않습니다. 작업이 적거나 너무 단순하면 이득이 없고, 때로는 프로그램이 더 느려지기도 합니다.
오류 2: 동기화 무시. 여러 스레드가 동일한 데이터를 동기화 없이 다루면 데이터 레이스가 발생하고, 로직이 깨지며, 잡기 어려운 버그가 생깁니다.
오류 3: 병렬 처리를 위한 병렬 처리. 병렬 처리는 그 자체가 목적이 아닙니다. 독립적으로 효과적으로 나눌 수 있는 실제 문제가 있을 때 필요합니다.
오류 4: 문제 특성을 고려하지 않음. 어떤 문제는 아예 병렬화할 수 없습니다(예: N+1단계가 N단계 결과에 의존). 이런 경우 병렬 처리는 이점을 주지 않습니다.
오류 5: 오버헤드를 고려하지 않음. 스레드 생성, 전환, 결과 수집 등은 모두 시간이 듭니다. 작은 작업에서는 이 시간이 작업 자체보다 더 클 수 있습니다.
GO TO FULL VERSION