Apache 정적/동적 분리 서빙
Apache+Tomcat 연동에서 정적 파일(CSS, JS, 이미지)은 Apache가 직접 서빙하고 동적 요청(JSP, 서블릿)만 Tomcat으로 전달하면 성능이 크게 향상됩니다. 각 연동 방식별 분리 방법을 설명합니다.
분리 원칙
[클라이언트]
↓
[Apache HTTPD]
├── /static/, /images/, /css/, /js/ → Apache 직접 서빙 (빠름)
└── /api/, /app/, *.do, *.jsp → Tomcat 전달 (동적 처리)
직접 서빙의 장점:
- Tomcat 부하 감소 (스레드 절약)
- 캐싱 헤더 제어 용이
- sendfile() 시스템 콜 활용으로 디스크 I/O 최소화
- 압축(gzip) 적용이 Apache 레벨에서 가능
mod_jk — JkUnMount 방식
JkMount로 전체를 Tomcat에 전달한 뒤 JkUnMount로 예외를 설정합니다.
<VirtualHost *:80>
ServerName example.com
# 정적 파일 디렉터리
DocumentRoot /var/www/myapp
Alias /static /var/www/myapp/static
Alias /images /var/www/myapp/images
# 전체를 Tomcat으로 전달
JkMount /* worker1
# 정적 경로 예외 처리 (Apache가 직접 서빙)
JkUnMount /static/* worker1
JkUnMount /images/* worker1
JkUnMount /css/* worker1
JkUnMount /js/* worker1
JkUnMount /fonts/* worker1
JkUnMount /favicon.ico worker1
JkUnMount /robots.txt worker1
<Directory "/var/www/myapp/static">
Options -Indexes
AllowOverride None
Require all granted
# 캐시 헤더
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType image/png "access plus 6 months"
ExpiresByType image/jpeg "access plus 6 months"
ExpiresByType image/svg+xml "access plus 6 months"
ExpiresByType font/woff2 "access plus 1 year"
</IfModule>
</Directory>
CustomLog ${APACHE_LOG_DIR}/myapp_access.log combined
ErrorLog ${APACHE_LOG_DIR}/myapp_error.log
</VirtualHost>
확장자 기반 JkUnMount
특정 확장자 패턴으로 정적 파일을 제외할 수 있습니다.
# 확장자 기반 제외 (mod_jk)
JkUnMount /*.css worker1
JkUnMount /*.js worker1
JkUnMount /*.png worker1
JkUnMount /*.jpg worker1
JkUnMount /*.gif worker1
JkUnMount /*.ico worker1
JkUnMount /*.woff worker1
JkUnMount /*.woff2 worker1
JkUnMount /*.ttf worker1
JkUnMount /*.svg worker1
JkUnMount /*.map worker1
주의: 확장자 패턴은 경로 패턴(
/static/*)과 함께 사용 가능하며 OR 조건으로 동작합니다.
mod_proxy_http — ProxyPass 제외 방식
!를 사용한 예외 ProxyPass 지시어로 정적 파일을 제외합니다.
<VirtualHost *:443>
ServerName example.com
DocumentRoot /var/www/myapp
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
# 정적 파일 디렉터리 매핑
Alias /static /var/www/myapp/static
Alias /images /var/www/myapp/images
# 정적 경로 제외 (! = 프록시하지 않음)
ProxyPass /static !
ProxyPass /images !
ProxyPass /css !
ProxyPass /js !
ProxyPass /fonts !
ProxyPass /favicon.ico !
ProxyPass /robots.txt !
# 동적 요청만 Tomcat으로
ProxyRequests Off
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:8080/ connectiontimeout=10 timeout=60
ProxyPassReverse / http://127.0.0.1:8080/
# 정적 파일 디렉터리 설정
<Directory "/var/www/myapp/static">
Options -Indexes
AllowOverride None
Require all granted
</Directory>
<Directory "/var/www/myapp/images">
Options -Indexes
AllowOverride None
Require all granted
</Directory>
# 정적 파일 캐시 + 압축
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType image/png "access plus 6 months"
ExpiresByType image/jpeg "access plus 6 months"
ExpiresByType image/webp "access plus 6 months"
ExpiresByType font/woff2 "access plus 1 year"
</IfModule>
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/css application/javascript
AddOutputFilterByType DEFLATE image/svg+xml application/json
</IfModule>
CustomLog ${APACHE_LOG_DIR}/myapp_access.log combined
ErrorLog ${APACHE_LOG_DIR}/myapp_error.log
</VirtualHost>
확장자 기반 라우팅 — RewriteRule 활용
mod_rewrite를 사용하면 더 세밀한 제어가 가능합니다.
sudo a2enmod rewrite
<VirtualHost *:443>
ServerName example.com
DocumentRoot /var/www/myapp
RewriteEngine On
# 정적 파일 확장자는 Apache 직접 서빙 (Tomcat 전달 생략)
RewriteRule \.(css|js|png|jpg|jpeg|gif|ico|woff|woff2|ttf|svg|webp|map)$ - [L]
# 나머지는 모두 Tomcat으로
RewriteRule ^/(.*)$ http://127.0.0.1:8080/$1 [P,L]
ProxyPassReverse / http://127.0.0.1:8080/
</VirtualHost>
SetHandler — 특정 경로 핸들러 지정
SetHandler는 특정 경로의 처리 방식을 강제로 지정합니다. 상태 페이지나 헬스체크 엔드포인트에 유용합니다.
# 서버 상태 페이지 (내부망만)
<Location /server-status>
SetHandler server-status
Require ip 127.0.0.1 10.0.0.0/8
</Location>
# 서버 정보 페이지
<Location /server-info>
SetHandler server-info
Require ip 127.0.0.1
</Location>
# mod_proxy_balancer 관리 페이지
<Location /balancer-manager>
SetHandler balancer-manager
Require ip 127.0.0.1
</Location>
# 헬스체크 전용 엔드포인트 (로드밸런서 체크용)
<Location /health>
SetHandler default-handler
# 정적 파일로 응답 (Tomcat 불필요)
</Location>
# mod_status 활성화
sudo a2enmod status
sudo systemctl reload apache2
Tomcat 측 정적 파일 비활성화
Apache가 정적 파일을 서빙하면 Tomcat의 DefaultServlet이 동일 파일을 이중 서빙하는 것을 방지합니다.
<!-- web.xml — DefaultServlet 정적 경로 제한 -->
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/dynamic-static/*</url-pattern>
<!-- /static/* 은 Apache에서 처리하므로 Tomcat에 매핑하지 않음 -->
</servlet-mapping>
또는 WAR 외부에 정적 파일을 배치:
<!-- server.xml — Context에 외부 경로 추가 -->
<Context path="/myapp" docBase="/opt/tomcat/webapps/myapp">
<!-- /static 은 Context 밖 -->
</Context>
빌드 산출물 배포 구조
SPA(Vue/React) 빌드 파일을 Apache에서 직접 서빙하는 일반적인 구조입니다.
/var/www/myapp/
├── static/ ← React/Vue 빌드 (Apache 서빙)
│ ├── css/
│ ├── js/
│ └── media/
├── index.html ← SPA 진입점 (Apache 서빙)
└── api/ ← API 요청만 Tomcat으로 (ProxyPass)
<VirtualHost *:443>
ServerName example.com
DocumentRoot /var/www/myapp
# API만 Tomcat으로
ProxyPass /api/ http://127.0.0.1:8080/api/
ProxyPassReverse /api/ http://127.0.0.1:8080/api/
# SPA 라우팅 — 존재하지 않는 경로는 index.html로
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !^/api/
RewriteRule ^ /index.html [L]
<Directory "/var/www/myapp">
Options -Indexes
AllowOverride None
Require all granted
</Directory>
</VirtualHost>
성능 비교
| 방식 | 정적 파일 처리 | Tomcat 부하 | 캐시 제어 |
|---|---|---|---|
| 전체 Tomcat 전달 | Tomcat DefaultServlet | 높음 | 어려움 |
| Apache 직접 서빙 | Apache mod_static | 낮음 | 쉬움 |
| CDN 오프로딩 | CDN | 없음 | CDN 설정 |
Summary
| 항목 | 방법 |
|---|---|
| mod_jk | JkUnMount /static/* worker1 |
| mod_proxy | ProxyPass /static ! |
| 확장자 기반 | RewriteRule \.(css|js|...)$ - [L] |
| 핸들러 지정 | SetHandler balancer-manager |
| 캐시 헤더 | mod_expires + ExpiresActive On |
| SPA 지원 | RewriteRule ^ /index.html [L] |