단일 코어 프로세서의 클럭 속도(Clock Speed) 향상이 물리적 한계에 도달하면서, 무어의 법칙(Moore's Law)은 더 이상 유효하지 않게 되었습니다. 현대 소프트웨어 엔지니어링의 핵심 과제는 단순히 코드의 실행 속도를 높이는 것이 아니라, 멀티코어 하드웨어 자원을 얼마나 효율적으로 점유하고 분배하느냐에 달려 있습니다. 대용량 트래픽을 처리하는 백엔드 시스템에서 동시성(Concurrency)과 병렬성(Parallelism)의 개념을 혼동하는 것은 치명적인 아키텍처 결함으로 이어집니다. 스레드 풀(Thread Pool) 고갈, 교착 상태(Deadlock), 그리고 예측 불가능한 레이턴시(Latency) 스파이크는 대부분 이 두 개념의 부적절한 적용에서 기인합니다. 본 글에서는 이 두 개념의 공학적 차이와 트레이드오프를 분석하고, 실무적인 최적화 전략을 다룹니다.
1. 동시성(Concurrency): 논리적 제어권 관리
동시성은 구조(Structure)에 관한 개념입니다. 싱글 코어 환경에서도 여러 작업이 동시에 진행되는 것처럼 보이게 만드는 기법으로, 핵심은 컨텍스트 스위칭(Context Switching)과 시분할(Time Slicing)입니다. 작업 A가 I/O 대기(DB 조회, 네트워크 요청 등) 상태에 진입했을 때, CPU 유휴 시간(Idle Time)을 방지하기 위해 제어권을 즉시 작업 B로 넘기는 방식입니다.
이 모델의 주 목적은 지연 시간(Latency) 은폐와 가용성(Availability) 확보입니다. Java Concurrency Docs를 보면 스레드 생명주기 관리가 동시성의 핵심임을 알 수 있습니다.
Context Switching의 비용(Overhead)
동시성을 구현하기 위해 OS 스케줄러는 실행 중인 프로세스의 상태(레지스터, PC 등)를 PCB(Process Control Block)에 저장하고 다음 프로세스를 로드합니다. 이 과정은 공짜가 아닙니다. 너무 잦은 컨텍스트 스위칭은 오히려 스레싱(Thrashing)을 유발하여 CPU가 실제 작업보다 전환 작업에 더 많은 자원을 소모하게 만듭니다.
2. 병렬성(Parallelism): 물리적 동시 실행
병렬성은 실행(Execution)에 관한 개념입니다. 멀티코어 프로세서나 분산 시스템에서 물리적으로 동시에 여러 계산을 수행하는 것을 의미합니다. 병렬성은 주로 대규모 데이터 처리나 복잡한 연산 작업(CPU-bound)의 처리량(Throughput)을 극대화하는 데 사용됩니다.
암달의 법칙 (Amdahl's Law)과 한계
병렬 처리를 위해 코어 수를 무한정 늘린다고 해서 성능이 선형적으로 증가하지 않습니다. 전체 시스템의 속도 향상은 병렬화가 불가능한 순차 실행 영역(Critical Section)에 의해 제한됩니다.
3. 공유 자원과 동기화 문제 (Synchronization)
멀티스레드 환경에서 가장 큰 엔지니어링 난제는 공유 자원(Shared Resource)에 대한 접근 제어입니다. 두 개 이상의 스레드가 동시에 변수를 수정하려 할 때 발생하는 경쟁 상태(Race Condition)는 데이터 무결성을 파괴합니다.
다음은 Java에서 발생할 수 있는 전형적인 Race Condition 예제와 Atomic 패키지를 이용한 해결책입니다.
public class CounterLogic {
private int count = 0;
// 해결책: AtomicInteger 사용 (CAS 알고리즘 기반)
// private AtomicInteger atomicCount = new AtomicInteger(0);
public void increment() {
// [Critical Section]
// 읽기(Read) -> 수정(Modify) -> 쓰기(Write)가 원자적(Atomic)이지 않음
// 스레드 A가 읽고 수정하는 사이, 스레드 B가 끼어들 수 있음
count++;
}
public int getCount() {
return count;
}
}
이 문제를 해결하기 위해 Mutex, Semaphore, Monitor 등의 Lock 메커니즘을 사용하지만, 이는 성능 저하(Blocking)와 교착 상태(Deadlock)의 위험을 동반합니다. 최근에는 Lock-Free 알고리즘이나 Go의 Channel과 같이 메시지 패싱(Message Passing)을 통한 동기화 방식이 선호되는 추세입니다.
4. 언어별 동시성 모델 비교 및 실무 적용
각 프로그래밍 언어는 동시성을 처리하는 고유의 런타임 모델을 가지고 있습니다. 프로젝트의 성격에 따라 적절한 언어를 선택하는 것이 중요합니다.
| 언어/모델 | 매커니즘 | 특징 및 Trade-off |
|---|---|---|
| Java (Traditional) | Native Thread (1:1 Model) | OS 스레드와 직접 매핑. 컨텍스트 스위칭 비용 높음. 메모리 사용량 큼. |
| Go (Goroutine) | M:N Model (Green Thread) | OS 스레드 하나에 수천 개의 고루틴 멀티플렉싱. 매우 가벼운 스위칭 비용. |
| Python | GIL (Global Interpreter Lock) | 한 시점에 하나의 바이트코드만 실행. 멀티스레드로 CPU 병렬성 달성 불가 (멀티프로세싱 필요). |
| Node.js | Event Loop (Single Thread) | 비동기 I/O에 최적화. CPU-bound 작업 시 이벤트 루프 블로킹 위험. |
결론: 트레이드오프에 기반한 설계
동시성과 병렬성은 상호 배타적인 개념이 아니라 상호 보완적인 도구입니다. 동시성은 프로그램의 논리적 설계를 단순화하고 I/O 효율을 높이며, 병렬성은 하드웨어 자원을 활용하여 물리적 처리 속도를 높입니다. 엔지니어는 단순히 "빠르게"가 아니라, "어떤 자원이 병목인지(CPU vs I/O)"를 파악하고, 암달의 법칙에 의한 한계를 인지한 상태에서 아키텍처를 설계해야 합니다. 무분별한 스레드 생성보다는 리액티브 프로그래밍(Reactive Programming)이나 경량 스레드 모델을 검토하여 시스템의 확장성을 확보하십시오.
Post a Comment