이 글에서 얻는 것
- 프로세스/스레드/코루틴(가상 스레드 포함)의 차이를 격리·메모리·스케줄링 관점으로 설명할 수 있습니다.
- “스레드가 많으면 빨라진다”가 왜 깨지는지(컨텍스트 스위칭/경합/캐시 미스) 이해합니다.
- 뮤텍스/세마포어/조건변수의 역할을 임계구역·대기·깨우기로 구분할 수 있습니다.
- 데드락의 4가지 조건을 알고, 코드/설계에서 회피하는 실전 패턴(락 순서, 타임아웃, 분리)을 익힙니다.
백엔드에서 OS를 왜 알아야 할까
애플리케이션(자바/스프링)은 결국 OS 위에서 스레드가 실행되고, 네트워크/디스크 같은 리소스를 경쟁합니다. 그래서 성능/장애의 원인이 “코드 한 줄”이 아니라 스케줄링/락/IO 대기에서 나오는 경우가 많습니다.
1) 프로세스 vs 스레드: 격리와 공유의 trade-off
프로세스(Process)
- 주소 공간(메모리)을 분리해서 격리가 강합니다.
- 대신 프로세스 간 통신(IPC)이 필요하고, 컨텍스트 전환 비용이 상대적으로 큽니다.
스레드(Thread)
- 같은 프로세스의 주소 공간을 공유해서 공유 메모리 접근이 쉽습니다.
- 대신 동기화가 필요하고, 경쟁/데드락/가시성 같은 문제가 생깁니다.
현업에서의 감각:
- “안전한 격리/롤링 리스타트”를 우선하면 멀티 프로세스/컨테이너가 유리합니다.
- “같은 메모리에서 빠르게 협업”해야 하면 멀티 스레드가 유리하지만, 동시성 비용을 지불합니다.
2) 컨텍스트 스위칭: ‘스레드가 많으면 느려지는’ 핵심 이유
CPU는 한 코어에서 한 순간에 하나의 실행 흐름만 실제로 수행합니다. 스레드가 늘면 OS는 스레드를 번갈아 실행시키는데, 이때 레지스터/스택/스케줄링 정보 등을 저장/복구하는 비용이 발생합니다.
컨텍스트 스위칭이 커질 때 자주 보이는 증상:
- CPU 사용률은 높은데 처리량이 안 오른다(= “돌긴 도는데 결과가 없다”)
- 락 경합이 많아지고, 스레드가 RUNNABLE/WAITING 사이를 반복한다
- 캐시 미스가 증가해 같은 코드가 더 느려진다(핫 데이터가 코어 캐시에 못 남음)
3) 스케줄링: CPU 바운드 vs IO 바운드를 분리해야 하는 이유
스케줄러는 “누가 다음에 CPU를 쓸지”를 정합니다. 중요한 건 알고리즘 이름(FCFS/SJF/RR) 암기보다, 내 작업이 어떤 성격인지(CPU 바운드/IO 바운드/락 대기) 파악하는 것입니다.
- CPU 바운드: 계산이 많아 CPU를 계속 사용합니다. → 코어 수 이상으로 스레드를 늘려도 이득이 제한적입니다.
- IO 바운드: 네트워크/디스크 대기가 큽니다. → 대기 동안 다른 작업이 CPU를 쓰게 설계하는 게 핵심입니다.
백엔드에서 흔한 실수:
- CPU 바운드 작업과 블로킹 IO를 같은 스레드 풀에 섞어서, IO 대기 때문에 CPU 작업까지 밀리는 상황
- 풀을 “크게” 잡아 놓고 해결했다고 생각하지만, 실제로는 컨텍스트 스위칭/락 경합으로 더 느려지는 상황
4) 동기화 기초: Mutex vs Semaphore vs Condition
동기화는 크게 두 문제를 다룹니다.
- 임계구역 보호(동시에 들어오면 깨짐)
- 대기/깨우기(조건이 만족될 때까지 기다리기)
Mutex(뮤텍스)
- “한 번에 한 명만” 들어가게 하는 잠금입니다.
- 자바의
synchronized/ReentrantLock은 개념적으로 뮤텍스에 가깝습니다.
Semaphore(세마포어)
- “동시에 N명까지” 허용하는 카운팅 잠금입니다.
- DB 커넥션 풀 같은 “동시 사용량 제한”에 직관적으로 대응합니다.
Condition(조건변수)
- “조건이 만족될 때까지 잠들어 있다가 깨우기”에 씁니다.
- 자바에서는
Object.wait/notify,Condition.await/signal로 만납니다.
5) 데드락: 4가지 조건과 실전 회피법
데드락은 아래 4가지 조건이 동시에 만족하면 발생할 수 있습니다.
- 상호배제(Mutual exclusion)
- 점유·대기(Hold and wait)
- 비선점(No preemption)
- 환형대기(Circular wait)
현업에서 자주 쓰는 회피 패턴:
- 락 순서 고정: 여러 락을 잡아야 하면 “항상 A → B 순서”처럼 규칙을 강제합니다.
- 타임아웃/tryLock: 무한 대기를 피하고, 실패 시 롤백/재시도 전략을 세웁니다.
- 락 범위 축소: 락 안에서 IO/외부 호출을 하지 않습니다(대기 시간이 폭증합니다).
- 리소스 분리: 서로 독립인 자원을 같은 락으로 묶지 않습니다(경합과 데드락 모두 줄어듭니다).
실무 적용: 스프링/자바에서 바로 연결되는 포인트
- 스레드 풀 크기는 “감”이 아니라 작업 성격(CPU/IO/락) 으로 잡습니다.
- 블로킹 IO가 많은 작업은 전용 풀로 분리하거나, 가능하면 논블로킹/비동기로 바꿉니다.
- 장애가 났을 때는 “추측” 대신 스레드 상태/스택을 먼저 봅니다. (BLOCKED/WAITING/RUNNABLE)
연습(추천)
- 일부러 데드락을 재현하는 코드를 만들고(락 2개, 순서 뒤집기) 스레드 덤프에서 어떻게 보이는지 확인해보기
- CPU 바운드 작업을 스레드 수 1→2→4→8로 늘려가며 처리량이 어디서 꺾이는지 관찰해보기
- “대기”와 “경합”을 구분하기: IO 대기(네트워크 호출) vs 락 대기(공유 자원) 각각에서 지표/스레드 상태가 어떻게 다른지 적어보기
💬 댓글