애플리케이션 서버
애플리케이션 코드를 대신 실행해 주는 서버입니다. 요청이 들어오면 그 코드를 부르고, 코드가 만든 결과를 응답으로 내보냅니다. 미리 만들어 둔 파일을 찾아 내보내는 일과는 다릅니다.
상세
관공서 창구를 떠올리면 쉽습니다. 이미 만들어져 서랍에 든 서류를 달라고 하면 창구 직원이 그 자리에서 꺼내 줍니다. 반대로 지금 계산해야 답이 나오는 것을 물으면 창구 직원은 안쪽 담당자에게 넘깁니다. 애플리케이션 서버는 그 안쪽 담당자 자리입니다.
애플리케이션 서버는 애플리케이션 코드를 담아 두고, 요청이 올 때마다 그 코드를 실행해 응답을 만드는 서버 소프트웨어입니다. 여기서 애플리케이션 코드란 그 서비스가 무슨 일을 하는지 적어 둔 부분입니다. 애플리케이션 서버 자신은 그 내용을 모릅니다.
역할을 둘로 갈라 보면 선이 또렷해집니다. 한쪽은 요청을 받고, 어느 코드를 부를지 고르고, 그 코드를 부르고, 돌아온 값을 응답으로 포장해 내보내는 일을 맡습니다. 다른 한쪽은 실제로 무엇을 할지 적힌 코드입니다. 애플리케이션 서버는 앞쪽입니다. 코드를 실행해 주는 쪽이지 그 코드 자체가 아닙니다.
이 갈림은 대개 인터페이스로 굳혀 둡니다. 부르는 쪽과 불리는 쪽이 서로를 어떻게 찾고 무엇을 주고받을지 미리 정해 두면, 같은 애플리케이션 코드를 다른 애플리케이션 서버 위에 옮겨 얹을 수 있습니다.
배경
초기의 웹 서버에 오던 요청은 대개 요청받은 자리의 파일을 찾아 그대로 내보내면 끝났습니다. 그런데 요청마다 답이 달라지는 것은 미리 파일로 만들어 둘 수 없습니다. 누군가 그 자리에서 계산해 만들어야 합니다.
그래서 서버가 바깥의 프로그램을 불러 답을 받아 오는 규약이 먼저 생겼습니다. RFC 3875(Common Gateway Interface, CGI)는 'server' 를 "클라이언트의 요청을 처리하려고 스크립트를 호출하는 응용 프로그램" 으로, 'script' 를 "이 인터페이스에 따라 서버가 호출하는 소프트웨어" 로 적습니다. 같은 문서는 그 스크립트가 "독립 실행 프로그램일 필요는 없고, 동적으로 적재되는 공유 라이브러리이거나 서버 안의 서브루틴이어도 된다" 고 적습니다.
호출하는 쪽과 호출당하는 쪽이 이렇게 갈리자, 호출당하는 코드를 담아 두고 요청이 올 때마다 실행해 주는 자리가 따로 남았습니다. 그 자리를 맡는 소프트웨어를 애플리케이션 서버라고 부릅니다.
예시
Gunicorn
파이썬 애플리케이션을 실행하는 애플리케이션 서버입니다. 공식 문서는 가장 단순한 애플리케이션 객체를 이렇게 보입니다.
def app(environ, start_response):
"""Simplest possible application object"""
data = b"Hello, World!\n"
status = "200 OK"
response_headers = [
("Content-type", "text/plain"),
("Content-Length", str(len(data)))
]
start_response(status, response_headers)
return iter([data])
그리고 이렇게 띄웁니다.
gunicorn --workers=2 test:app
test:app 은 test 안의 app 을 가리킵니다. 애플리케이션 코드는 저 함수 하나뿐이고,
요청을 받아 저 함수를 부르는 일은 Gunicorn 이 맡습니다.
-w · --workers 는 워커 프로세스의 수이고, 공식 문서는 보통 CPU(Central Processing Unit) 코어당 두 개에서 네 개를 든다고 적습니다.
PHP-FPM
PHP(PHP: Hypertext Preprocessor) 애플리케이션을 실행하는 프로세스 매니저입니다. 풀마다 아래 지시어로 자식 프로세스를 몇 개 어떻게 둘지 정합니다.
| 지시어 | 공식 문서가 정한 것 |
|---|---|
pm |
프로세스 매니저가 자식 프로세스 수를 어떻게 조절할지 고릅니다. 값은 static · ondemand · dynamic 이고 필수입니다. static 이면 자식 프로세스 수가 pm.max_children 으로 고정됩니다 |
pm.max_children |
static 일 때 만들 자식 프로세스 수이자 dynamic · ondemand 일 때 만들 수 있는 최대치입니다. 필수이고, 동시에 처리될 요청 수의 상한을 정합니다 |
listen |
FastCGI 요청을 받을 주소입니다. ip.add.re.ss:port · port · /path/to/unix/socket 을 쓸 수 있고 풀마다 필수입니다 |
세 지시어 모두 애플리케이션 코드가 아니라 그 코드를 실행하는 쪽의 설정입니다.
listen 이 IP(Internet Protocol) 주소와 유닉스 소켓을 둘 다 받는 자리라는 점도 같은 이야기입니다.
Apache Tomcat
서블릿과 JSP(JavaServer Pages) 페이지를 실행하는 애플리케이션 서버입니다.
공식 문서는 HTTP(HyperText Transfer Protocol) Connector 를 "HTTP/1.1 프로토콜을 지원하는 Connector 구성요소" 로 적고,
이 요소가 "Catalina 가 서블릿과 JSP 페이지를 실행하는 능력에 더해 단독 웹 서버로도 동작할 수 있게 한다" 고 적습니다.
port 속성은 이 Connector 가 서버 소켓을 만들어 들어오는 연결을 기다릴 TCP(Transmission Control Protocol) 포트 번호입니다.
같은 문서는 운영체제가 특정 IP 주소의 특정 포트 번호를 두고 하나의 서버 애플리케이션만 듣도록 허용한다고 덧붙입니다.
경계
nginx 도 애플리케이션 서버인가. 아닙니다.
nginx 공식 문서는 ngx_http_proxy_module 을 "요청을 다른 서버로 넘길 수 있게 하는 모듈" 이라고 적습니다.
예제 설정은 proxy_pass http://localhost:8000; 한 줄입니다.
ngx_http_fastcgi_module 도 같은 꼴입니다. "요청을 FastCGI 서버로 넘길 수 있게 하는 모듈" 이고 예제는 fastcgi_pass localhost:9000; 입니다.
두 경우 모두 응답을 만드는 일은 넘겨받은 쪽이 합니다.
반대쪽에서도 같은 선이 보입니다. Gunicorn 공식 문서는 Gunicorn 을 프록시 서버 뒤에서 돌리기를 강력히 권한다고 적고, 거기 실린 nginx 예제 설정은 이렇게 갈라집니다.
location / {
# checks for static file, if not found proxy to app
try_files $uri @proxy_to_app;
}
location @proxy_to_app {
...
proxy_pass http://app_server;
}
정적 파일이 있으면 nginx 가 그 자리에서 돌려주고, 없을 때만 뒤로 넘어갑니다.
flowchart TD
A["요청"] --> B["웹 서버"]
B -->|"파일이 있다"| C["파일을 그대로 응답"]
B -->|"파일이 없다"| D["애플리케이션 서버"]
D --> E["애플리케이션 코드 실행"]
E --> F["만들어진 응답"]
그래서 선은 이렇습니다. 응답을 자기가 계산하면 애플리케이션 서버이고, 계산할 쪽으로 넘기면 그 자리는 애플리케이션 서버가 아닙니다.
한 프로그램이 두 일을 겸할 수는 있습니다. Tomcat 공식 문서가 HTTP Connector 를 두고 "단독 웹 서버로도 동작할 수 있게 한다" 고 적는 것이 그 경우입니다. 겸한다고 해서 선이 흐려지지는 않습니다. 겸하는 프로그램 안에서도 파일을 찾아 내보내는 부분과 코드를 실행하는 부분은 여전히 다른 일입니다.
관련 항목
요청을 먼저 만나는 앞단 계층
웹 서버 · nginx · 리버스 프록시 · 로드 밸런서 · 정적 파일
애플리케이션 코드와 잇는 규약
CGI · FastCGI · WSGI · 서블릿 · 서블릿 컨테이너 · 미들웨어
이것을 실제로 구현·채택한 제품
Gunicorn · PHP-FPM · Apache Tomcat
요청이 오가는 통로
TCP · IP · 유닉스 소켓
실행 단위를 이루는 자원
다른 이름: application server · app server · WAS · 웹 애플리케이션 서버