동시성 (Concurrency)
목차
- 동시성이란
- 프로세스와 스레드
- 동기화와 경쟁 조건
- 락, 뮤텍스, 세마포어
- Deadlock과 Race Condition
- 안전성과 처리량의 트레이드오프
- 코드 예시
- 면접 포인트
- 참고 자료
동시성이란
동시성(Concurrency) 은 여러 작업이 겹쳐서 진행되도록 구성하는 방식입니다.
동시성과 병렬성은 같은 말이 아닙니다.
- 동시성: 여러 작업을 번갈아 진행하도록 구성하는 모델
- 병렬성: 여러 작업이 실제로 동시에 실행되는 것
백엔드 면접에서 동시성은 다음 주제로 자주 이어집니다.
- 요청을 동시에 처리할 때 안전한가
- 공유 자원 접근 시 문제가 없는가
- 처리량을 높이려다 데이터가 깨지지 않는가
즉, 동시성은 “빠르게 만든다”보다 동시에 처리해도 올바르게 동작하게 만드는 문제에 가깝습니다.
프로세스와 스레드
동시성을 설명할 때 가장 먼저 나오는 기본 구분입니다.
| 항목 | 프로세스 | 스레드 |
|---|---|---|
| 메모리 | 보통 독립 주소 공간 | 같은 프로세스의 메모리 공유 |
| 격리 수준 | 높음 | 낮음 |
| 통신 | IPC 필요 | 메모리 공유로 직접 통신 가능 |
| 장점 | 장애 격리와 안정성 | 생성/전환 비용이 비교적 낮음 |
| 주의점 | 통신 비용이 큼 | 공유 상태 때문에 동기화 필요 |
프로세스가 잘 맞는 경우:
- 강한 격리가 필요함
- 한 작업의 장애가 다른 작업에 덜 번지게 하고 싶음
스레드가 잘 맞는 경우:
- 같은 메모리를 공유하며 빠르게 협업해야 함
- 같은 프로세스 안에서 여러 작업을 병렬/동시 처리해야 함
면접에서는 “스레드가 더 빠르다”처럼 단정하기보다,
공유 메모리의 장점과 동기화 비용이 함께 따라온다고 설명하는 편이 좋습니다.
동기화와 경쟁 조건
동시성에서 가장 흔한 문제는 경쟁 조건(Race Condition) 입니다.
경쟁 조건은 여러 실행 흐름이 같은 데이터를 동시에 다루면서, 실행 순서에 따라 결과가 달라지는 문제입니다.
예를 들어:
- 스레드 A가
count = 10을 읽음 - 스레드 B도
count = 10을 읽음 - 둘 다 1을 더해 11을 씀
원래 기대는 12지만 실제 결과는 11이 될 수 있습니다.
그래서 공유 상태를 다룰 때는 동기화(Synchronization) 가 필요합니다.
- 한 번에 하나만 접근하게 막기
- 읽기와 쓰기 순서를 보장하기
- 원자적 연산을 사용하기
- 가능하면 공유 상태 자체를 줄이기
좋은 답변은 “락을 건다”에서 멈추지 않고,
무엇을 공유하고 있고 왜 경쟁이 생기는지를 먼저 설명하는 답변입니다.
락, 뮤텍스, 세마포어
동시성 제어에서 자주 나오는 기본 도구입니다.
락 (Lock)
공유 자원에 동시에 접근하지 못하게 막는 일반적인 개념입니다.
뮤텍스 (Mutex)
보통 한 번에 하나의 실행 흐름만 임계 구역에 들어가게 하는 락입니다.
- 장점: 이해하기 쉽고 많이 쓰임
- 주의점: 락 범위가 커지면 대기 시간이 길어짐
세마포어 (Semaphore)
한 번에 여러 실행 흐름이 제한된 수만큼 자원에 접근하도록 제어하는 도구입니다.
- 장점: 제한된 리소스 풀을 제어하기 좋음
- 주의점: 잘못 쓰면 상태 추적이 어려움
| 도구 | 주 용도 | 주의점 |
|---|---|---|
| Mutex | 한 번에 한 실행 흐름만 진입 | 경쟁은 막지만 대기가 생김 |
| Semaphore | 정해진 개수만큼 동시 진입 허용 | 관리가 복잡할 수 있음 |
| Atomic 연산 | 간단한 카운터, 플래그 갱신 | 복잡한 상태 전이에는 한계 |
면접에서는 도구 이름을 나열하기보다, 왜 이 자원은 단일 진입이 필요하고 왜 저 자원은 제한된 동시 접근이면 되는지를 설명하는 편이 좋습니다.
Deadlock과 Race Condition
Race Condition
앞서 본 것처럼 실행 순서에 따라 결과가 달라지는 문제입니다.
Deadlock
서로가 가진 락을 기다리며 영원히 진행하지 못하는 상태입니다.
대표 예시는 다음과 같습니다.
- 스레드 A가 락 1을 잡고 락 2를 기다림
- 스레드 B가 락 2를 잡고 락 1을 기다림
- 둘 다 영원히 멈춤
Deadlock을 줄이는 대표 방법은 다음과 같습니다.
- 락 획득 순서를 일관되게 맞추기
- 한 번에 필요한 락을 확보하도록 설계하기
- 타임아웃과 실패 복구 경로를 준비하기
- 공유 자원 수를 줄이기
면접에서 race condition과 deadlock을 구분하지 못하면 기본기가 약해 보이기 쉽습니다.
둘은 모두 동시성 문제지만, 하나는 잘못된 결과, 다른 하나는 진행 불가 상태라는 점이 다릅니다.
안전성과 처리량의 트레이드오프
동시성 설계는 항상 안전성(Safety) 과 처리량(Throughput) 사이의 균형 문제입니다.
- 락을 강하게 걸면 안전성은 좋아지지만 병렬성이 줄어듭니다.
- 락을 줄이면 처리량은 올라가지만 데이터 경쟁 위험이 커집니다.
- 공유 메모리 대신 메시지 전달 모델을 쓰면 안전성이 좋아질 수 있지만 구조가 달라집니다.
예를 들어:
- 계좌 잔액처럼 무결성이 중요하면 보수적으로 동기화하는 편이 낫습니다.
- 통계 집계처럼 약간의 지연이나 eventual consistency가 허용되면 비동기 처리로 throughput을 높일 수 있습니다.
즉, 동시성의 정답은 “락을 많이 쓰는가”가 아니라
어떤 데이터는 강하게 보호하고, 어떤 데이터는 병렬성을 더 우선할지 구분하는 것입니다.
관련 언어별 심화는 Go, JavaScript 문서와 연결해서 보면 좋습니다.
코드 예시
아래 예시는 여러 goroutine이 공유 카운터를 갱신할 때 sync.Mutex로 보호하는 기본 패턴입니다.
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
var count int
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
count++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println(count)
}
이 예시에서 중요한 포인트는 다음입니다.
- 여러 goroutine이 같은
count를 공유합니다. Mutex가 없으면 race condition이 발생할 수 있습니다.- 락은 안전성을 높이지만, 동시에 실행되는 구간을 줄이는 대가가 있습니다.
면접 포인트
- 동시성은 여러 작업을 겹쳐 처리하는 모델이고, 병렬성과는 구분해서 설명해야 합니다.
- 프로세스는 격리에 강하고, 스레드는 공유 메모리 협업에 유리하지만 동기화 비용이 따라옵니다.
- race condition과 deadlock은 다른 문제이며, 각각 잘못된 결과와 진행 불가 상태로 구분해야 합니다.
- Mutex, Semaphore, Atomic은 쓰임새가 다르므로 자원 특성과 함께 설명하는 편이 좋습니다.
- 좋은 답변은 안전성과 throughput 사이의 트레이드오프를 같이 설명합니다.