이번 주는 소켓 프로그래밍을 중심으로, 네트워크가 실제로 어떻게 동작하는지에 대해 공부하고 간단한 실습까지 진행했다.
사실 이쪽은 그동안 “대충 이런 느낌이겠지” 정도로만 알고 있던 영역이었는데, 이번 기회에 제대로 파보게 됐다. 평소에도 한 번은 깊게 이해해보고 싶었던 주제라 그런지 생각보다 재미있게 몰입해서 공부할 수 있었다.
이번 주는 특히 접근 방식을 조금 바꿔보려고 했다.
기존에는 과제 요구사항이나 책 내용 자체를 이해하는 데 집중했다면, 이번에는 “이게 실제로 어디에 쓰이는지”, “실무에서는 어떻게 쓰는지”까지 같이 보려고 했다.
지난주 이동석 코치님의 피드백을 듣고 나서, 내가 과제 자체에만 너무 매몰되어 있었다는 걸 느꼈기 때문이다.
그래서 공부하다가 생긴 궁금증들은 그냥 넘기지 않고 따로 찾아보면서 정리했고, 수요 코딩회에서 겪었던 이슈들과 함께 이번 WIL에 기록해두려고 한다.
핵심 역량 평가
| 역량 | 달성도 | 목표 |
| 문제해결 | 0% | proxy.c의 part2(동시성 문제)까지 구현한다. |
| 시간 부족으로 인해 proxy.c 코드는 직접 짜지 못했다.. | ||
| 설계 | 50% | 요청 파싱, 응답 생성, 서버 연결, 캐시 기능을 분리하여 이후 병렬 처리 및 캐시 확장이 가능하도록 구조를 설계한다. |
| 직접 코드를 구현하지는 못했지만, 기존 echo.c/tiny.c의 구조적 문제를 파악하고 실제로는 어떤 방법들을 사용하는지 찾아보았다. | ||
| 구현 | 0% | socket, HTTP 프로토콜을 기반으로 tiny 웹서버와 proxy 서버를 직접 구현하고, 요청/응답 흐름이 실제로 동작하도록 만든다. |
| 직접 코드를 구현하지는 못했다. | ||
| 품질 | 100% | 브라우저, curl 등을 활용해 다양한 요청을 테스트하고, 정상/비정상 요청(404, 잘못된 헤더 등)에 대해 올바르게 동작하는지 검증한다. |
| 브라우저/curl 등을 활용해 적절하게 테스트를 진행하였다. | ||
| 유지보수 | 80% | 함수 단위로 역할을 분리하고, 주요 로직에 대해 간단한 주석을 작성하여 코드의 흐름을 쉽게 이해할 수 있도록 한다. |
| 직접 짠 코드는 아니지만, tiny.c의 함수들에 대해 line by line로 학습하며 주석을 달고 이해하였다. | ||
| 협업 | 80% | 어려웠던 개념을 혼자 알고 넘어가는 것이 아니라, 팀원들과 공유한다. |
| 스스로 추가학습한 부분을 노션에 정리하여 팀원들과 공유하였다. | ||
| 태도 | 50% | 네트워크 및 시스템 레벨의 오류가 발생하더라도 로그와 흐름을 기반으로 끝까지 원인을 추적하며 해결한다. |
| 수요코딩회에서 b+ 트리가 오염되는 원인을 찾으려 많은 노력을 했지만 결국 정확한 원인을 파악하는 것은 실패했다. | ||
| AI 활용 | 50% | tiny 웹서버 및 HTTP 구조 이해, 디버깅 과정에서 AI를 활용하되, 핵심 로직은 직접 구현하고 검증한다. |
| 핵심 로직을 직접 구현하지는 못했지만 소켓 프로그래밍 구조를 확실히 이해하기 위해 AI를 적극적으로 활용하였다. | ||
| 학습 민첩성 | 50% | 네트워크 개념을 깊게 파고들기보다, AI를 활용해 핵심 이론을 빠르게 학습한 뒤 소켓 프로그래밍 구현 단계에 즉시 적용한다. |
| Deep dive를 하지 않기로 맹세했지만 CSAPP를 읽으며 나도 모르게 네트워크 이론에 파고들었던 날이 있었다. | ||
계획한 내용대로 흘러가지 않은 주차였던 것 같다. 🥲
CSAPP 내용은 간단히 이해하며 넘어가고, tiny.c의 핵심 로직을 직접 설계해보는 것도 좋았을 것 같다.
Weekly Questions
이번주에 네트워크 개념을 학습하며 궁금했던 2개의 질문에 대해 찾아본 내용을 정리해두려고 한다.
1. 서버가 항상 accept()만 waiting 하는 구조가 정상적일까?
책에서 소개하는 tiny.c의 main 함수를 보다가 이상한 점을 발견했다.
while (1)
{
clientlen = sizeof(clientaddr);
// 요청이 들어올 때까지 blocking!
connfd = Accept(listenfd, (SA *)&clientaddr,
&clientlen);
// IP 주소 + 포트 → (호스트 이름, 서비스/포트 문자열)로 변환
Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE,
0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
doit(connfd);
Close(connfd);
}
이 구조에서는
1. accept()로 연결 하나를 받음
2. 이 연결을 doit()에서 끝까지 처리함
3. 처리 끝나기 전까지는 다른 연결을 안 봄
-> 연결 하나를 끝까지 처리한 뒤 다음 연결을 받는다.
이렇게 되면 당연히 비효율적인 상황이 발생할 수 밖에 없다.
🔥 문제 상황
클라이언트 A → 처리 5초
클라이언트 B → 처리 1초
이런 상황에서 A의 요청을 먼저 처리하게 되면, B는 1초면 끝나는 일을 6초나 기다려야 되는 문제가 생긴다.
이를 해결하기 위한 방안으로 멀티 프로세스/멀티 스레드를 통해 작업 공간 자체를 늘려버리는 방법이 있지만, 이는 근본적인 문제를 해결해주지 못할 것이라고 생각했다.
왜냐하면 이 방식은 결국 blocking I/O 구조를 그대로 둔 채, 처리 흐름만 병렬로 늘리는 접근이기 때문이다. 즉, 연결 하나를 끝까지 처리하는 방식 자체는 변하지 않는다.
그래서 이번에는 접근 방식을 바꿔, 여러 연결을 동시에 감시하고 준비된 작업만 처리하는 이벤트 기반 모델에 대해 살펴보게 되었고, 그 핵심이 바로 epoll_wait이다.
epoll_wait
epoll은 여러 파일 디스크립터(fd)를 한 번에 감시하는 메커니즘이다.
기존에는 accept() 하나만 바라보고 있었다면, 이제는 listen 소켓과 이미 연결된 모든 소켓을 함께 감시한다.
while (1) {
int n = epoll_wait(epfd, events, MAX, -1);
// epfd : 감시 대상 목록을 들고 있는 객체(감시 중인 fd 목록)
// events : 결과를 담아줄 배열.
// MAX : 한 번에 받아올 최대 이벤트 수
// timeout(-1) : 얼마나 기다릴지(-1은 이벤트 생길 때까지 무한 대기)
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
if (fd == listenfd) {
// 새로운 연결
connfd = accept(listenfd, ...);
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, ...);
} else {
// 기존 연결에서 이벤트 발생
read(fd, ...);
write(fd, ...);
}
}
}
기존 구조처럼 하나의 연결을 끝까지 붙잡고 처리하는 구조가 아니라 이벤트 단위로 처리한다.
이벤트 발생 -> 해당 작업만 처리 -> 다시 대기
✅ 어떤 이벤트를 기다리는가
epoll_wait는 단순히 "연결 요청"만 기다리는 게 아니다.
- 새로운 연결 (listenfd)
- 읽기 가능(readable)
- 쓰기 가능(writeable)
- 연결 종료
👉 즉, 모든 소켓 상태 변화를 감지한다.
❓읽기/쓰기 가능 여부 이벤트는 왜 보낼까?
이를 이해하기 위해서는 tiny.c와 실제 서버의 구조 차이를 먼저 이해해야 한다.
tiny.c는 연결이 도착하면 해당 fd 하나만 확인하면된다.
connfd = accept(...);
doit(connfd);
- connfd 소켓 하나에 대해서만 처리하면 되는 구조
하지만 실제 서버는 여러개의 연결을 처리해야되기 때문에 연결된 소켓도 여러개이다.
connfd A
connfd B
connfd C
...
이 상태에서 서버는 지금 어떤 소켓에 데이터가 와 있는지 알 수 없다.
-> 즉, 지금 read/write를 수행해도 되는 소켓이 무엇인지 모른다.
이를 해결하기 위해 등장하는 것이 epoll_wait이다.
epoll은 서버가 미리 여러 소켓(fd)을 등록해두면, 그 중에서 상태 변화가 발생한 소켓들만 선별하여 알려주는 메커니즘이다.
서버는 epoll_wait을 호출함으로써, “지금 읽을 수 있는 소켓”, “지금 쓸 수 있는 소켓”과 같이 현재 I/O가 가능한 소켓들의 목록을 전달받게 된다.
그리고 이 목록에 포함된 소켓들에 대해서만 read 또는 write를 수행한다.
결과적으로 서버는 더 이상 모든 소켓을 직접 확인할 필요 없이, 커널이 전달해주는 ‘처리 가능한 소켓 집합’을 기반으로 효율적으로 작업을 수행하는 구조로 동작하게 된다.
2. 현대 웹에서의 동적 컨텐츠 처리
tiny.c 코드를 공부하면서 이해가 되지 않는 부분이 정적 컨텐츠와 동적 컨텐츠를 구분하여 실행하는 것이었다.
최근 웹에 대해서 공부할 때 백엔드 코드에서 이런식으로 분기하는 것을 본 적이 없기 때문이다.
또 CGI 프로그램을 직접 프로세스를 만들어서 실행하는 로직도 이상하다고 느껴졌다.
과거 웹에서의 동적 컨텐츠 처리
tiny.c에서 1 + 2를 수행하는 로직은 다음과 같은 순서로 흘러간다.
요청: /cgi-bin/adder?1&2
웹서버
→ fork()로 adder 프로세스 새로 생성
→ adder가 1+2 계산
→ 결과 출력
→ 프로세스 종료
-> 매 요청마다 프로세스를 새로 만들고 죽인다.
이는 지금 시점에서 보면 분명 문제가 있는 구조이다.
왜냐하면..
- fork 비용
- execve 비용
이 너무 비싸고, 상태 유지도 불가능하기 때문이다.
하지만, 이 당시에는 이 방법이 아주 합리적인 선택이었다.
이 방법은 독립된 프로세스를 통해 요청들을 완벽히 격리시킬 수 있었으며, 구현이 매우 쉽기 때문이다.
또한 당시의 웹은 정적 컨텐츠가 대부분이었고, 동적 컨텐츠는 일부 특별한 요청에만 필요했다. 따라서 매 요청마다 새로운 프로세스를 방식이 다소 비효율적이더라도, 전체 시스템 성능에 치명적인 문제가 되지는 않았다.
현대 웹에서의 동적 컨텐츠 처리
사실 최근에는 동적/정적 구분 자체가 흐려졌다.
과거처럼 “파일을 그대로 보내면 정적, 프로그램을 실행하면 동적”이라는 식의 구조적인 차이는 거의 사라졌고,
지금은 대부분의 요청이 하나의 동일한 처리 흐름 안에서 다뤄진다.
요청 수신 → 라우팅 → 핸들러 실행 → 응답 생성 → 반환
여기서 중요한 점은 정적 파일이든, DB 조회 결과든, API 응답이든 항상 "핸들러가 응답을 만든다"는 것이다.
예를 들어:
- /index.html -> 파일을 읽는 핸들러
- /api/user -> DB 조회하는 핸들러
- /posts/1 -> 템플릿을 렌더링하는 핸들러
겉으로는 서로 다른 성격의 요청처럼 보이지만, 내부적으로는 모두 동일하게 하나의 함수 실행 과정으로 처리된다.
과거에는 이런 방식이 아니었다. 서버가 동적 컨텐츠를 직접 만들어내는 구조가 보편적이지 않았기 때문에, 동적 요청이 들어오면 별도의 프로그램을 실행해 그 결과를 받아오는 방식이 주로 사용되었다. 이때 사용된 방식이 fork() 후 execve()로 CGI 프로그램을 실행하는 구조였다.
반면 현대 웹에서는 요청마다 새로운 프로그램을 실행하지 않는다. 이미 실행 중인 애플리케이션 서버가 요청을 받은 뒤, 그 안에 등록된 라우터와 핸들러 함수를 호출해 응답을 생성한다. 즉, 예전에는 요청마다 프로그램을 실행했다면, 지금은 계속 실행 중인 서버 내부에서 필요한 로직만 호출하는 방식에 가깝다.
이 차이 때문에 오늘날의 동적 컨텐츠는 더 이상 특별한 처리 방식으로 분리되지 않는다. 파일 응답이든, 데이터 조회 결과든, 템플릿 렌더링이든 모두 같은 서버 프로세스 안에서 처리되며, 차이는 단지 어떤 로직이 응답을 만들어내느냐에 있다.
'크래프톤 JUNGLE' 카테고리의 다른 글
| [Week10] WIL - Pintos_Project 2 (0) | 2026.05.08 |
|---|---|
| [Week9] WIL - Pintos_Project1 (0) | 2026.04.30 |
| [Week7] WIL - 무한 코어타임 지옥 (2) | 2026.04.16 |
| [Week6] WIL - Hello C World! (0) | 2026.04.09 |
| [Week5] WIL - DP 알고리즘 부수기 (0) | 2026.04.02 |