최근 프로젝트의 웹 서버를 확인 중 프로젝트마다 웹 서버의 다른 관리 방법이 이제서야 의문이 들기 시작했다. 현재는 여러 서비스를 담당하고 있지만 한 서비스는 프론트 프로젝트 내 웹 서버 설정 및 배포 프로세스가 포함되어 있고 다른 서비스에서는 서버 프로젝트에서 웹 서버를 관리하고 있다. 이렇게 다른 관리가 궁금하여 주변 다른 팀 혹은 회사 지인들에게도 물어보았지만 크게 위 두 가지 형태를 기반으로 관리 방법이 나누어지는 것을 확인 할 수 있었다. 이 방법들이 각 환경에 따라 달라질 수 있다는 생각이 들지만, 웹 서버에 대해 알아보고 각 프로젝트의 웹 서버 관리를 어떻게 하는 것이 좋을지 정리해 보려고 한다.
Web Server 란?
“웹 서버”란 단순히 브라우저에서 HTTP 요청에 따른 HTTP 응답을 전송해 주는 서버라고도 할 수 있을 것 같다. MDN에 정리된 “웹 서버란 무엇일까?”에 따르면 웹 서버는 크게 하드웨어, 소프트웨어 측면으로 나눌 수 있으며 정적 웹 서버와 동적 웹 서버로 나눌 수 있을 것 같다.
앞서 언급한 동적 웹 서버는 통상적으로 애플리케이션 서버에 가깝다고 생각되어 여기서는 소프트웨어 측면의 정적 웹 서버를 기준으로 웹 서버에 대해 알아볼 생각이다.
Static Web Server
정적 웹 서버는 브라우저의 HTTP 요청에 맞는 HTML, CSS, JS, 이미지 파일과 같이 변하지 않는 정적 리소스를 HTTP 응답으로 전송해 주는 서버이다. 이때 웹 서버는 저장된 파일을 지정된 경로에서 찾아 추가적인 가공 없이 요청에 맞게 응답해 주어야 한다.
대표적인 웹 서버로는 nginx, Apache 서버가 사용되며 nginx는 2004년, Apache는 1995년부터 수많은 기업에 사용되어 대형 트래픽 처리 및 운영체제, 브라우저, CDN 등과의 호환성과 보안 측면에서 수년간 쌓아온 신뢰를 하고 있기에 아직도 웹 서버로의 주 선택지가 아닌가 싶다.
Web Server 구축
Linux 환경에서 nginx로 웹 서버를 구축하는 것을 정리해 보겠다. nginx 웹 서버 구축에는 Linux가 많이 사용되는데 이유는 운영체제 안정성과 성능이 뛰어나고 시스템에서의 고성능 네트워크를 적극적으로 활용할 수 있다고 한다. windows의 경우 nginx에 대한 기능 제한이 많아 성능적으로도 Linux가 뛰어나다고 한다. 또한 windows nginx 버전은 beta 버전으로 간주된다.
nginx
Linux 환경에서 nginx 설치 확인 및 설치는 간단하게 아래 커맨드로 가능하다.
cli$ nginx -v # 패키지 리스트 업데이트 $ sudo apt update # nginx 설치 $ sudo apt install nginx -y
아래는 nginx에서 사용되는 기본적인 설정들로 정리해 보았다. 해당 설정은 nginx.conf라는 파일에 정의되어 nginx 웹 서버 실행 시 사용된다.
conf# nginx 메인 설정 파일 # nginx 프로세스를 실행할 사용자 user www-data; # 워커 프로세스 수 (auto: CPU 코어 수에 맞게 자동 설정) worker_processes auto; # 에러 로그 파일 경로 및 로그 레벨 (warn 이상만 기록) error_log /var/log/nginx/error.log warn; # nginx 프로세스 ID 파일 위치 pid /var/run/nginx.pid; # ----------------------------- # 이벤트 처리 설정 블록 events { # 각 워커 프로세스가 동시에 처리할 수 있는 최대 연결 수 worker_connections 1024; } # ----------------------------- # HTTP 관련 전역 설정 블록 http { # MIME 타입 정의 파일 포함 (Content-Type 헤더에 사용) include /etc/nginx/mime.types; # MIME 타입을 알 수 없을 때 기본으로 사용할 타입 default_type application/octet-stream; # 액세스 로그 파일 경로 설정 access_log /var/log/nginx/access.log; # 파일 전송 시 sendfile 시스템 콜 사용 (성능 향상) sendfile on; # 연결 유지 시간 (초) - keep-alive 설정 keepalive_timeout 65; # ----------------------------- # Gzip 압축 설정 (선택적 성능 최적화) # Gzip 압축 사용 여부 gzip on; # 압축할 콘텐츠 타입 목록 (텍스트, JSON, JS 등) gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; # ----------------------------- # 기본 서버 블록 설정 (가상 호스트) server { # 이 서버 블록이 리스닝할 포트 (HTTP 기본 포트 80) listen 80; # 수신할 도메인 이름 (여기선 localhost만 처리) server_name localhost; # 웹 루트 디렉토리 (정적 파일이 위치한 곳) root /usr/share/nginx/html; # 디폴트로 로드할 인덱스 파일 목록 index index.html index.htm; # ----------------------------- # 요청 라우팅 설정 location / { # 요청 경로가 실제 파일이면 서빙, 없으면 404 반환 try_files $uri $uri/ =404; } # ----------------------------- # 에러 페이지 설정 # 404 에러 발생 시 보여줄 사용자 정의 페이지 error_page 404 /404.html; # /404.HTML 요청은 내부 처리만 허용 (외부 접근 차단) location = /404.html { internal; } } }
CLI
nginx 실행은 Linux 환경에서는 아래 시스템 명령으로 실행 및 상태 확인 등이 가능하지만 Docker 컨테이너 환경에서는 systemd(init 시스템)을 포함하지 않기 때문에 직접 nginx 프로세스를 실행하거나 수동으로 제어해야 한다.
cli# Linux 환경에서 시스템 명령 실행 $ sudo systemctl start nginx $ sudo systemctl enable nginx $ sudo systemctl status nginx
Dockerfile
Docker 컨테이너 환경에서는 Docker file 내 CMD 설정으로 컨테이너 실행 시 포그라운드(foreground) 모드로 실행해야 한다. nginx는 기본적으로 백그라운드 실행되며 Docker 컨테이너 환경에서 포그라운드로 실행하지 않으면 바로 종료 되어버린다고 한다.
- 포그라운드: 프로세스를 분리해 실행하지 않고 터미널을 점유하며 제어를 계속 유지하도록 설정
dockerfileFROM nginx:alpine # 기본 nginx 경량 버전으로 alpine Linux 기반 이미지 COPY ./html /usr/share/nginx/html # 정적 파일 위치 COPY conf/nginx.conf /etc/nginx/nginx.conf # nginx 설정 파일 덮어쓰지 -> 커스텀 서버 설정 CMD ["nginx", "-g", "daemon off;"] # nginx 포그라운드 실행
nginx를 위와 같은 형태로 실행 이후 브라우저에서 웹 서버 도메인에 진입하는 경우, DNS 서버에서 도메인에 맞는 IP로 변환 및 요청이 전송되고 웹 서버는 요청에 맞는 응답을 처리하게 된다. 이때 프론트에서 관리하는 리소스들에 대해 현재 우리는 어떻게 관리하고 있는지 정리해 보겠다.
Static Resource
정적 리소스는 크게 HTML 그리고 번들된 js와 이미지, 폰트 등이 존재한다. 여기서의 정적 리소스 관리 설명은 js 번들을 어떻게 하고 이미지와 폰트는 어떻게 최적화하고 있으며 HTML 내에서는 리소스를 어떠한 방식으로 불러온다와 같은 설명은 하지 않을 예정이다. 이 글에서의 메인인 웹 서버가 정적 리소스를 어디서, 어떻게 서빙하는지에 대해 정리해 보겠다.
CDN
대부분 정적 리소스는 CDN(Content Delivery Network)으로 관리되고 있지 않나 싶다. 처음 도메인으로의 요청을 받은 웹 서버는 같은 컨테이너에 위치한 HTML을 응답하게 된다. 이후 브라우저가 HTML을 응답받고 CDN 경로에 맞는 리소스를 요청하여 응답받게 된다.
- 브라우저 → 웹 서버 → HTML 응답
- 브라우저 HTML 파싱 → CDN → 리소스 응답
위와 같은 처리가 아마도 내가 생각하는 정상적인 웹 서버와 CDN에서의 응답 처리 구조가 아닐까 생각한다. 하지만 관리 중인 특정 프로젝트는 다른 형태로 리소스 관리되고 있다.
일단 HTML을 포함한 모든 리소스가 CDN에서 관리가 되고 있다. HTML이 CDN에서 관리되고 있다 보니 브라우저에서 처음 도메인으로의 요청에 웹 서버는 CDN으로 프록시하여 HTML을 내려주도록 응답하고 있다. 그리고 다른 정적 리소스 또한 CDN으로 요청을 바로 하는 것이 아닌 웹 서버로 요청되고 웹 서버에서 HTML과 동일하게 CDN으로 프록시하여 리소스를 내려주는 구조로 구현되어 있다.
- 브라우저 → 웹 서버 → CDN → HTML 응답
- 브라우저 HTML 파싱 → 웹 서버 → CDN → 리소스 응답
왜 이러한 구조로 되었을까?에 대한 고민은 쉽게 답을 찾을 수 있었다. 바로 웹 서버를 프론트에서 관리하는게 아닌 백엔드에서 관리하는 상황에 발생할 수 있는 문제로 생각할 수 있었다. 앞선 구조에서는 HTML을 포함한 모든 정적 리소스는 프론트에서 관리하고 있으니 빌드 결과물을 CDN에 업로드 하는것으로 본인들의 역할을 모두 수행했다고 할 수 있다. 백엔드는 단순히 정적 리소스에 대한 경로가 지정된 웹 서버를 실행해 두는 것으로 역할을 다했다고 할 수 있는데 이때 HTML에 대한 관리는 프론트에서 처리하고 있다 보니 웹 서버에 HTML에 대한 CDN 경로를 지정하는 것으로 역할을 모두 수행했다고 할 수 있겠다.
사실 위 처리로도 정상적인 서비스는 가능하지만 문제라고 생각했던 부분들을 정리해보겠다. 첫번째로 HTML은 항상 배포된 최신 버전으로 응답되어야하지만 CDN으로 적절하지 않은 캐시 무효화 전략이 사용되는 경우 사용자에게 잘못된 HTML이 전달될 수 있다. 그리고 HTML 요청을 포함하여 nginx 프록시를 통해 CDN에서 리소스를 응답 받아 처리하는 경우 실제 CDN에서 바로 응답을 받는 속도보다 느리게 처리될 것이다.
Docker Container
현재 관리 중인 프로젝트중 일부는 CDN을 사용하지 않고 웹 서버와 동일한 Docker Container 내에 리소스를 위치시키고 웹 서버에서 해당 리소스를 바로 서빙하는 방식으로 관리되는 프로젝트도 존재한다.
아래는 간단하게 위 구조를 설명하기 위한 Dockerfile 예제이다.
dockerfile# 1단계: 빌드 스테이지 (Node.js로 React 앱 빌드) FROM node:18-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm install COPY . . RUN npm run build # 2단계: 실행 스테이지 (nginx로 정적 파일 서빙) FROM nginx:1.21-alpine AS run # 빌드된 파일을 nginx 기본 디렉터리로 복사 COPY --from=build /app/dist /usr/share/nginx/html # 커스텀 nginx 설정 복사 COPY nginx.conf /etc/nginx/nginx.conf # 포트 80 노출 EXPOSE 80 # nginx 실행 (포그라운드 모드) CMD ["nginx", "-g", "daemon off;"]
Docker Container에 정적 리소스 포함하는 구조로 서비스를 관리하는 경우 하나의 컨테이너로 웹 서비스를 관리할 수 있어 단순한 빌드/배포 구조로 관리 할 수 있다. 그리고 CDN에 의존성이 없어 CDN 장애 시에도 정상적인 서비스가 가능하다. 하지만 정적 리소스가 이미지에 포함되므로 이미지 크기가 증가하고 배포 시간 및 스토리지 비용이 증가할 수 있다. 또한 트래픽이 많은 서비스의 경우 정적 리소스에 대한 네트워크 비용 및 스케일링 비용이 기하급수적으로 증가할 수 있다.
해당 구조의 프로젝트는 처음 프로젝트 설계 시, 그 당시 CDN 장애가 가끔 발생하였고 CDN 장애에 영향받지 않도록 서비스 내부적으로 관리하여 정상적인 서비스를 하는 게 좋겠다는 서버측 의견으로 결정되었다. 이 프로젝트에서는 프론트 프로젝트의 빌드/배포 과정에 정적 리소스를 이미지 내 포함이 필요하다. 그래서 nginx 설정과 위 예시와 같은 정적 리소스를 빌드, 복사하고 nginx 웹 서버를 실행하는 Dockerfile이 프론트 프로젝트 내에서 관리되고 있다. 그리고 프론트 역할은 빌드/배포까지로 완료되고 이후 웹 서버에 대한 Docker Container는 서버에서 관리하게 된다.
Web Server는 누구의 것인가?
지금 생각해 보면 그동안 웹 서버가 우리의 관리의 대상이라고 생각하지 않았던 것 같다. 그렇다 보니 위와 같은 요상한 구조에 대해 뒤늦게 알게 되었고 해결해야 하는 상황에서도 서버분들과의 논의가 필요하다고 생각하고 있다. 실제로 nginx에 대한 설정이나 실행 중인 서버를 서버분들이 관리하고 있는 프로젝트들도 있으니 이 부분은 당연하다고 할 수 있을 것 같다. 물론 필요한 경우 nginx 구축 및 설정을 확인하고 문제가 되는 경우 수정 관리도 하지만 좀 더 좋은 웹 서비스를 위해서라면 웹 서버에 대한 설정부터 모든 환경까지 프론트 개발자가 담당자로 확실하게 배정되어 관리하는게 좋지 않을까 생각된다.
최근 여러 회사의 프론트 자격 요건을 보다 보면 우대 사항에 웹 서버에 대한 관리나 k8s 운영 경험 등이 포함된 곳도 있으며 가끔 필수사항에 포함하는 경우도 있었다. 이러한 상황들을 보다 보니 이제는 단순히 웹 페이지를 개발하는 프론트 개발자에서 벗어나 웹 서버 그리고 인프라를 포함한 능력을 갖춘 개발자가 될 수 있어야 하는 시대가 이미 온 것이 아닌가 생각이 든다.
"The best way to prepare for the future is to shape it." - Peter Drucker -