IocpObject: 완료를 받는 주체
한 줄 정의
IocpObject는 IOCP에 등록되어 완료 이벤트를 Dispatch 받는 엔진 객체의 공통 인터페이스입니다.
핵심 인터페이스
class IocpObject : public std::enable_shared_from_this<IocpObject>
{
public:
virtual ~IocpObject() = default;
virtual HANDLE GetHandle() = 0;
virtual void Dispatch(class IocpEvent* iocpEvent, int32 numOfBytes = 0) = 0;
};
책임 경계
| 함수 | 책임 |
|---|
GetHandle() | IOCP 등록 대상 핸들 반환(소켓 등) |
Dispatch(...) | 완료 이벤트를 타입별 처리 함수로 분기 |
Listener, Session 같은 구체 타입은 이 인터페이스를 구현해 "자기 이벤트"만 처리합니다.
IocpEvent: 요청 1건의 컨텍스트
핵심 개념
- I/O 요청 1건(Recv/Send/Accept)마다 완료 시 되돌아올 컨텍스트가 필요합니다.
- 이 컨텍스트가
IocpEvent이며, OVERLAPPED는 반드시 offset 0에 있어야 합니다.
권장 구조
enum class EventType { Accept, Connect, Recv, Send, Disconnect };
struct IocpEvent
{
OVERLAPPED overlapped{};
EventType eventType = EventType::Recv;
IocpObjectRef owner;
};
struct RecvEvent : IocpEvent
{
WSABUF wsaBuf{};
uint8 buffer[8192]{};
};
중요한 규칙
- Outstanding I/O 1개당 Event 1개를 사용해야 안전합니다.
- 동일 Event를 동시에 여러 I/O에 재사용하면 완료 결과가 섞일 수 있습니다.
식별자 두 축: Completion Key와 OVERLAPPED*
GQCS에서 받는 값
| 값 | 의미 | 주용도 |
|---|
Completion Key | 어느 객체의 완료인지 | Session/Listener 식별 |
OVERLAPPED* | 어떤 작업의 완료인지 | Recv/Send/Accept 식별 |
owner 기반 수명 보장
- Event 안에
owner(shared_ptr)를 넣으면 완료 시점까지 객체 생존을 보장하기 쉽습니다.
- Completion Key는 식별용으로 유지하고, 실제 수명 보장은
owner로 가져가는 하이브리드 전략이 실무에서 안전합니다.
복원 예시
OVERLAPPED* ov = ;
auto* event = reinterpret_cast<IocpEvent*>(ov);
IocpObjectRef owner = event->owner;
if (!owner)
return;
owner->Dispatch(event, static_cast<int32>(numOfBytes));
캐스팅/초기화 안전 규칙
캐스팅 안전성 보강
static_assert(offsetof(IocpEvent, overlapped) == 0, "OVERLAPPED must be first");
inline IocpEvent* ToIocpEvent(OVERLAPPED* ov)
{
return reinterpret_cast<IocpEvent*>(ov);
}
재사용 시 초기화
void InitEvent(IocpEvent& e, EventType type, IocpObjectRef owner)
{
::ZeroMemory(&e.overlapped, sizeof(e.overlapped));
e.eventType = type;
e.owner = std::move(owner);
}
흔한 초기화 실수
shared_ptr, vector, string이 들어간 구조체 전체를 memset(this, 0, sizeof(*this)) 하면 UB입니다.
- 반드시
OVERLAPPED 필드만 ZeroMemory로 초기화하세요.
상속 vs 포함(composition) 선택
| 방식 | 예시 | 장점 | 주의 |
|---|
| 상속 | struct Evt : OVERLAPPED { ... }; | 캐스팅 코드가 짧음 | 가상 함수/가상 상속 금지, 레이아웃 주의 |
| 포함 | struct Evt { OVERLAPPED ov; ... }; | 의도가 명확, 유지보수 쉬움 | ov를 첫 멤버로 고정 필요 |
실무에서는 가독성과 실수 방지를 위해 포함 방식을 선호하는 팀이 많습니다.
자주 터지는 버그와 대응
| 버그 | 증상 | 대응 |
|---|
| Event 재사용 충돌 | 잘못된 이벤트 타입/버퍼 처리 | I/O 1건당 Event 1개 정책 |
| owner 미설정 | Completion Key 댕글링 크래시 | 등록 전에 owner 설정 강제 |
| eventType 누락 | Dispatch 오분기 | InitEvent 단일 진입점 사용 |
| OVERLAPPED 미초기화 | 예측 불가 실패 코드 | 등록 직전 ZeroMemory(&overlapped, ...) |
강의 시 유의사항
강조 포인트
- Object는 완료를 받는 주체, Event는 완료를 식별하는 티켓입니다.
- "누구(Key) + 무엇(Overlapped)" 두 축으로 생각하면 IOCP 디버깅이 쉬워집니다.
- 레이아웃 규칙(offset 0)과 수명 규칙(owner)은 문법보다 더 중요합니다.
자주 하는 오해
| 오해 | 바로잡기 |
|---|
| Completion Key 하나만 있으면 충분하다 | 작업 타입 구분은 Event/Overlapped가 담당한다 |
| Event는 아무 때나 재사용 가능하다 | 이전 I/O 완료 전 재사용하면 충돌한다 |
| 전체 memset이 가장 안전하다 | non-trivial 멤버가 있으면 UB 위험이 있다 |
체크 질문 (스스로 답해보기)
- 왜
OVERLAPPED가 반드시 첫 멤버여야 하는가?
- Completion Key와 Event의 역할을 분리하면 디버깅이 왜 쉬워지는가?
- Event에
owner를 넣지 않으면 어떤 종료 레이스가 발생할 수 있는가?