Webserv
2023-06-26
🧠 요약
- 요약: 이 글은 42 Seoul의 Webserv 과제 수행 경험에 대한 회고이다. HTTP의 근본적인 동작 원리를 파악하고, NGINX의 설정을 교과서 삼아 C++로 직접 웹서버를 구현하는 전 과정을 다룬다. 특히 HTTP 요청/응답 처리의 핵심 로직, I/O 멀티플렉싱을 통한 동시성 관리, CGI를 이용한 동적 콘텐츠 생성, 그리고 쿠키 및 세션 관리 같은 확장 기능 구현에 대한 고민과 해결 과정을 기록했다.
📌 본문
왜 URL은 'HTTP'로 시작하는가?
Webserv 과제의 서브젝트는 "이제 여러분은 마침내 URL이 왜 HTTP로 시작하는지 이해하게 될 것입니다."라는 문장으로 시작한다. 이 과제는 단순히 주어진 기능을 구현하는 것을 넘어, 우리가 매일 사용하는 웹의 가장 근본적인 통신 규약인 HTTP(HyperText Transfer Protocol)의 실체를 파헤치는 여정이었다. 키보드로 URL을 입력하고 엔터를 누르는 그 찰나의 순간, 브라우저와 서버 사이에서 어떤 일이 벌어지는지 직접 코드로 구현해보는 것. 길고 길었던 Webserv 과제를 마무리하며 그동안의 학습과 경험을 정리해본다.
우리의 교과서: NGINX 설정 파일
맨땅에서 웹서버를 만드는 것은 막막한 일이다. 다행히 우리에게는 NGINX라는 훌륭한 교과서가 있었다. NGINX의 nginx.conf 설정 파일을 분석하며 실제 프로덕션 레벨의 웹서버가 어떻게 구성되고 동작하는지에 대한 핵심적인 아이디어를 얻을 수 있었다.
- Nginx
user www www; worker_processes 5; error_log logs/error.log; pid logs/nginx.pid; worker_rlimit_nofile 8192; events { worker_connections 4096; } http { include conf/mime.types; #... }
위 설정은 단순한 텍스트가 아니었다. worker_processes나 worker_connections 같은 지시어는 서버의 동시 처리 성능과 직결되는 중요한 개념이었고, include conf/mime.types는 서버가 어떻게 수많은 종류의 파일(HTML, CSS, JPG 등)을 구별하여 클라이언트에 정확한 타입을 알려주는지에 대한 해답이었다. 이 설정 파일을 해부하며 우리가 만들어야 할 웹서버의 청사진을 구체적으로 그릴 수 있었다.
핵심 기능 구현기: 바닥부터 쌓아 올린 웹서버
1. I/O 멀티플렉싱: 동시 접속의 시작 웹서버는 수많은 클라이언트의 요청을 동시에 처리해야 한다. 이를 위해 I/O 멀티플렉싱 기술은 필수적이었다. select(), poll(), kqueue(), epoll() 등 다양한 모델을 학습하고, 그중 하나를 선택해 여러 소켓의 이벤트를 한 번에 감지하고 처리하는 로직을 구현했다. 이 과정을 통해 블로킹(blocking) 방식의 한계를 명확히 이해하고, 이벤트 기반(event-driven) 프로그래밍의 효율성을 체감할 수 있었다.
2. HTTP 요청과 응답의 생명주기
-
요청 파싱: 클라이언트(브라우저)로부터 들어온 요청은 순수한 텍스트 덩어리다. 이 Raw Text를 파싱하여 Start Line(Method, URI, Version), Headers, Body로 정확하게 분리하는 파서를 구현하는 것이 첫 관문이었다. 특히
Content-Length헤더를 기준으로 Body의 끝을 판단하고, 청크(Chunked) 인코딩 같은 복잡한 케이스를 고려하는 과정에서 많은 시간을 쏟았다. -
응답 생성: 파싱된 요청을 분석하여 그에 맞는 응답을 생성해야 한다. 요청된 URI에 해당하는 파일을 찾아 읽고, 적절한 Status Code(200 OK, 404 Not Found, 500 Internal Server Error 등)와 Headers(Content-Type, Content-Length, Date 등)를 조합하여 완전한 HTTP 응답 메시지를 만들어 클라이언트에 전송했다. 이 과정은 HTTP 명세(RFC)를 수시로 들여다보며 표준을 지키기 위해 노력했던 기억이 생생하다.
3. CGI (Common Gateway Interface): 동적인 웹을 향하여 정적인 HTML 파일만 제공하는 서버는 반쪽짜리다. 서버에서 스크립트(주로 PHP나 Python)를 실행하여 그 결과를 클라이언트에 전달하는 CGI는 동적인 웹을 구현하기 위한 핵심 기능이었다. fork()로 자식 프로세스를 만들고, execve()로 CGI 스크립트를 실행시킨 뒤, 파이프(pipe)를 통해 스크립트의 표준 출력(stdout)을 받아 클라이언트 응답으로 가공하는 과정을 구현했다. 여러 CGI 요청을 동시에 안정적으로 처리하는 것은 이 과제의 가장 큰 도전 과제 중 하나였다.
4. 쿠키와 세션: 상태를 기억하는 서버 HTTP는 본질적으로 상태가 없는(stateless) 프로토콜이다. 하지만 로그인과 같은 기능을 구현하려면 서버는 클라이언트를 기억해야 한다. 이를 위해 쿠키와 세션 관리 기능 구현을 고려했다. 서버가
Set-Cookie 헤더를 통해 클라이언트에 식별자를 보내고, 클라이언트는 이후 요청에 Cookie 헤더로 그 식별자를 다시 보내는 메커니즘을 구현함으로써, 비로소 상태를 기억하는 웹서버의 기반을 닦을 수 있었다.
🔄 회고 및 인사이트
-
새로 알게 된 점
-
HTTP는 단순한 URL의 접두사가 아니라, 클라이언트와 서버 간의 약속이자, 명확한 규칙을 가진 텍스트 기반의 통신 규약 그 자체라는 사실을 코드로 구현하며 체감했다.
-
NGINX 설정 파일의 각 지시어가 실제로는 소켓 프로그래밍, 프로세스 관리, 파일 시스템 접근 등 저수준 시스템 콜과 어떻게 연결되는지 깊이 있게 이해하게 되었다.
-
CGI의 동작 원리를 파이프와 프로세스 관점에서 이해함으로써, 웹서버가 어떻게 외부 프로그램과 상호작용하여 동적인 콘텐츠를 생성하는지에 대한 그림을 명확히 그릴 수 있었다.
-
-
관점의 변화
-
이전에는 웹서버를 단순히 '아파치나 NGINX를 설치해서 사용하는 프로그램'으로만 생각했다. 이제는 소켓 통신, I/O 멀티플렉싱, 프로토콜 파싱, 프로세스 관리 등 복잡한 저수준(low-level) 로직이 유기적으로 결합된 하나의 거대한 시스템으로 바라보게 되었다.
-
'추상화'의 위대함을 깨달았다. 브라우저에 URL을 입력하는 간단한 행위 뒤에 숨겨진 이 모든 복잡한 과정을 직접 구현해보니, Node.js의 Express나 Python의 Django 같은 현대적인 웹 프레임워크가 개발자에게 얼마나 엄청난 편의를 제공하는지, 그 고마움을 뼈저리게 느꼈다.
-
-
추후 확장 아이디어
-
구현한 웹서버에 쿠키와 세션 관리 기능을 실제로 완성하여, 간단한 로그인과 게시판 같은 상태 기반 서비스를 직접 만들어보고 싶다.
-
성능 개선을 위해
select()대신kqueue()(macOS)나epoll()(Linux)을 사용하여 더 효율적인 I/O 멀티플렉싱 모델을 적용하고 싶다. -
HTTP/1.1의 Keep-Alive, 파이프라이닝 같은 고급 기능을 추가로 구현하여 더 빠르고 효율적인 웹서버로 발전시켜보고 싶다. 이 과제는 끝이 아니라, 웹의 깊은 곳을 탐험하는 새로운 시작이다.
-