1. 서론
웹 지식을 공부하다보면 웹 서버와 웹 어플리케이션 서버(WAS)에 대한 언급이 심심치 않게 나온다. 필자는 웹 서비스 아키텍처에 대한 제대로된 개념이 확립되지 않았을 시기, 이러한 용어들이 직관적으로 와닿지 않았다. 따라서 웹 서버와 WAS가 "실제로" 어떤 형태로 존재하고 쓰이는지 정확하게 인지할 수 없었다. 하지만 프로젝트의 개발과 배포 과정을 몸소 거치며 실제 아키텍처를 피부로 느끼는 과정에서 해당하는 개념들을 나름대로 정확히 이해할 수 있게 되었다. 이번 게시에서 웹 서버와 WAS 및 Nginx 대한 설명을 기록하며 스스로는 물론이고 보는 이로 하여금 해당 개념에 대한 직관적인 이해를 제시하려 한다.
2. 웹 서비스 아키텍처
먼저 간단하게 설명하자면, 웹 서버는 정적인 컨텐츠(HTML문서, CSS, 이미지, 파일 등)를 제공하고, WAS는 DB와 소통하며 상황에 따라 변하는 동적인 컨텐츠를 제공한다. 여기서 웹 서버와 WAS에 대해 설명을 덧붙이기 전에 전체적인 웹 서비스의 구조부터 살펴보고자 한다. 쓰임새를 알아야 이해가 쉽다. 웹 서비스는 다음과 같이 여러가지의 구조를 가질 수 있다.
1. Client - Web Server
2. Client - WAS - DB
3. Client - Web Server - WAS - DB
1번의 구조는 웹 서버로만 구성되어 정적 컨텐츠를 응답할 수 있다.
2번의 구조는 WAS가 웹 서버의 역할까지 수행하는 구조이다. 즉, WAS가 동적, 정적 컨텐츠를 모두 제공하는 구조이다.
3번의 구조는 웹 서버가 정적 컨텐츠를 제공하고, 동적 컨텐츠의 경우에는 웹 서버가 WAS에 요청하여 제공받는 형식이다.
위 세가지 구조 중 가장 안정적인 구조는 3번 구조이다.
1번의 경우, 웹 서버로만 구성되어 있으므로 동적인 컨텐츠를 유연하게 제공할 수 없다.
그러면 2번과 3번은 웹 서버와 WAS를 분리하느냐 아니냐의 차이인데, 분리해서 얻을 수 있는 이점은 크게 다음과 같다.
- 서버 부하 방지 : 웹 서버는 정적 컨텐츠 제공, WAS는 동적 컨텐츠 제공으로 기능적 분류를 실현함으로써 효과적인 분산을 유도해 WAS의 과부하를 방지할 수 있다. 동적 컨텐츠 제공 시 비즈니스 로직과 DB 조회와 같은 작업만해도 이미 많은 컴퓨팅 자원을 요구하기 때문에 각각의 역할에 특화된 서버로 구성할 필요성이 생긴다.
- 확장성 : 바로 위에 상기한 이유와 연결되는데, 기능적으로 분류함에 따라 컴퓨팅 자원을 많이 요구하는 WAS를 필요에 때라 수평적으로 확장하여 부하 분산을 효과적으로 처리할 수 있게 한다.(Load Balancing)
- 보안성 : 2번과 같이 클라이언트가 바로 WAS에 접근하려면 WAS는 외부 통신이 되는 구간에 위치해야 한다. 이 경우 WAS 내에서 뒷단의 DB에 대한 정보가 훨씬 노출되기 쉬워진다. 하지만 3번과 같이 WAS, DB를 내부망에 두고 웹 서버만 외부 망에 두게 된다면, 클라이언트는 웹 서버에만 접근할 수 있게 되므로 2번의 구성보다는 훨씬 안전한 구성이 된다. 아울러 웹 서버와 WAS가 분리되어 있으므로 각각의 서버의 보안에 대한 전문적인 대응이 가능하고, 관리가 쉬워진다.
따라서 웹 서비스는 3번과 같은 구조를 지향해야하며, 앞으로 후술할 내용에 대해 이를 기준으로 웹 서버와 WAS를 설명하겠다.
3. Web Server와 WAS
- Web Server는 클라이언트로부터 HTTP 요청을 받으면, HTML 문서나 파일과 같은 정적인 컨텐츠를 제공한다. 이때 동적인 요청이 들어올 경우, 이를 WAS에게 전달하여 응답받고 다시 클라이언트에게 제공한다.
- WAS는 웹 어플리케이션을 실행하여 다양한 비즈니스 로직과 DB 조회와 같은 기능을 수행하고 해당하는 결과를 웹 서버에게 전달한다. 이를 통해 클라이언트는 정적인 데이터가 아닌 상황에 따라 달라지는 동적인 데이터를 전달받을 수 있게 되는 것이다.
하지만 생각해보면 웹 서버나 WAS는 그 자체만으로는 껍데기에 불과하다. 우리가 실제로 프로젝트를 구성하는데 가장 많이 차지하는 부분은 프로그래밍 언어이다. 예를 들어 프론트로 React, 백엔드로 Spring boot를 활용해서 프로젝트를 구성한다는 말이다. 하지만 해당 모듈들로만 사용자들에게 컨텐츠를 제공할 수 있었는가? 아니다. 서버가 필요하다. 즉, React나 Spring boot를 "호스팅"해줄 서버가 필요하다. 따라서 프론트엔드의 경우 React를 호스팅하는 서버로 웹 서버를 사용하고, 백엔드의 경우 Spring boot를 호스팅하는 서버로 WAS를 사용하는 것이다. Spring boot의 경우 WAS로 사용하는 톰캣이 그 예시이다. 우리가 React나 Spring boot 어플리케이션을 만들면, 이들을 감싸고 외부로부터 요청을 받을 수 있게 하여 이들을 구동시키는 주체가 웹 서버와 WAS인 것이다. 웹 서버의 안에 React 프로젝트, WAS의 안에 Spring boot 프로젝트가 들어가는 것이라고 생각하면 쉽다.
정리하자면, 전술한 웹 서비스 아키텍처를 봤을 때 보통 프론트엔드에서 사용되는 것이 웹 서버이고, 백엔드에서 사용되는 것이 WAS이다. 이에 대한 한 예시로 웹 서버에 React 프로젝트를 담아 프론트엔드를 구성하고, WAS에 Spring boot 프로젝트를 담아 백엔드를 구성할 수 있는 것이다.
4. Nginx
WAS의 한 대표적인 예시로는 톰캣이 있다. 그렇다면 웹 서버의 대표적인 예시로는 누가 있는가?
바로 Nginx이다. Nginx는 가볍고 빠르게 정적 콘텐츠를 제공하는 경량 웹서버로 쓰인다.
- Nginx는 같은 웹 서버인 Apache와 다르게 고정된 프로세스만 생성해서 사용하고, 비동기 방식으로 요청들을 동시에 처리한다.
- 즉, 새로운 요청이 들어오더라도 새로운 프로세스 내지는 쓰레드를 생성하지 않기 때문에 이에 대한 생성 비용이 존재하지 않아 적은 자원으로도 효율적인 운용을 할 수 있다.
- 따라서 이러한 특성을 가지는 Nginx의 구조 상 단일 서버에서도 동시에 많은 연결을 처리할 수 있다.
Nginx의 주요 구성 요소
- 마스터 프로세스 : Nginx의 설정 파일을 읽고, 유효성을 검사한다. 또한 워커 프로세스를 관리한다.
- 워커 프로세스 : 클라이언트로부터 오는 실제 요청을 처리하고 이에 대한 응답을 반환한다. 여러 개의 워커 프로세스가 병렬로 동작하며, 프로세스 개수는 설정 파일에서 정의되거나 사용 가능한 CPU 코어 숫자에 맞게 자동으로 조절된다.(CPU의 코어 개수와 동일했을 때가 최고의 효율을 가지므로 권장된다.) 각 워커 프로세스는 단일 스레드로 동작하며, 독립적으로 동작한다. 각 프로세스는 공유 리소스에 대해 공유 메모리를 사용하며 통신할 수 있다.
- 설정 파일 : 각종 디렉토리에 나누어 저장되며, Nginx 동작 방식 및 동작에 대한 설정이 담긴다.
Nginx 관련 디렉토리 구조
- /etc/nginx : Nginx 설정 파일이 위치한다.
- /etc/nginx/nginx.conf : Nginx의 주요 설정 파일이며, 전체적인 서버의 동작 방식에 대한 설정이 담겨 있다.
- /etc/nginx/sites-available : 가상 호스트 설정 파일이 위치한다. 이 위치에 설정 파일을 작성하며, /etc/nginx/sites-enabled 위치에 심볼릭 링크를 생성하여 해당 설정을 활성화시킬 수 있다.
- /var/log/nginx : Nginx의 로그 파일들이 위치한다. 주요 로그 파일인 접속 로그(access.log)와 에러 로그(error.log)가 있다.
- /usr/share/nginx/html : Nginx가 웹 서버로 동작할 때 제공하는 정적 컨텐츠가 위치한다.
- /etc/nginx/conf.d : 추가적인 설정 파일들이 위치하며, 서버 관련 설정을 추가로 만들어 포함시킬 수 있다.
Nginx 동작
- Nginx가 실행되면 마스터 프로세스가 생성되며, 마스터 프로세스는 설정 파일을 읽고 이에 따라 정해진 개수의 워커 프로세스를 생성한다.
- 워커 프로세스는 생성될 때 내부에 listen 소켓을 배정받으며, 요청 받을 시 이 소켓에 커넥션을 형성하고 처리한다.
- 커넥션은 여러 개로 구성될 수 있고, 정해진 시간만큼 유지된다.
- 커넥션 형성/제거 및 새로운 요청 처리하는 작업을 Event라고 하는데, Event는 큐에 담기고 워커 프로세스가 이를 처리할 때까지 비동기 방식으로 대기한다.
- 워커 프로세스는 하나의 스레드로 Event를 큐에서 꺼내 처리한다.
- 만약 작업 시간이 길어지는 작업이 있을 경우 쓰레드 풀에게 해당 작업을 위임하고 큐안의 다른 작업을 처리한다.
- 쓰레드 풀은 시간이 오래걸리는 요청 처리를 위해 만들어진 개념이다. 쓰레드의 생성 비용은 비싸므로 필요할 때 편하게 늘릴 수 있도록 쓰레드를 미리 만들어놓고 필요한 작업에 할당하게 된다.
- 각 워커 프로세스들은 Disk I/O처럼 오래 걸리는 작업을 감지하고 이를 처리하기 위한 역할을 하는 쓰레드 풀에게 처리를 위임하는 것이다.
위와 같이 Nginx는, 자동으로 내지는 정해진 순서에 따라 작업을 처리하는 것이 아닌 어떤 "일(Event)에 대한 반응"으로 작업 처리가 발생하는 Event-Driven 구조로 동작한다. 이에 더해 워커 프로세스가 CPU의 코어 개수만큼 생성되므로 프로세스를 전환하는 Context Switching 현상이 줄어들기 때문에 CPU 사용률이 낮다. 따라서 이러한 구조로 인해 트래픽이 많이 발생하는 동시 커넥션이 중요한 환경에서 Nginx는 강력한 성능을 발휘한다.
5. /etc/nginx/nginx.conf(주석 부분 제외, 초기 값)
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
}
http {
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
- user www-data; : Nginx의 워커 프로세스가 실행되는 권한이다. www-data는 Ubuntu의 웹 서버(Apache, Nginx 등)가 기본적으로 일반적인 작업에 사용하는 사용자이다. 웹 서버 프로세스는 www-data 파일에 액세스 할 수 있다. 이는 리눅스의 권한과 보안에 관련이 있다. 만약 해커에 의해 프로그램의 권한을 뺏길 경우, 해커는 시스템에서 www-data가 할 수 있는 일만 할 수 있다. 이를 반대로 말하면 권한을 root로 설정해 놓았을 때 해커는 root 권한으로 시스템을 주무를 수 있게 되므로, 보안상 적절한 권한을 프로그램에 할당하는 것이 좋다.
- worker_processes auto; : 워커 프로세스의 개수를 설정한다. auto는 자동으로 CPU 코어의 개수만큼 워커 프로세스를 생성하게 한다.
- pid /run/nginx.pid; : 해당하는 파일에 Nginx 마스터 프로세스 ID가 저장된다.
- events 블록의 worker_connections 768; : 워커 프로세스가 동시에 처리할 수있는 접속자의 수이다.
- http 블록 : HTTP 통신과 관련된 모듈의 지시어와 블록을 정의한다. 하위 블록으로 server와 location을 가지며 설정 값들이 하위 블록에 상속되며 적용된다.
- sendfile on; : kernel file buffer를 사용할 지에 대한 설정이다. user-space의 buffer 메모리를 사용하지 않는 방식이므로 빠른 성능이 보장된다.
- tcp_nopush on; : tcp_nopush 옵션이 활성화되면 HTTP 헤더와 본문이 함께 전송될 수 있도록 TCP 패킷을 관리한다. 헤더와 본문이 하나의 패킷에 효율적으로 포함되면 네트워트 효율을 높일 수 있으며, 정적 파일을 전송할 때 유리하다. sendfile 옵션이 활성화되었을 때만 가능하다.
- types_hash_max_size 2048; : Hash Table의 최대 크기이다.
- include /etc/nginx/mime.types; : mime.types(인터넷에 전달할 파일 포맷과 포맷 컨텐츠를 위한 식별자) 파일에 작성된 내용을 현재 설정 파일에 가져온다.
- default_type application/octet-stream; : 웹 서버의 기본 content-type을 정의한다.
- ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; : 사용할 SSL 프로토콜 버전에 대한 설정이다.
- ssl_prefer_server_ciphers on; : SSL-TLS 협상 과정에서 서버에 설정된 암호 알고리즘을 우선하게 하는 설정이다.
- access_log /var/log/nginx/access.log; : 접속 로그 기록에 대한 경로이다.
- error_log /var/log/nginx/error.log; : 에러 로그 기록에 대한 경로이다.
- gzip on; : 응답 데이터를 gzip으로 압축하여 보내는 것에 대한 설정이다. 이 옵션을 사용하면 네트워크 사용량을 줄여 응답 시간을 단축시킬 수 있다.
- include /etc/nginx/conf.d/*.conf; : *.conf와 같은 패턴의 파일명을 가지는 추가 설정 파일을 모두 포함시킨다.
- include /etc/nginx/sites-enabled/*; : 해당 경로에서 가상 호스팅과 관련된 설정 파일을 모두 포함시킨다.
6. /etc/nginx/site-enabled/default
Nginx를 처음 설치하면 /etc/nginx/sites-enabled 디렉토리에 심볼릭 링크가 걸린 default 파일이 이미 존재한다. 우리는 이 파일을 이해해야 Nginx가 웹 서버로 동작하는 원리를 파악할 수 있다.
default (주석 제외, 초기 값)
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html;
index index.html index.htm index.nginx-debian.html;
server_name _;
location / {
try_files $uri $uri/ =404;
}
}
- listen 80 default_server; : IPv4에 대한 listen이다. 해당하는 포트로 들어오는 요청을 처리한다는 의미이다.
- listen [::]:80 default_server; : IPv6에 대한 listen이다.
- root /var/www/html; : 루트 디렉토리를 설정한다. 이 server 블록의 웹 컨텐츠 경로를 설정하는 것이다. 다시 말해 컨텐츠를 제공할 때 서버의 어떤 경로에서 파일을 찾는지를 제시한다.
- index index.html index.htm index.nginx-debian.html; : 요청받은 URL에 더는 참고할 경로 정보가 없을 시 Nginx가 사용할 기본 파일 목록이다. 설정된 루트 디렉토리 기준이다.
- server_name _; : 이 서버 블록의 모든 호스트 이름을 수락한다. 즉, 후일 _ 대신에 우리가 사용하는 도메인 네임이 여기에 들어가면 된다.
- location / {
try_files $uri $uri/ =404;
}
: /의 URI 값으로 요청이 들어올 경우, 지정된 루트 디렉토리 경로(/var/www/html)에서 index로 지정된 파일을 찾는다. try_files 구문을 이용하여 $uri(/var/www/html/<index 파일>)을 먼저 찾고, 만약 없다면 $uri/(/var/www/html/<index 파일>/)을 찾는다. 이것도 없다면 404 error를 띄운다.
root@ip-xxx-xx-xx-xx:/# ls /var/www/html/
index.nginx-debian.html
실제로 해당하는 경로에 index.nginx-debian.html가 존재하고 있음을 알 수 있다. 따라서 우리가 Nginx가 작동 중인 서버에 대해 "http://<해당 서버 IP>"의 주소로 접근을 하게 되면 위 default 설정 파일에 의해 index.nginx-debian.html(테스트 페이지)의 문서를 응답 받을 수 있다. 즉, nginx.conf 파일에서 sites-enabled 디렉토리에 존재하는 설정 파일(default)을 포함하고 default 설정 파일에 명시된 논리대로 정적 컨텐츠(HTML 문서)가 제공되는 것이다. 이처럼 이러한 설정들을 커스터마이징하며 우리는 Nginx를 웹 서버로 활용할 수 있다.
7. Nginx - Reverse Proxy Server
Nginx는 웹 서버말고도 리버스 프록시 서버로 활용이 가능하다.
먼저 프록시 서버는 간단하게 통신을 중계하는 서버라고 생각하면 된다. 프록시 서버에는 포워드 프록시 서버와 리버스 프록시 서버가 존재한다.
포워드 프록시 서버
- 포워드 프록시 서버는 클라이언트와 인터넷 사이에 존재하며 클라이언트가 정보 요청 시 이 요청을 대신 받아 서버에게 전달한다.
- 클라이언트의 IP 주소가 웹 서버에 노출되지 않으므로 클라이언트의 위치나 신원을 식별하는 것이 어려워져 보안을 강화할 수 있다.
- 또한 클라이언트에 대한 접근 제어를 수행해서 특정 IP 내지는 URL에 대한 접근을 제한시켜 버릴 수 있다.
- 미디어 파일을 캐싱해 클라이언트에게 빠르게 제공할 수 있도록 사용할 수도 있다.
리버스 프록시 서버
- 리버스 프록시 서버는 인터넷과 서버 사이에 존재하며 실제 서버 앞 단에서 클라이언트의 요청을 먼저 받아서 동작한다.
- 포워드 프록시는 클라이언트 측에 존재하는 반면, 리버스 프록시는 서버 측에 위치하며 사용자의 요청을 설정에 맞게 적합한 서버에게 전달한다.
- 요청을 서버 앞 단에서 먼저 받기 때문에 보안적으로 유리한 구조를 가지며, 암호화나 서버 공격을 대신 받음으로써 서버들을 보호할 수 있다.
- 이에 더해 사용자의 요청들을 적절하게 분배하여 서버의 과부하를 방지하고 부하를 분산하는 로드밸런싱의 기능도 수행할 수 있다.
따라서 전술한 Nginx 서버 설정 파일들을 통해 Nginx가 리버스 프록시 서버 및 로드 밸런서로 동작하도록 만들 수 있다. 이는 추후 프로젝트와 관련해서 Nginx를 다뤘던 내용을 통해 자세히 알아볼 것이다.
References
- https://code-lab1.tistory.com/199
- https://cocomodo.tistory.com/259
- https://lilo.tistory.com/57
- https://velog.io/@bky373/Web-%EC%9B%B9-%EC%84%9C%EB%B2%84%EC%99%80-WAS
- https://velog.io/@gusbms0627/nginx-%EB%9E%80-%ED%95%84%EC%9A%94%EC%84%B1-%EA%B5%AC%EC%A1%B0-%EB%8F%99%EC%9E%91%EC%9B%90%EB%A6%AC
- https://medium.com/@jina-dev/nginx-%EA%B8%B0%EB%B3%B8%EC%84%A4%EC%A0%95-fa06e7ef612d
- https://lu-coding.tistory.com/101
- https://sihyung92.oopy.io/server/nginx_feat_apache
- https://blog.naver.com/gi_balja/223028077537