Redis를 공부하다 보면 이런 설명을 자주 접하게 된다.
Redis는 싱글 스레드 기반이지만 빠르다.
처음 들으면 조금 이상하다.
싱글 스레드라면 요청이 여러 개 들어왔을 때 하나씩 처리해야 할 것 같은데, Redis는 수많은 클라이언트 연결을 동시에 처리할 수 있다.
이걸 이해하려면 **이벤트 루프(Event Loop)**와 I/O Multiplexing이라는 개념을 알아야 한다.
이번 글에서는 이벤트 루프가 왜 필요한지부터 시작해서, I/O Multiplexing과 epoll이 어떤 역할을 하는지, 그리고 이 구조가 Redis 같은 서버에서 어떻게 사용되는지까지 정리해보려고 한다.
1. 가장 단순한 서버를 생각해보자
서버에 클라이언트 요청이 들어왔다고 생각해보자.
가장 단순하게 구현한다면 다음과 같은 구조를 생각할 수 있다.
요청 수신
↓
요청 처리
↓
DB / Network I/O
↓
응답
문제는 I/O에는 대기 시간이 존재한다는 것이다.
예를 들어 네트워크에서 데이터를 기다리고 있다고 해보자.
Client A 요청
↓
처리 시작
↓
네트워크 응답 기다리는 중...
↓
응답
이때 스레드가 I/O가 끝날 때까지 아무것도 하지 못하고 기다린다면 비효율적이다.
CPU 입장에서는 처리할 수 있는 다른 요청이 있는데도 하나의 I/O가 끝나기를 기다리고 있는 셈이다.
그래서 전통적인 서버에서는 여러 요청을 동시에 처리하기 위해 여러 스레드를 사용하는 방식을 사용할 수 있다.
Client A → Thread 1
Client B → Thread 2
Client C → Thread 3
Client D → Thread 4
A가 I/O를 기다리는 동안 Thread 2가 B를 처리할 수 있으니 동시 처리에는 유리하다.
하지만 클라이언트가 수천, 수만 개가 된다면 이야기가 달라진다.
연결마다 스레드를 계속 늘리는 것은 메모리 비용이 발생하고, 스레드 간 Context Switching 비용도 증가한다.
그렇다면 이런 생각을 해볼 수 있다.
하나의 스레드가 여러 연결을 관리하면서, 지금 처리할 수 있는 요청만 골라서 처리하면 안 될까?
여기서 이벤트 기반(Event-driven) 구조가 등장한다.
2. 이벤트 루프란?
이벤트 루프를 아주 단순하게 표현하면 다음과 같다.
while (true) {
처리 가능한 이벤트 확인
이벤트가 있다면 처리
}
즉 하나의 루프가 계속 돌면서 현재 처리할 준비가 된 이벤트를 가져와 처리하는 구조다.
예를 들어 세 개의 클라이언트가 서버에 연결되어 있다고 해보자.
Client A ─┐
Client B ─┼──→ Server
Client C ─┘
현재 상태가 다음과 같다고 가정해보자.
A → 아직 처리할 것 없음
B → 읽을 데이터 있음
C → 아직 기다리는 중
이벤트 루프 입장에서는 굳이 A와 C를 기다릴 필요가 없다.
지금 처리 가능한 B만 처리하면 된다.
그리고 다시 다음 이벤트를 확인한다.
이벤트 확인
↓
B 처리 가능
↓
B 처리
↓
이벤트 확인
↓
A 처리 가능
↓
A 처리
↓
...
이것이 이벤트 루프의 기본적인 아이디어다.
3. 그런데 처리 가능한 이벤트를 어떻게 알까?
여기서 중요한 질문이 하나 생긴다.
서버에 연결된 소켓이 10,000개라고 해보자.
이벤트 루프가 직접 하나씩 확인한다면 어떻게 될까?
socket 1 확인
socket 2 확인
socket 3 확인
...
socket 10000 확인
매번 모든 소켓을 검사하는 것도 상당히 비효율적이다.
그래서 운영체제가 제공하는 기능을 활용한다.
바로 I/O Multiplexing이다.
Unix/Linux 계열에서는 대표적으로 다음과 같은 방식이 있다.
select
poll
epoll
특히 Linux에서는 epoll이 대표적으로 사용된다.
4. I/O Multiplexing이란?
I/O Multiplexing의 핵심 아이디어는 간단하다.
애플리케이션이 모든 소켓을 직접 확인하는 대신 운영체제에게 이렇게 요청하는 것이다.
"내가 관심 있는 소켓들이 여기 있는데, 이 중에서 지금 읽거나 쓸 수 있는 소켓이 생기면 알려줘."
예를 들어 서버에 다음과 같은 연결이 있다고 해보자.
Socket A
Socket B
Socket C
Socket D
Socket E
애플리케이션은 이 소켓들을 epoll에 등록한다.
Socket A ─┐
Socket B ─┤
Socket C ─┼──→ epoll
Socket D ─┤
Socket E ─┘
그리고 이벤트 루프는 운영체제에게 준비된 소켓이 있는지 요청한다.
epoll_wait(...)
만약 B와 D에서 읽을 데이터가 도착했다면 운영체제는 준비된 이벤트를 반환한다.
Ready Events
Socket B
Socket D
그러면 애플리케이션은 B와 D에 해당하는 이벤트만 처리하면 된다.
모든 소켓을 애플리케이션 레벨에서 하나씩 확인할 필요가 없다.
이게 I/O Multiplexing의 핵심이다.
5. 이벤트 루프와 epoll은 같은 걸까?
여기서 이벤트 루프와 epoll을 같은 개념으로 생각하기 쉽다.
하지만 둘은 역할이 다르다.
epoll은 Linux 커널이 제공하는 I/O Multiplexing 인터페이스이고,
이벤트 루프는 준비된 이벤트를 받아 실제 작업을 처리하고 다시 이벤트를 기다리는 애플리케이션의 실행 구조다.
단순화하면 다음과 같다.
┌────────────────────┐
│ epoll │
│ │
│ 어떤 소켓이 │
│ 준비됐는지 알려준다 │
└─────────┬──────────┘
│
Ready Events
│
▼
┌────────────────────┐
│ Event Loop │
│ │
│ 이벤트 처리 │
└─────────┬──────────┘
│
▼
다시 이벤트 대기
즉,
이벤트 기다림
→ 준비된 이벤트 확인
→ 이벤트 처리
→ 다시 이벤트 기다림
→ ...
이 과정을 반복하는 구조가 이벤트 루프다.
6. 그럼 싱글 스레드인데 어떻게 여러 요청을 처리할까?
여기가 가장 중요한 부분이다.
싱글 스레드 이벤트 루프가 요청 여러 개를 실제 같은 순간에 실행하는 것은 아니다.
예를 들어 다음과 같이 처리될 수 있다.
A 처리
↓
B 처리
↓
C 처리
↓
A의 다음 이벤트 처리
↓
D 처리
CPU가 하나의 스레드를 실행하고 있다면 한 순간에는 하나의 작업만 실행된다.
하지만 각 작업을 짧게 처리하고, I/O를 기다려야 하는 작업은 기다리지 않고 다음 작업으로 넘어간다.
그래서 여러 요청이 같은 시간 구간 안에서 함께 진행될 수 있다.
여기서 **동시성(Concurrency)**과 **병렬성(Parallelism)**을 구분해야 한다.
동시성
여러 작업이 같은 시간 구간 동안 번갈아 진행되는 것이다.
시간 ─────────────────────→
A ███ ██
B ███
C ██ █
병렬성
여러 작업이 실제 같은 순간에 실행되는 것이다.
CPU 1 → A █████████
CPU 2 → B █████████
CPU 3 → C █████████
따라서 싱글 스레드 이벤트 루프가 제공하는 핵심은 병렬 실행이 아니라 여러 작업을 효율적으로 진행시키는 동시성이다.
7. 그런데 I/O가 오래 걸리면 이벤트 루프도 기다리는 것 아닌가?
여기서 이벤트 기반 구조의 핵심이 나온다.
예를 들어 A 요청을 처리하다가 네트워크 I/O가 필요하다고 해보자.
Blocking 방식이라면 다음과 같다.
A 처리
↓
I/O 요청
↓
........ 기다림 ........
↓
I/O 완료
↓
A 계속 처리
I/O가 완료될 때까지 현재 스레드가 기다린다.
반면 non-blocking I/O와 이벤트 루프를 사용하는 구조에서는 I/O가 완료될 때까지 이벤트 루프가 그 요청만 붙잡고 있을 필요가 없다.
A 처리
↓
I/O 요청
↓
A 대기
B 처리
↓
C 처리
↓
D 처리
A I/O 준비 완료
↓
A 관련 이벤트 처리
즉,
I/O를 기다리는 동안 다른 일을 한다.
이것이 적은 수의 스레드로 많은 연결을 관리할 수 있는 핵심이다.
8. 그렇다고 이벤트 루프가 무조건 빠른 것은 아니다
이벤트 루프가 하나의 스레드에서 실행된다면 그 스레드에서 오래 걸리는 작업을 수행했을 때 문제가 발생한다.
예를 들어 다음과 같은 이벤트가 있다고 해보자.
A → 1ms
B → 2ms
C → CPU 연산 5초
D → 1ms
E → 1ms
처리는 이렇게 될 수 있다.
A
↓
B
↓
C █████████████████████████████ 5초
↓
D
↓
E
C가 5초 동안 이벤트 루프 스레드를 점유하면 D와 E는 이미 처리 가능한 상태여도 기다려야 한다.
즉 이벤트 루프 자체가 Blocking된다.
따라서 이벤트 루프 기반 시스템에서는 특히 다음과 같은 작업을 조심해야 한다.
- 오래 걸리는 CPU 연산
- Blocking I/O
- 오래 실행되는 Callback
- 대량 데이터를 한 번에 처리하는 작업
이런 작업은 시스템에 따라 Worker Thread나 Thread Pool 등 별도의 실행 영역으로 넘겨 이벤트 루프가 오래 점유되지 않도록 설계한다.
9. Redis가 싱글 스레드인데 빠른 이유와 연결해보자
이제 Redis 이야기가 이해되기 시작한다.
Redis는 전통적으로 명령 실행을 싱글 스레드 중심으로 처리하는 구조로 알려져 있다.
그런데도 동시에 많은 클라이언트 연결을 관리할 수 있다.
그 이유 중 하나가 바로 이벤트 기반 I/O 처리와 I/O Multiplexing이다.
단순화하면 다음과 같이 이해할 수 있다.
Client A ─┐
Client B ─┤
Client C ─┼──→ I/O Multiplexing
Client D ─┤ │
Client E ─┘ ▼
Event Loop
│
▼
Command 실행
클라이언트마다 스레드를 하나씩 생성하는 것이 아니라 여러 연결을 감시하다가 처리할 준비가 된 요청을 이벤트 루프가 가져와 빠르게 실행한다.
그리고 Redis의 많은 명령은 메모리에서 빠르게 수행된다.
GET → 처리
SET → 처리
GET → 처리
INCR → 처리
...
각 명령의 실행 시간이 짧다면 하나의 실행 흐름에서도 많은 요청을 빠르게 처리할 수 있다.
따라서 Redis가 빠른 이유를 단순히
"메모리를 사용해서 빠르다."
라고만 설명하면 조금 부족하다.
메모리 기반 데이터 처리 + 이벤트 기반 구조 + I/O Multiplexing + 빠른 명령 실행
같은 특징들이 함께 작동한다고 보는 것이 더 정확하다.
다만 최신 Redis에서는 네트워크 I/O 등의 일부 작업에 멀티스레딩을 활용할 수 있다.
따라서 **"Redis 프로세스의 모든 작업이 단 하나의 스레드에서만 실행된다"**고 이해하면 안 된다.
10. 이벤트 루프는 언제 유리할까?
이벤트 루프는 특히 I/O 대기가 많은 시스템에서 장점이 크다.
예를 들어,
네트워크 통신
소켓 통신
DB 요청
파일 I/O
같은 작업은 CPU가 실제 계산하는 시간보다 외부 작업을 기다리는 시간이 긴 경우가 많다.
이때 하나의 요청이 I/O를 기다리는 동안 다른 요청을 처리하면 자원을 효율적으로 사용할 수 있다.
반대로 CPU 연산 자체가 오래 걸리는 작업이라면 이야기가 달라진다.
영상 인코딩
대규모 압축
복잡한 암호화 연산
대규모 데이터 계산
이런 작업을 이벤트 루프 스레드에서 오래 실행하면 다른 이벤트 처리가 지연될 수 있다.
따라서 이벤트 루프를 이해할 때 중요한 것은
"싱글 스레드라서 빠르다."
가 아니다.
정확히는,
I/O를 기다리면서 실행 스레드를 계속 점유하지 않고, 처리 가능한 이벤트를 빠르게 처리하기 때문에 많은 연결을 효율적으로 관리할 수 있다.
라고 이해하는 것이 좋다.
11. 정리
이벤트 루프를 한 문장으로 정리하면 다음과 같다.
이벤트 루프는 처리 가능한 이벤트를 받아 실행하고, 다시 다음 이벤트를 기다리는 과정을 반복하는 실행 구조다.
Linux에서는 epoll 같은 I/O Multiplexing 기능을 이용해 여러 소켓 중 어떤 소켓이 처리 가능한 상태인지 효율적으로 확인할 수 있다.
전체 흐름을 단순화하면 다음과 같다.
Client Connections
│
▼
┌──────────────────┐
│ I/O Multiplexing │
│ (epoll) │
└────────┬─────────┘
│
Ready Events
│
▼
┌──────────────────┐
│ Event Loop │
└────────┬─────────┘
│
▼
Event 처리
│
└──────────────→ 다시 반복
결국 이벤트 루프의 핵심은 I/O 대기 때문에 실행 흐름을 불필요하게 붙잡아 두지 않는 것이다.
I/O가 완료되기를 기다리는 동안 다른 준비된 이벤트를 처리하고, 완료 이벤트가 발생하면 다시 해당 작업을 이어간다.
이 구조 덕분에 적은 수의 스레드로도 많은 네트워크 연결을 효율적으로 관리할 수 있다.
그리고 이 개념을 이해하면 자연스럽게 다음 질문으로 이어진다.
그렇다면 epoll은 수만 개의 소켓 중에서 어떻게 준비된 소켓만 효율적으로 찾아낼까?
이 질문을 이해하려면 다음으로 select, poll, epoll의 차이를 살펴볼 필요가 있다.
'CS 정리' 카테고리의 다른 글
| 복합 인덱스, 컬럼 순서는 어떻게 정할까? (0) | 2026.09.05 |
|---|---|
| select 조회 속도가 느리면 뭐부터 확인해야할까? (0) | 2026.09.04 |
| DNS TTL이란? TTL을 300초로 설정하면 정말 5분 뒤에 IP가 바뀔까? (0) | 2026.08.31 |
| Cache-Aside를 실무에서 사용하면 어떤 문제가 생길까? (0) | 2026.08.29 |
| Cache-Aside란? (0) | 2026.08.28 |