웹 서버와 웹 어플리케이션 서버

Nginx를 알아보기 전에 웹 서버와 웹 어플리케이션 서버의 차이점을 알아보도록 하자.

웹 서버

웹 서버는 클라이언트의 요청에 대해서 정적인 컨텐츠를 응답해준다. 이때 정적인 컨텐츠는 HTML, CSS, JavaScript를 비롯하여, 이미지, 비디오, 오디오같은 파일이라고 생각하면 된다.

개인적으로 비디오나 오디오가 과연 정적 컨텐츠인가? 라는 생각이 들었어서 조금 더 찾아봤는데, 한 번 저장되면 어떤 클라이언트가 요청해도 같은 컨텐츠를 반환하는 것들을 모두 정적 컨텐츠라고 일컫는다고 한다.

웹 어플리케이션 서버

웹 서버가 정적인 컨텐츠를 처리한다면, 웹 어플리케이션 서버는 동적인 컨텐츠를 처리하게 된다. 동적인 컨텐츠라는 것은 클라이언트가 보내는 요청에 따라서 그 응답이 바뀌는 것을 의미한다.

이때 클라이언트가 기대하는 응답을 처리하기 위해서 비즈니스 로직을 통하여 클라이언트의 요청을 처리하고, 데이터베이스를 비롯한 서드 파티 시스템과의 통신을 수행한 다음에 클라이언트에게 응답하는 구조를 가진다.


정리하자면 어떤 홈페이지에 들어갔을 때 어떤 사용자라도 무조건 보게 되는 메인 페이지 같은 컨텐츠는 웹 서버가 처리하고, 로그인이나 어떤 상품을 주문하는 등의 행위는 웹 어플리케이션이 처리한다고 보면 된다.

Nginx

이제 웹 서버와 웹 어플리케이션 서버의 차이점도 알아봤으니 Nginx를 알아보도록 하자. Nginx는 경량 웹 서버로 정적파일을 제공하거나 요청을 다른 서버로 전달하는 리버스 프록시 서버로 활용된다.

또한 서버를 여러 대 두는 스케일 아웃의 상황에서 Nginx를 활용하면 로드 벨런싱이나 무중단 배포도 가능하다. 여기서 왜 최근에는 Nginx를 많이 채택하는 지 알아봐야 한다. 따라서 아파치에 대해서 짧게 알아보도록 하자.

아파치 웹 서버의 한계

2000년대에는 아파치 웹 서버의 시장 점유율이 압도적으로 1등이었다.

웹 서버 시장 점유율

이런 아파치 웹 서버는 클라이언트의 요청에 대해서 각각의 스레드를 생성하여 처리한다. 이런 처리 방식은 컴퓨터의 보급률이 올라감에 따라 클라이언트의 요청이 증가하면서 클라이언트가 1만 이상 동시에 요청을 보낸다면 CPU와 메모리의 사용량이 증가하고 문맥 전환에 대한 비용이 커지는 문제가 발생하였다.

결과적으로 클라이언트의 요청에 대한 처리 시간(RPS)가 증가하게 되었고, 이를 C10K 문제라고 한다. 이를 자세하게 다룬 올리브영 테크 블로그 포스팅을 참고자료에 첨부하였다. 정리하자면 아파치 웹 서버는 대규모 동시 접속자 수를 효율적으로 처리할 수 없는 구조적 한계를 가지고 있다.

Nginx는 다른가?

Nginx는 하나의 마스터 프로세스와 다수의 워커 프로세스로 구성되어 실행된다. 그리고 클라이언트의 요청을 이벤트로 간주하여 처리하는 이벤트 기반 방식으로, 하나의 워커 프로세스가 클라이언트의 요청을 처리하게 된다.

이때 중요한 것은 워커 프로세스의 스레드 개수는 하나라는 것이다. 스레드가 하나임에도 대규모 요청에 탁월한 이유는 이벤트 기반 방식을 통하여 비동기로 모든 클라이언트의 요청을 처리하기 때문이다.

이러한 장점 덕분에 아파치 웹 서버만큼 자원을 소모하지 않으면서도 대규모 동시 접속자 수를 효율적으로 처리할 수 있게 된 것이다. 정리하자면 Nginx의 장점은 다음과 같다.

  • 자원의 효율적인 사용
  • 비동기적 이벤트로 인한 동시적 요청 처리
  • 간단한 설정
  • 리버스 프록시

Nginx 활용하기

이제 Nginx가 무엇인지 알았으니 활용을 해보도록 하자. 나의 경우에는 Nginx를 정적 파일 호스팅과 리버스 프록시 서버로서 활용하였다. 참고로 Nginx를 서버에 도입하면 기본적인 구조는 아래와 같다고 보면 된다.

서버 구조

물론 개발자 개인은 가난하기에 이 모든 것을 하나의 EC2에 띄울 수 밖에 없다. 따라서 앞서 배포한 도커 컴포즈 실습 자료를 활용하도록 하자. 젠킨스를 활용하지 않기 때문에 단순히 로컬에서 도커 컴포즈를 실행시켜도 된다.

nginx.conf 파일 마운트

Nginx의 모든 설정은 내부적으로 sites-available와 sites-enabled를 통하지만, 도커를 통하여 Nginx를 실행시킨다면 nginx.conf 파일에 모두 정의하는 것이 속편하다. 그렇기 때문에 Nginx 도커 컴포즈를 설정할 때 호스트의 nginx.conf 파일을 Nginx 컨테이너의 nginx.conf 파일에 마운트하는 방식을 추천한다.

services:
  nginx:
    container_name: nginx
    image: nginx:stable-alpine3.20-perl
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./settings/nginx/nginx.conf:/etc/nginx/nginx.conf
      - ./settings/nginx/ssl/localhost+3.pem:/etc/nginx/ssl/fullchain.pem:ro
      - ./settings/nginx/ssl/localhost+3-key.pem:/etc/nginx/ssl/privkey.pem:ro
      # 정적 파일 호스팅을 하는 경우
      # - ./dist:/usr/share/nginx/html
    environment:
      - TZ=Asia/Seoul
    depends_on:
      - spring
      - mysql
      - mongo
    restart: always
    networks:
      - compose-network

HTTPS 설정 적용하기

EC2의 경우

Nginx를 EC2에 띄운다면은 certbot같은 인증서 발급 어플리케이션을 설치하여 발급받고, 컴포즈 파일에서 volumes 부분을 변경하기만 하면 된다. 예를 들어서 인증서를 발급받은 경로가 /example/path/라면 다음과 같은 설정이 될 것이다.

volumes:
  - ./settings/nginx/nginx.conf:/etc/nginx/nginx.conf
  - /example/path/fullchain.pem:/etc/nginx/ssl/fullchain.pem:ro
  - /example/path/privkey.pem:/etc/nginx/ssl/privkey.pem:ro
  # 정적 파일 호스팅을 하는 경우
  # - ./dist:/usr/share/nginx/html

이러기만 하면 인증서가 만료될 시점에 certbot을 주기적으로 실행시켜 인증서를 갱신하기만 하면 끊임없이 HTTPS 프로토콜을 EC2에 적용시킬 수 있다.

로컬 환경의 경우

로컬 환경의 경우에는 다음 포스팅을 참고하도록 하자. 내 로컬 환경은 윈도우이기 때문에(맥북 소망…) 다음과 같은 명령어들을 타이핑 하였다. (CMD 관리자 권한 필수)

choco install mkcert
mkcert -install
mkcert "localhost" localhost 127.0.0.1 ::1

그리고 실습 자료 디렉터리에 있는 /settings/nginx/ssl 여기에 발급받은 두 pem 파일을 붙여넣으면 된다.

nginx.conf 설정

이미 실습자료에 있지만 nginx.conf 설정만 필요한 사람이 있을 수 있기 때문에 한 번 더 여기에 작성하도록 하겠다.

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    client_max_body_size 50M;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    # conf에서 모든 것을 정의하면 필요가 없음
    # include /etc/nginx/conf.d/*.conf;

    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    ssl_certificate /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    server {
        listen 80 default_server;
        listen [::]:80 default_server;
        server_name localhost;
        # 모든 HTTP 요청을 HTTPS 리디렉트
        return 301 https://$host$request_uri;
    }

    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        server_name localhost;

        location /api/ {
            proxy_pass http://spring:8080;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_redirect off;

            # WebSocket 지원 (Spring Boot에서 필요할 경우 추가)
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "Upgrade";
        }

        location /jenkins/ {
            sendfile off;
            proxy_pass http://jenkins:9000;
            proxy_http_version 1.1;
            proxy_redirect off;

            # Required for Jenkins websocket agents
            proxy_set_header   Connection        $connection_upgrade;
            proxy_set_header   Upgrade           $http_upgrade;

            proxy_set_header   Host              $http_host;
            proxy_set_header   X-Real-IP         $remote_addr;
            proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Proto $scheme;
            proxy_max_temp_file_size 0;

            #this is the maximum upload size
            client_max_body_size       10m;
            client_body_buffer_size    128k;

            proxy_connect_timeout      90;
            proxy_send_timeout         90;
            proxy_read_timeout         90;
            proxy_request_buffering    off; # Required for HTTP CLI commands
        }

        location / {
            root /usr/share/nginx/html;
            index index.html;
            error_page 404 /index.html;
            error_page 403 /index.html;
        }
    }
}

nginx는 다른 사람들이 작성한 부분에서 내가 원하는 부분을 복붙하고, 문제가 생기면 chatGPT의 도움을 받는 등으로 작성하였다. 그렇기 때문에 세부적인 문법은 잘 모른다…

개인적으로 jenkins를 리버스 프록시로 돌릴 때 조금 애먹었었는데, 이 부분과 관련된 좋은 포스팅이 있어서 공유한다. 이 블로그와 동일한 설정을 했더니 잘 되었다.

실습 자료를 로컬에서 그대로 실행했다면 localhost/api/v1/hello에 접속해보자. 다음과 같은 응답이 올 것이다.

// https://localhost/api/v1/hello

{
  "message": "Hello World"
}

S3 + 프론트 클라우드를 통한 배포와 Route 53을 활용한 도메인 연결 포스팅 이후에 젠킨스를 통한 CICD를 다룰 것인데, 이를 위해 스프링 부트 부분은 자신이 직접 만든 간단한 어플리케이션으로 대체하는 것을 추천한다.


이렇게 Nginx에 대해서 간단히 알아보고, Nginx를 도커를 통하여 실행시켜봤다. 뭔가 설정 파일만 덩그러니 올릴 수 있었지만, 양심의 가책이 조금 들어서 이것저것 찾아보니 포스트가 생각보다 길어진 것 같다.

참고자료

Nginx란 무엇인가?

웹 서버와 애플리케이션 서버의 차이점은 무엇인가요?

동적 컨텐츠 및 정적 컨텐츠

Nginx 란?

Nginx vs Apache

고전 돌아보기, C10K 문제 (C10K Problem)

로컬 개발환경에 HTTPS 적용하기

Jenkins Nginx Reverse Proxy 적용

댓글남기기