Apache 정적 파일 서빙과 캐시 제어
Apache에서 정적 파일을 효율적으로 서빙하고 브라우저 캐시를 올바르게 제어하는 방법을 다룹니다. mod_expires, mod_deflate, ETag 설정을 통해 웹 성능을 크게 향상시킬 수 있습니다.
Apache의 정적 파일 서빙 기본
Apache는 요청된 URL을 DocumentRoot와 결합하여 파일 시스템에서 파일을 찾아 반환합니다.
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/html
<Directory /var/www/html>
Options -Indexes +FollowSymLinks # 디렉터리 목록 비활성화, 심볼릭 링크 허용
AllowOverride None
Require all granted
</Directory>
</VirtualHost>
요청: GET /images/logo.png
→ 실제 파일: /var/www/html/images/logo.png
mod_expires — 캐시 만료 헤더
mod_expires는 Expires와 Cache-Control: max-age 헤더를 자동으로 설정합니다.
# 모듈 활성화 (Ubuntu)
sudo a2enmod expires
sudo systemctl reload apache2
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/html
<IfModule mod_expires.c>
ExpiresActive On
# 기본값 — 모든 파일에 1주일 캐시
ExpiresDefault "access plus 1 week"
# HTML — 캐시 없음 (항상 최신 확인)
ExpiresByType text/html "access plus 0 seconds"
# CSS / JavaScript — 1년 (빌드 해시 포함 파일)
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType text/javascript "access plus 1 year"
# 이미지 — 1개월
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/webp "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
ExpiresByType image/x-icon "access plus 1 year"
# 폰트 — 1년 (변경 거의 없음)
ExpiresByType font/woff "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType application/font-woff "access plus 1 year"
ExpiresByType application/font-woff2 "access plus 1 year"
# 기타
ExpiresByType application/pdf "access plus 1 month"
ExpiresByType application/json "access plus 0 seconds"
</IfModule>
</VirtualHost>
mod_headers — Cache-Control 정밀 제어
mod_expires는 Expires 헤더를 설정하지만, Cache-Control을 더 세밀하게 제어하려면 mod_headers를 함께 사용합니다.
sudo a2enmod headers
sudo systemctl reload apache2
<IfModule mod_headers.c>
# 빌드 해시가 붙은 JS/CSS — 1년 강력 캐시 (immutable)
<FilesMatch "\.[0-9a-f]{8,}\.(js|css)$">
Header set Cache-Control "max-age=31536000, public, immutable"
</FilesMatch>
# 일반 JS/CSS
<FilesMatch "\.(js|css)$">
Header set Cache-Control "max-age=31536000, public"
</FilesMatch>
# 이미지·폰트
<FilesMatch "\.(png|jpg|jpeg|gif|webp|ico|woff|woff2|ttf|svg)$">
Header set Cache-Control "max-age=2592000, public"
</FilesMatch>
# HTML — no-cache
<FilesMatch "\.html$">
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "0"
</FilesMatch>
# API 응답 — 캐시 없음
<LocationMatch "^/api/">
Header set Cache-Control "no-cache, no-store, must-revalidate"
</LocationMatch>
</IfModule>
ETag 설정
ETag는 파일의 고유 식별자로, 브라우저가 캐시 만료 후 서버에 재검증 요청을 보낼 때 사용합니다.
# ETag 활성화 (기본값)
FileETag MTime Size
# 분산 서버 환경에서 ETag 비활성화 권장
# (서버마다 inode가 달라 ETag 값이 다를 수 있음)
FileETag None
# 또는 inode 제외
FileETag MTime Size
분산 환경 주의: 여러 서버를 로드밸런싱하는 경우 기본 ETag에 inode 번호가 포함되어 서버마다 다른 ETag를 반환할 수 있습니다.
FileETag MTime Size로 설정하거나FileETag None으로 ETag를 비활성화하세요.
mod_deflate — Gzip 압축
sudo a2enmod deflate
sudo systemctl reload apache2
<IfModule mod_deflate.c>
# 압축할 MIME 타입
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE text/javascript
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/json
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE image/svg+xml
AddOutputFilterByType DEFLATE font/woff font/woff2
# 이미 압축된 파일 형식은 제외
SetEnvIfNoCase Request_URI \
\.(?:gif|jpe?g|png|webp|zip|gz|bz2|rar|7z|mp4|mp3|ogg)$ \
no-gzip dont-vary
# Vary 헤더 추가 (프록시 캐시가 인코딩별로 캐시하도록)
Header append Vary Accept-Encoding
# 압축 레벨 (1~9)
DeflateCompressionLevel 6
</IfModule>
Gzip 효과 확인
curl -H "Accept-Encoding: gzip" -I http://example.com/app.js
# 응답 헤더 확인:
# Content-Encoding: gzip
# Vary: Accept-Encoding
정적 파일 MIME 타입 설정
Apache가 파일 확장자에 올바른 Content-Type을 반환하도록 MIME 타입을 설정합니다.
# /etc/apache2/mime.types에 없는 타입 추가
AddType application/wasm .wasm
AddType font/woff2 .woff2
AddType image/webp .webp
AddType video/mp4 .mp4
AddType application/manifest+json .webmanifest
종합 설정 예시
실무 운영 환경에서 사용하는 정적 파일 서빙 최적화 전체 설정:
<VirtualHost *:443>
ServerName static.example.com
DocumentRoot /var/www/static
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/static.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/static.example.com/privkey.pem
<Directory /var/www/static>
Options -Indexes -MultiViews +FollowSymLinks
AllowOverride None
Require all granted
</Directory>
# Gzip 압축
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css application/javascript application/json image/svg+xml font/woff font/woff2
DeflateCompressionLevel 6
Header append Vary Accept-Encoding
</IfModule>
# 캐시 만료 설정
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/html "access plus 0 seconds"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/webp "access plus 1 month"
ExpiresByType font/woff2 "access plus 1 year"
</IfModule>
# Cache-Control 헤더
<IfModule mod_headers.c>
<FilesMatch "\.(js|css)$">
Header set Cache-Control "max-age=31536000, public, immutable"
</FilesMatch>
<FilesMatch "\.(png|jpg|jpeg|gif|webp|ico|woff|woff2)$">
Header set Cache-Control "max-age=2592000, public"
</FilesMatch>
<FilesMatch "\.html$">
Header set Cache-Control "no-cache"
</FilesMatch>
</IfModule>
ErrorLog /var/log/apache2/static.error.log
CustomLog /var/log/apache2/static.access.log combined
</VirtualHost>
정리
| 최적화 항목 | 모듈 | 효과 |
|---|---|---|
| 캐시 만료 헤더 | mod_expires | 재방문 시 서버 요청 감소 |
| Cache-Control 정밀 제어 | mod_headers | immutable 등 세밀한 캐시 정책 |
| Gzip 압축 | mod_deflate | 텍스트 파일 50~80% 전송량 감소 |
| ETag 조정 | FileETag | 분산 환경에서 불필요한 재검증 방지 |
| 디렉터리 목록 비활성화 | Options -Indexes | 보안 강화 |