[개발일지]/CS 스터디 플랫폼

5장. 프론트엔드 서버는 Nginx 에 올려라. 왜?

biz-ninza 2025. 1. 13. 02:45

스프링부트에는 톰캣이라는 내장 WAS가 있어서 정적페이지 제공도 가능하다.
그런데 '리액트 쓸거면 Nginx 올려라' 는 강사의 조언을 받고 배포를 한 경험이 있다.
이번에는 why 를 생각하며 다시 그 의미를 되새김해보자.
 
웹서버와 WAS에 대해서 정리를 해놓았으니 아직 개념이 모호한 독자라면 (내가 그랬어서 ^^;) 이를 읽은 이후 본 글을 읽어내려가는 것을 추천한다.
 
 
 
https://biz-ninza.tistory.com/12

 

웹서버와 WAS

웹 서버와 WAS (Web Application Server) 1. 웹서버 웹이란 인터넷을 기반으로 한 정보를 공유 검색할 수 있게 하는 서비스.구성요소에는 URL(주소), HTTP(통신규칙), HTML(내용) 이 있겠다. 서버란 클라이언트

biz-ninza.tistory.com

 
 
 

목차

1. 아파치서버와 Nginx 의 대결?

 

1) Apache HTTP Server - 1995 등장
2) Nginx - 2004 등장

 

2. Nginx 가 뭐가 좋은건데?

 

1) Nginx 는 비동기 이벤트 기반 아키텍처
2) Nginx 의 단점
3) Nginx 의 장점 - 그래도 1등에는 그만한 이유가 있다.

 

3. Nginx 썼습니다.

 
 
 
 
 

1. 아파치서버와 Nginx 의 대결?

 

2023년 기준 Nginx 웹서버가 아파치서버를 제쳤다 (https://nginxstore.com)

 
 

1) Apache HTTP Server - 1995 등장

 
아파치 서버는 멀티 프로세스 기반으로 요청이 들어오면 connection 을 형성하기 위해 프로세스를 생성한다.
(UNIX 계열의 OS가 네트워크 커넥션을 형성하는 모델을 차용)
 
요청마다 독립적인 프로세스를 생성함으로써 격리된 환경에서 서버가 실행되도록 한다는 의미다. 따라서 안정성과 보안측면에서 유리하였으나 메모리와 CPU 자원을 많이 사용하면서 부하가 높은 환경에서는 감당하기 어려웠다.
 
게다가 아파치 서버는 요청이 들어오기 전에 프로세스를 미리 만들어 놓는 prefork 방식을 사용했다.
새로운 클라이언트로부터 요청이 들어오면 미리 만들어 놓은 프로세스를 가져다 사용하는 것으로 초기화 시간을 줄일 수 있다.
 
하지만 역시 요청마다 별도의 프로세스를 차지한다는 근본적인 문제는 해결하지 못했고 요청이 많이 들어올 때 메모리 사용량이 급격히 증가했다. 이 문제는 2008년 경 스마트폰이 대중화되면서 대규모 트레픽을 받아야하는 IT기업입장에서는 심각한 고민거리가 되었다.
 
 
하지만 아파치 서버가 가진 장점도 분명히 존재한다.
 
다양한 모듈을 만들어서 서버에 빠르게 기능을 추가할 수 있다. 
즉, HTTPS 통신을 위한 SSL/TLS 암호화(mod_ssl), 정적리소스나 DB쿼리결과의 캐싱(mod_cache), 인증(mod_auth) 등의 기능을 플러그인 형태로 유연하게 확장할 수 있다.
 
또 기본적으로 아파치 서버는 WAS 이므로 동적 컨텐츠도 처리가 가능하다. 다시말해 요청을 받고 응답을 처리하는 과정을 하나의 서버에서 처리할 수 있다. 그래서 위에서 언급했든 안정성과 보안성이 높다.
 
그러나 멀티 프로세스 기반으로 메모리 소모가 많고 무엇보다 수많은 동시 connection 을 감당하기 어렵다는 것은 계속되는 골치아픈 문제였다. 

 

(새로운 연결이 생길 때 프로세스 생성 비용, 컨텍스트 스위칭 비용이 커서 한 서버에서 동시연결이 1만개 이상되면 이를 감당하기 힘들어 서버가 다운되거나, 새로운 연결을 거부하거나 응답이 지연될 수 있다. 이를 C10k 문제라고 한다)

 

이를 해결하기위한 소프트웨어가 nginx 다.
 
 

2) Nginx - 2004 등장

 

https://hostingcanada.org/nginx-vs-apache-explained/

 
 
아파치가 수많은 동시 커넥션을 감당하기 어렵다면 아파치 서버 앞단에 동시 커넥션을 대신해서 유지할 미들서버를 하나 둔다면 어떨까?
 
초창기에는 아파치 서버와 함께 사용하기 위해 만들었다.
즉 Apache 의 한계를 극복하기 위해 설계된 것이 nginx다.
 
Nginx 는 웹서버이므로 정적파일 요청은 스스로 처리할 수 있다. 
클라이언트로부터 동적파일에 대한 요청을 받았을때만 뒤에 있는 서버와 connection을 형성한다.
즉 아파치 서버의 리소스를 커넥션 유지가 아닌 로직처리에만 사용하게 된다.
 
 
이 때 Nginx 는 동시 커넥션 처리를 극대화하기 위해 이벤트 기반의 아키텍쳐를 도입한다.
 
요청할 때마다 새로운 프로세스를 생성하는 방식이 아닌, 하나의 프로세스에서 다수의 연결을 처리할 수 있는 비동기 이벤트 루프 방식을 채택했다.
 
단일 스레드 또는 워커 프로세스가 비동기 I/O를 통해 수많은 클라이언트 요청을 동시에 처리하는 것이다. 요청당 프로세스나 스레드를 생성하지 않으니 메모리나 CPU 사용량을 획기적으로 낮출 수 있다.
 
 
 

2. Nginx 가 뭐가 좋은건데?

 

1) Nginx 는 비동기 이벤트 기반 아키텍처

 
Nginx는 이벤트루프와 비동기 I/O를 활용하여 하나의 스레드에서 여러 연결을 효율적으로 처리한다.
조금 더 자세히 알아보자.
 
Nginx 에는 설정 파일을 읽고 전체 서버를 관리하는 마스터 프로세스가 있다.
그리고 실제 클라이언트의 요청을 처리하는 워커 프로세스가 존재한다.
 
워커프로세스는 단일 스레드로 동작한다.
 
 
워커 프로세스가 만들어질 때 listen 소켓을 배정하고 그 소켓에 새로운 클라이언트로부터 요청이 들어오면 커넥션을 형성하고 그 요청을 처리하게 된다. (keep-alive 시간 만큼 유지)
 
한편 워커 프로세스는 해당 커넥션에서 요청이 없으면 새로운 커넥을 만들거나 다른 커넥션의 새로운 요청을 처리하는데 이렇게 커넥션을 형성,제거,새로운 요청을 처리하는 것을 각각 이벤트라고 부른다.
 
OS커널이 큐 형식으로 이벤트들을 워커프로세스에게 전달한다.
이때 이벤트는 큐에 담긴 상태에서 워커 프로세스가 처리할 때까지 비동기 방식으로 대기한다.
 
이것이 '비동기 이벤트 기반 아키텍쳐' 다.
 
 
아파치 서버가 요청당 프로세스를 생성하는 방식이었다면 하나의 워커 프로세스를 뽑아 먹을 때까지 뽑아먹어서(?) 적은 리소스로 많은 요청을 받게 하는 것이다. (일반적으로 1워커 프로세스당 10만 개 이상의 동시 커넥션을 처리)
 
워커 프로세스가 실제 요청을 처리하는 친구들로 보통 cpu 코어개수 만큼만 생성한다. 아파치 서버와 다르게 cpu 를 왔다갔다 하면서 컨텍스트 스위칭 비용을 줄인다.
 
* 컨텍스트 스위칭
: CPU가 스레드나 프로세스의 실행상태를 저장하고 다른 작업으로 전환하는 과정. 현재 실행중인 프로세스의 레지스터 상태, 프로그램 카운터, 스택 포인터들을 저장해놓고 복원하는 작업이 리소스가 많이 들고 nginx 에서는 이 작업에 대한 비용을 없애기 위해 워커프로세스를 cpu 코어 개수만큼만 분배하는 것
 
 
게다가 디스크 I/O 와 같이 시간이 오래걸리는 작업은 스레드풀에 위임한다. 그리고 그동안 다른 요청을 처리한다. 
대기 시간을 감소시키며 효율적인 리소스활용이 가능한 것이다.
 
이러한 아키텍쳐로 아파치서버에 비해 더욱 쉴새없이 효율적으로 처리할 수 있는 것이다.

 

 
즉 동시 커넥션양이 100~1000배 증가. 동일한 커넥션일때도 속도 2배이상이다.

 

핵심은 '동시' 이다.

단일 요청에서의 트래픽 처리 성능은 크게 차이가 안나지만 동시연결이 많아지면 Nginx 가 압도적인 성능을 보인다.
 
 

2) Nginx 의 단점

 
그렇다고 Nginx 가 만능해결수단은 아니다.
 
nginx 는 I/O 중심의 작업에 최적화되어 있다. 다시말해 이미지 처리, 데이터 암호화 같이 cpu를 많이 사용하여야하는 작업은 워커 프로세스를 과부하 상태로 만들고 이 경우 멀티 스레드 기반 서버가 더 적합할 수 있다.

 

단일 스레드에서 실행된다는 점이 컨텍스트 스위칭 비용을 아끼긴 하지만 cpu 코어를 모두 활용하지 못한다는 단점으로 작용할 수 있다는 뜻이다. 따라서 고성능을 위해 Nginx 는 워커 프로세스를 CPU 개수만큼 고정시키는 것.

 

게다가 에러처리를 잘못하면 


소규모 트래픽 환경에서는 설정이 간단한 톰캣이 더 간단한 대안이 될 수도 있다
(고 하는데 아직 이 부분은 와닿지 않음. 설명가능하신 분은 댓글로...🙏 )
 
 


3)
Nginx 의 장점 - 그래도 1등에는 그만한 이유가 있다.

 
- 리버스 프록시 (로드 밸런싱)
 
nginx 는 비동기 아키텍쳐로 트래픽 자체를 분산해서 효율적 처리할 뿐 아니라 동시 커넥션을 유지한 채 뒷단 (아파치서버)에 로드밸런싱하는 알고리즘을 지원한다.
 
즉 다수의 애플리케이션 서버 앞단에 위치하여 트래픽을 받고 동적 컨텐츠가 필요한 경우 WAS 에 쫙 분산하는 용도로 쓸 때 탁월하다.
 
한편, 리버스 프록시란 클라이언트 요청을 받아서 실제 서버(백엔드 서버)에 전달하는 서버를 의미한다

 

클라이언트가 웹 서버에 요청을 보내면 리버스 프록시는 요청을 받아 백엔드 서버에 전달하고 백엔드 서버에서 처리한 응답을 리버스 프록시가 클라이언트로 반환하는 과정을 의미한다. 단일인입점 역할을 해주는 것이다.

 

* '리버스' 프록시라는 말의 의미

보통 Proxy 라 하면 Client 요청을 전달하는 대리자를 의미하는데 Forward 프록시는 클라이언트 대신 서버에 직접 요청을 보내는 대리인 역할을 하므로 Forward, Reverse 프록시는 클라이언트에서 요청을 받고 다시 응답을 전달해주므로 서버 측 대리인 역할을 한다는 의미에서 Reverse 가 붙었다. 

(VPN이 대표적인 포워드 프록시다.브라우저가 VPN 을 통해 요청을 보내면 VPN 서버가 요청을 대신 전달하며 실제 서버는 클라이언트가 아닌 VPN 서버를 요청 출처로 인식한다)

 
그러다보니 이 과정에서 실제 서버의 IP 주소를 숨기고 방화벽 역할을 수행하거나 SSL/TLS 암호화 처리를 대행하는 보안역할도 해줄 수 있다. 클라이언트에게 HTTPS 를 받아 WAS 와는 HTTP 통신을 하는 프로토콜 변환 역할을 수행하는 것이다. 복호화 과정을 대신 처리해준다. (보통 웹서버와 WAS 는 private subnet 에 묶여있으니 HTTP 통신이 문제가 되지 않을 것이다)
 
WAS 가 하는 역할을 대행해준다. WAS의 부담이 확! 줄어든다.
WAS는 비지니스 로직 처리에만 집중할 수 있다.
 
 
- 고트래픽 환경의 정적 컨텐츠 제공
 
Nginx 서버로부터 받은 응답을 스스로 보관해서 클라이언트에게 돌려주는 캐싱역할도 한다.
유저 경험도 좋아진다.
 
좀 더 풀어서 얘기하면 이미지, 비디오 호스팅 같은 경우에는 정적파일을 캐싱하며, 기본적으로 요청에 대한 응답 즉 I/O에 최적화되어있다보니 유저에게 빠른 컨텐츠를 제공할수 있다.

(Nginx는 파일 읽기 등 I/O 작업을 대기하지않고 비동기적으로 ~에 위임해 놓고 다른 요청을 처리하다보니, 하나의 요청이 I/O 대기상태일 때 스레드가 블로킹되어 다른 요청을 처리못하는 아파치서버보다 효율이 증가하는 것. 게다가 스레드/프로세스 생성 혹은 스위칭 비용도 없다)
 
또 동시커넥션을 유지할 수 있으니 스트리밍 등의 실시간 데이터 전송에 매우 유리하다. 
 
 
- 추가 공부 키워드

HSTS, CORS처리, TCP/UDP 커넥션 부하분산, HTTP/2 지원 등 많은 방식으로 서버를 '지원' 한다고 함
 
 
 

3. Nginx 썼습니다.




이러한 이유로 2007년 까진 아파치가 1위였다가 2008년 스마트폰 나오면서 tcp 동시 커넥션이 많아지면서 nginx가 각광받기 시작하였고 2023년 이후로는 Nginx 가 웹서버 점유율 1위를 차지함.
 
Nginx 는 로직 자체의 최적화는 물론이고 파일을 메모리에 캐싱하는 등 설계 철학 자체가 고성능과 경량성을 목표로 개발되어 있어서 정적컨텐츠를 제공하는 속도가 아파치보다 훨씬 빠르다.
 
우리 교육플랫폼에서는 사실상 인강이 메인 컨텐츠가 될 것이다.
자세한 것은 말하기 곤란하지만 학생들이 미리 인강과 자료를 통해 학습한 이후 강사들이 수업을 진행하는 Flip-learning 이 메인 컨셉이기 때문이다.
 
결국 유저들의 대부분 체류시간과 서비스 경험이 이 곳에서 일어날 것이고 고품질의 비디오를 빠르게 서빙하기 위해선 Nginx 를 쓰는게 합리적이다.
 
게다가 앞으로 우리플랫폼은 J커브를 그리며 성장할 것이기 때문에 유저 요청과 응답을 효율적으로 처리하고 뒷단 WAS 의 부담을 줄일 수 있는 Nginx 가 필수적으로 보인다 :) 

 

  // Actions.yml 일부
  
  dockerize-frontend:
    name: 프론트엔드 도커 이미지화 및 Docker Hub 푸시
    # needs: build-frontend # 프론트엔드 빌드 후 실행
    runs-on: ubuntu-latest
    steps:
      - name: 레포지토리 코드 가져오기
        uses: actions/checkout@v3

      - name: 현재 폴더경로 조회
        run: ls

      - name: Docker 이미지 빌드하기
        run: docker build -t frontend-app:latest ./frontend

      - name: Docker Hub 로그인하기
        run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin

      - name: 이미지에 태그 붙여 Docker Hub에 푸시
        run: |
          docker tag frontend-app:latest ${{ secrets.DOCKER_USERNAME }}/frontend-app:latest
          docker push ${{ secrets.DOCKER_USERNAME }}/frontend-app:latest

github 의 main 브렌치에 푸시가 되면 이 레포지토리를 불러와 리액트프로젝트를 빌드하며 도커허브에 도커 이미지를 저장한다.
 
빌드 과정에서 아래의 도커파일과 nginx 설정파일을 사용한다.

// 도커파일

# Node.js 기반 이미지로 React 앱 빌드
FROM node:18 AS build
LABEL authors="jin66"

# 작업공간
RUN mkdir /app
WORKDIR /app

COPY package.json ./
COPY package-lock.json ./
RUN npm install

# 애플리케이션 소스 코드 복사
COPY . ./

# React 애플리케이션 빌드
RUN npm run build

# Nginx로 제공
FROM nginx:1.23

# 빌드된 파일을 Nginx로 복사
COPY --from=build /app/build /usr/share/nginx/html

# Nginx 설정 파일 복사 
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Nginx 컨테이너 실행
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# ENTRYPOINT ["top", "-b"]

 

// nginx.conf

# Nginx 서버 블록 설정
server {
    # Nginx가 요청을 수신할 포트를 지정 (HTTP 기본 포트: 80)
    listen 80;

    # 처리할 도메인 이름 또는 호스트 이름 설정 (현재는 로컬호스트)
    server_name 52.78.55.86;

    # 요청된 파일이 위치하는 기본 디렉터리를 설정
    root /usr/share/nginx/html;

    # 기본 페이지로 사용할 파일 이름을 지정
    index index.html;

    # 루트 경로(`/`)에 대한 요청 처리
    location / {
        # 요청된 파일이 존재하면 반환하고, 없으면 index.html 반환
        try_files $uri /index.html;
    }
}

 
 
 - 추후 설정파일에 대한 내용을 추가 기술할 예정

 

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# ENTRYPOINT ["top", "-b"] 
 
등등

 

 

 

(25.1.28 추가)

 

* node.js 도 Nginx 와 마찬가지로 단일 스레드와 비동기 이벤트 루프를 사용하며 I/O 작업을 논블로킹 방식으로 한다. Nginx 가 워커프로세스 수를 CPU 수만큼 고정하듯이 Node.js 도 클러스터 모드를 설정해 다중 프로세스 아키텍쳐를 만들 수 있다.

다만 Nginx 는 주로 리버스 프록시 역할을 하며 Node.js 는 서버의 애플리케이션을 실행하는데 사용한다는 점에서 차이가 있겠다.

 

* Nginx 가 아파치보다 무조건 좋다는 의미의 글은 아니다는 걸 다시 짚고 넘어감. 아파치도 MPM 에 따라 prefork(프로세스당 1스레드), worker(1프로세스당 여러 스레드), event (비동기 I/O와 멀티스레드) 등 다양한 설정을 할 수 있다. 

다시말해 prefork, worker 에서는 블로킹 I/O를 사용하지만 Event 로 설정하면 비동기 I/O 방식으로 동작시킬 수 있다.

다만, 아파치의 event MPM 은 나중에 추가된 기능이라 구조적으로 완전한 비동기 설계가 아니라 성능의 한계가 있을뿐

 

 

(25.3.27 추가)

 

단순히 정적 React 앱 배포라면 S3 를 쓸수도 있다. 빌드된 정적파일들을 S3 버킷에 올려놓으면 S3가 웹서버역할을 하며 호스팅한다. 따로 웹서버를 운영하지 않아도 된다. 서버리스라는 것은 운영비용이 낮다는 것을 의미함. 게다가 S3는 트래픽에 따라 자동으로 확장됨. 다만, EC2 + Nginx 를 쓸 경우 세밀한 서버 구성이 가능