2025. 11. 25. 20:01ㆍ개념 공부
0. 이 챕터가 해결하려는 문제: “왜 굳이 스레드 말고 이벤트?”
- 스레드 기반 동시성은 락 누락/데드락/경쟁조건 등 “정확성”이 어렵습니다.
- 또한 스레드 방식은 “지금 CPU가 뭘 실행할지”를 OS 스케줄러에 맡기므로, 개발자가 스케줄링/처리 순서 제어를 하기 어렵습니다.
- 이벤트 방식은 단일 스레드에서 이벤트를 하나씩 처리하므로, 어떤 이벤트를 다음에 처리할지(=스케줄링)를 앱이 직접 결정할 수 있습니다.
1. 핵심 모델: Event Loop(이벤트 루프)
이벤트 기반 서버의 “뼈대”는 아래처럼 단순합니다. 루프가 이벤트를 기다렸다가, 이벤트 목록을 받아 하나씩 처리(handler)합니다.
// Canonical event loop (pseudocode)
while (1) {
events = getEvents();
for (e in events)
processEvent(e);
}
여기서 중요한 해석
- processEvent(e) 가 실행되는 동안 시스템에서 “실질적으로 진행되는 일”은 이것 하나뿐 → 그래서 다음에 뭘 할지 = 스케줄링입니다.
- 스레드가 많아져도 “동시에 실행되는 것처럼 보이는” 이유는 OS가 번갈아 스케줄링하기 때문인데, 이벤트 방식은 그 스케줄링을 앱이 직접 쥡니다(대신 제약이 큼).
2. 이벤트를 어떻게 “받아오나?”: select() / poll()
이벤트 루프에서 getEvents() 역할을 OS가 도와주는 대표 API가 select() / poll() 입니다. 이들은 “지금 읽을/쓸 준비가 된 FD(소켓 등)가 있나?”를 확인하게 해 줍니다.
select()의 감각
- read ready: 읽을 데이터가 도착(예: inbound 패킷)해서 처리 가능
- write ready: 보내도 되는 상태(예: outbound queue가 꽉 차지 않음)
- timeout: NULL이면 준비될 때까지 블록, 0이면 즉시 리턴(폴링 느낌) 등 다양한 운영이 가능
책의 예시 코드는 “fd_set을 만들고 → select로 준비된 FD를 받고 → 준비된 FD만 처리” 흐름입니다.
// Simple select() usage (simplified from the chapter)
while (1) {
fd_set readFDs;
FD_ZERO(&readFDs);
for (fd = minFD; fd < maxFD; fd++)
FD_SET(fd, &readFDs);
select(maxFD+1, &readFDs, NULL, NULL, NULL);
for (fd = minFD; fd < maxFD; fd++)
if (FD_ISSET(fd, &readFDs))
processFD(fd);
}
3. (중요) Blocking을 하면 왜 망하나?
이벤트 기반 서버의 “규칙”은 딱 하나로 요약됩니다: 이벤트 핸들러 안에서 블로킹 호출을 절대 하면 안 된다.
왜?
- 스레드 기반: 어떤 스레드가 read() 때문에 잠들어도, 다른 스레드가 계속 진행 → I/O와 계산이 자연스럽게 겹침
- 이벤트 기반(단일 루프): 핸들러가 open()/read() 같은 블로킹 I/O를 호출하면, 서버 전체가 멈춤 → 그 동안 다른 이벤트를 처리할 주체가 없음
4. 해결: Asynchronous I/O(AIO)
블로킹 문제를 피하려면, 디스크 같은 느린 I/O는 비동기(Asynchronous)로 요청만 던지고 즉시 돌아와야 합니다.
AIO의 큰 그림
- 요청 정보(aiocb) 채우기: fd, offset, buffer 주소, bytes 등
- aio_read() 호출: 성공하면 “요청만 걸고” 바로 리턴
- 완료 확인: aio_error()로 완료 여부 확인(완료면 0, 아니면 EINPROGRESS)
현실의 고민: 완료 확인을 매번 polling 해야 해?
- 요청이 수십/수백 개면, 매번 다 확인하는 건 고통스럽습니다.
- 그래서 어떤 시스템은 시그널(UNIX signals) 같은 “인터럽트 기반 알림”으로 AIO 완료를 알려줍니다
- 또는 현실적으로 “네트워크는 이벤트로, 디스크는 스레드풀로” 같은 하이브리드 구조도 연구/실무에서 씁니다.
5. 이벤트 방식의 진짜 난점: 상태 관리(Manual Stack Management)
스레드 방식은 “다음에 해야 할 일”에 필요한 상태가 스레드 스택에 자연스럽게 남아 있습니다. 하지만 이벤트 방식은 핸들러가 비동기 I/O를 걸고 리턴해버리면, 다음 이벤트(완료 이벤트)가 왔을 때 쓸 상태를 직접 저장/복원해야 합니다. 이걸 책에서는 manual stack management라고 부릅니다.
스레드 기반은 왜 쉬운가?
예를 들어 “파일 fd에서 읽고 → 소켓 sd로 쓰기”는 스레드에서는 아래처럼 직관적입니다. 읽기가 끝나면 스택에 sd가 그대로 있으니까요.
// Thread-based (concept)
int rc = read(fd, buffer, size);
rc = write(sd, buffer, size);
이벤트 기반은 왜 어려운가?
AIO로 읽기를 “요청만” 하고 돌아왔다는 건, 현재 함수/스택 프레임이 끝난다는 뜻입니다. 나중에 I/O 완료 이벤트가 왔을 때, “어느 소켓(sd)으로 write 해야 하는지” 같은 상태를 어디선가 찾아와야 해요.
해법: Continuation(컨티뉴에이션)
- 핵심 아이디어: “이 작업을 마무리하는 데 필요한 정보를 구조체/테이블에 기록해두고, 완료 이벤트가 오면 그걸 찾아 마저 처리한다.”
- 책의 예: (fd → sd) 매핑을 어딘가(예: 해시테이블)에 저장해 두었다가, 디스크 I/O 완료 시 fd로 continuation을 찾아 sd를 얻고 write 수행
즉, 이벤트 기반 코드는 점점 “A 요청 시작 핸들러”와 “A 요청 완료 핸들러”처럼 로직이 조각조각 찢어지는 경향이 생깁니다. 이게 흔히 말하는 “콜백 지옥 / 상태 머신 지옥”의 근본 원인 중 하나입니다(책에서는 API 의미가 바뀌면 핸들러를 또 찢어야 한다고 경고).
6. “락이 필요 없어져서 좋은데요?” — 장점의 정확한 범위
단일 CPU + 단일 이벤트 루프라면, 한 번에 한 이벤트만 처리하므로 동시에 임계영역에 들어올 주체가 없어서 락이 불필요해집니다.
하지만 이 말은 “이벤트 방식이면 무조건 락이 필요 없다”가 아닙니다: 멀티코어에서 성능을 뽑으려고 이벤트 핸들러를 병렬로 돌리려는 순간, 다시 동기화(락/임계구역)가 필요해진다고 책에서 분명히 말합니다.
7. 이벤트 기반이 여전히 어려운 지점(실무 인사이트 포인트)
- 멀티코어: 여러 CPU를 쓰려 하면 결국 병렬성 → 동기화 이슈 귀환
- 페이징/페이지 폴트: 핸들러가 page fault를 일으키면 “명시적으로 블로킹 호출을 안 했는데도” 멈출 수 있음
- API 의미 변화 리스크: 어떤 함수가 나중에 blocking 성격으로 바뀌면, 해당 핸들러를 다시 쪼개야 함(유지보수 난이도)
- 비동기 I/O 통합 난이도: 네트워크는 select, 디스크는 AIO 등 인터페이스가 “깔끔하게 하나로” 합쳐지지 않는 경우가 많음
9) 미니 퀴즈(자기 점검)
- 이벤트 기반 서버에서 “락이 덜 필요해지는” 이유를 한 문장으로 설명해보세요.
정답 보기
단일 이벤트 루프에서 한 번에 하나의 이벤트 핸들러만 실행되므로, 같은 공유 상태를 동시에 건드릴 경쟁자가 사라져 기본적으로 락이 필요 없어집니다.
단, 멀티코어 활용을 위해 핸들러를 병렬화하면 다시 동기화 문제가 돌아옵니다. - 이벤트 기반 서버에서 “블로킹 호출 금지”가 왜 치명적인 규칙인가요?
정답 보기
이벤트 기반은 대개 단일 루프/단일 실행 흐름이어서, 핸들러가 블로킹 I/O를 호출하면 서버 전체가 멈춰 다른 이벤트를 처리할 주체가 사라집니다. - select()가 서버에 주는 “이벤트”의 정체는 무엇인가요?
정답 보기
“읽기/쓰기/에러 처리 준비가 된 파일 디스크립터 집합”입니다. 준비된 FD만 골라 핸들러를 돌리게 해 줍니다. - AIO에서 aio_read()와 aio_error()는 각각 왜 필요한가요?
정답 보기
aio_read()는 디스크 I/O를 “요청만” 걸고 즉시 돌아오기 위해 필요하고, aio_error()는 요청이 끝났는지(완료/진행중)를 확인하기 위해 필요합니다. - “manual stack management”가 정확히 무엇을 의미하나요?
정답 보기
비동기 I/O를 걸고 핸들러가 끝나면 스택 프레임이 사라지므로, 나중에 완료 이벤트가 왔을 때 필요한 상태(예: 어느 sd로 write할지)를 직접 저장/복원해야 하는 작업을 말합니다.
10. 한 줄 요약
- 이벤트 기반 동시성은 이벤트 루프가 select/poll 등으로 “준비된 I/O 이벤트”를 받고, 핸들러를 순차 실행하며 스케줄링 제어를 얻는 방식이다.
- 대신 블로킹 호출 금지, 비동기 I/O(AIO), continuation 기반 상태 관리 같은 복잡성이 따라온다.
'개념 공부' 카테고리의 다른 글
| 운영체제 아주 쉬운 세가지 이야기 13주차(하드 디스크 드라이브) (0) | 2025.12.02 |
|---|---|
| 운영체제 아주 쉬운 세가지 이야기 13주차(I/O 장치) (1) | 2025.12.02 |
| 운영체제 아주 쉬운 세가지 이야기 12주차(병행성 관련 버그) (0) | 2025.11.25 |
| 운영체제 아주 쉬운 세가지 이야기 11주차(세마 포어) (0) | 2025.11.18 |
| 운영체제 아주 쉬운 세가지 이야기 11주차(컨디션 변수) (0) | 2025.11.18 |