고객사 서버 점검 의뢰를 받아 SSH로 접속했습니다. 웹 서버 프로세스를 확인했더니 nginx가 아닌 httpd가 떠 있습니다. 레거시 Java EE 프로젝트가 Tomcat과 붙어 있고, Apache가 앞단에서 요청을 받아 프록시하는 구조입니다. VirtualHost 설정이 어디 있는지, mod_proxy는 어떻게 확인하는지, 로그는 어디를 봐야 하는지 — Nginx만 써왔다면 당황스러운 순간입니다.
Apache httpd는 여전히 엔터프라이즈 환경에서 광범위하게 쓰입니다. 레거시 유지보수, 고객사 서버 진단, Nginx 도입 전 마이그레이션 계획 — 어떤 상황에서든 Apache를 읽고 운영할 수 있어야 현장에서 막히지 않습니다.
- 1Apache MPM 모델과 Nginx 이벤트 기반 모델의 차이를 설명할 수 있다
- 2VirtualHost 블록을 작성하고 apachectl configtest로 검증할 수 있다
- 3mod_proxy와 mod_proxy_http를 확인·활성화하고 ProxyPass 설정을 작성할 수 있다
- 4access_log 포맷 필드(%h, %t, %>s, %T, %D)를 해석할 수 있다
- 5graceful restart와 일반 restart의 차이를 이해하고 상황에 따라 선택할 수 있다
Apache 구조와 Nginx와의 차이
Apache MPM — 프로세스/스레드 기반 처리 모델
Apache와 Nginx가 동시 요청을 처리하는 방식은 근본적으로 다릅니다. 이 차이를 모르면 "Apache가 왜 느린가"라는 질문에 답하지 못하고, 레거시 서버를 최적화할 때 잘못된 판단을 하게 됩니다.
확대
Apache는 Multi-Processing Module(MPM) 방식으로 요청을 처리합니다. 어떤 MPM을 쓰느냐에 따라 프로세스/스레드 사용 방식이 달라집니다.
| MPM | 방식 | 특징 | 권장 상황 |
|---|---|---|---|
prefork | 요청당 독립 프로세스 | 안정적, 메모리 많이 씀 | mod_php 같은 비스레드세이프 모듈 |
worker | 멀티스레드 + 멀티프로세스 | 메모리 효율적 | PHP 없이 정적/프록시만 할 때 |
event | worker + 비동기 Keep-Alive | 고동시 연결에 유리 | Apache 2.4 기본값, 현재 권장 |
# 현재 사용 중인 MPM 확인
httpd -V | grep -i mpm
# 또는
apachectl -V | grep MPM
# 출력 예시:
# Server MPM: event
# httpd compiled with: -D APR_HAS_SENDFILE ...
Nginx와의 핵심 차이:
Nginx는 마스터 프로세스 하나와 소수의 worker 프로세스가 이벤트 루프로 수만 개의 연결을 처리합니다. Apache event MPM도 비슷하게 진화했지만, 역사적으로 prefork가 기본이었고 모듈 생태계가 프로세스 기반을 전제로 만들어진 것이 많습니다.
현장에서 Apache가 여전히 쓰이는 이유:
- mod_php: PHP를 Apache 프로세스 내에서 직접 실행 — 공유 호스팅 환경 다수
- mod_jk / mod_proxy_ajp: Tomcat과의 AJP 연결 — 레거시 Java EE 인프라
- 모듈 호환성: mod_rewrite, mod_auth_*, .htaccess — 기존 설정 자산
- 레거시 유지: 수십 년 된 배포를 건드리지 않는 조직 다수
요청 하나가 Apache를 통과하는 7단계 — accept부터 응답까지
curl http://api.example.com/app/page 한 줄, 또는 브라우저 주소창에 도메인을 치고 Enter를 누르면 화면이 뜹니다. 이 짧은 순간에 Apache 안에서는 연결 수락 → 워커 배정 → VirtualHost 선택 → 모듈 훅 통과 → 핸들러 실행 → 응답이 순서대로 일어납니다. 이 흐름을 단계로 나눠 두면 "왜 403이지", "왜 503이지", "왜 502지"를 특정 단계로 좁혀 진단할 수 있습니다. Apache는 요청을 뭉뚱그려 처리하는 게 아니라 아래 7단계를 순서대로 밟습니다.
[클라이언트] GET /app/page HTTP/1.1 Host: api.example.com
│
① 리스너 소켓 accept (Listen 80/443, MPM이 TCP 연결 수락)
│
② MPM 워커 배정 (prefork=자식 프로세스 / worker·event=스레드)
│ → 유휴 워커에 연결을 넘김. 없으면 대기·거부
│
③ 요청 파싱 → VirtualHost 선택 (Host 헤더로 매칭되는 VirtualHost 결정)
│
④ 모듈 훅 체인 통과 (mod_rewrite → mod_authz → mod_headers ...)
│ → URL 재작성·접근제어·인증·헤더를 단계별 훅으로 순서대로 처리
│
⑤ 핸들러 결정·실행 (정적파일 core / mod_proxy / CGI·PHP)
│
⑥ 출력 필터 통과 → 클라이언트로 응답 (mod_deflate 압축, mod_headers 등)
│
⑦ 액세스 로그 기록 → 워커 반납 (keep-alive면 연결 유지하며 다음 요청 대기)
▼
[클라이언트] 200 OK (워커는 풀로 돌아가 다음 요청 대기)
각 단계에서 무슨 일이 일어나고, 막히면 어떤 증상인가:
| 단계 | 하는 일 | 여기서 막히면 |
|---|---|---|
| ① accept | Listen 지시어의 포트로 리스너 소켓이 TCP 연결을 수락 | 포트 미개방·방화벽 차단 → Connection refused(즉시 거부) 또는 timeout(응답 없음) |
| ② 워커 배정 | MPM이 유휴 워커(prefork=프로세스 · worker·event=스레드)에 연결을 넘김 | 워커 전부 busy로 MaxRequestWorkers 도달 → 새 요청은 TCP 백로그에서 대기, CPU·메모리가 남아도 지연·503 |
| ③ VirtualHost 선택 | 요청 라인·Host 헤더를 읽어 매칭되는 VirtualHost 블록 결정 | ServerName·ServerAlias 불일치 → 첫 번째(기본) VirtualHost로 잘못 매칭돼 엉뚱한 사이트 응답 |
| ④ 모듈 훅 | mod_rewrite·mod_authz·mod_auth·mod_headers가 훅 순서대로 URL 재작성·접근제어·인증 수행 | LoadModule 순서·누락 → rewrite 규칙 미적용, 접근제어 실패 → 403·401, 또는 지시어 자체가 Invalid command |
| ⑤ 핸들러 | 정적파일(core)·프록시(mod_proxy)·CGI 중 콘텐츠 생성 주체를 결정·실행 | 프록시 백엔드 연결 실패 → 502 · 백엔드 전부 unavailable·과부하 → 503 · 백엔드 응답 지연 → 504 |
| ⑥ 출력 필터 | 생성된 응답 바디가 출력 필터 체인을 거쳐 클라이언트로 전송 | 프록시 응답에 ProxyPassReverse 누락 → Location 헤더에 내부주소(127.0.0.1) 노출 → 리다이렉트 깨짐 |
| ⑦ 로그·반납 | mod_log_config가 access_log를 기록한 뒤 워커를 풀에 반납 | prefork + 긴 KeepAliveTimeout → 워커가 유휴 keep-alive 연결에 붙잡혀 ②가 조기 고갈 |
즉 에러 코드는 어느 단계에서 났는지를 알려주는 지도입니다. 403·401은 ④(접근제어·인증)에서, 404는 ⑤(핸들러가 파일·매핑을 못 찾음)에서, 502·503·504는 ⑤의 프록시 백엔드에서 갈립니다 — 502는 백엔드 연결 실패, 504는 백엔드 응답 타임아웃, 503은 워커 고갈(②) 또는 백엔드 unavailable입니다. 그래서 503을 만나면 먼저 mod_status의 scoreboard에서 BusyWorkers가 MaxRequestWorkers에 닿았는지(②)를 보고, 아니라면 프록시 백엔드 상태(⑤)를 봅니다. 요청이 어느 단계까지 갔는지는 error_log의 단계별 메시지로 좁힙니다.
VirtualHost 설정
Name-based VirtualHost — 하나의 서버, 여러 도메인
고객사 서버를 인수인계받아 VirtualHost 파일을 처음 열었을 때 /etc/httpd/conf.d/ 아래 .conf 파일이 10개 이상 있는 경우가 있습니다. 어떤 도메인이 어느 파일에서 처리되는지 모르면 Nginx 설정이라도 짐작하겠지만, Apache 문법이 낯설면 더 막막합니다. VirtualHost를 이해하면 이 구조를 5분 안에 파악할 수 있습니다. 서버 IP가 하나뿐인데 여러 도메인이 돌아가야 할 때, HTTP의 Host 헤더를 보고 요청을 구분하는 것이 Name-based VirtualHost입니다.
VirtualHost는 Apache에서 멀티도메인 서비스를 구현하는 핵심 메커니즘입니다. 클라이언트가 HTTP 요청을 보낼 때 Host 헤더에 도메인을 담아 보내고, Apache는 그 헤더 값을 보고 어느 VirtualHost 블록이 처리할지 결정합니다.
확대
설정 파일 위치:
- CentOS/RHEL:
/etc/httpd/conf.d/*.conf - Ubuntu/Debian:
/etc/apache2/sites-available/*.conf(활성화는a2ensite)
# /etc/httpd/conf.d/example.conf
<VirtualHost *:80>
# 이 VirtualHost가 응답할 기본 도메인
ServerName www.example.com
# 동일 콘텐츠로 응답할 추가 도메인 (별칭)
ServerAlias example.com
# 정적 파일을 서빙할 루트 디렉터리
DocumentRoot /var/www/example
# DocumentRoot에 대한 접근 제어
<Directory /var/www/example>
# -Indexes: 파일 목록 노출 금지 (보안)
Options -Indexes
# AllowOverride None: .htaccess 무시 (성능 향상)
AllowOverride None
# Require all granted: 모든 클라이언트 허용
Require all granted
</Directory>
# 이 VirtualHost 전용 로그 (전역 로그와 분리)
ErrorLog /var/log/httpd/example-error.log
CustomLog /var/log/httpd/example-access.log combined
</VirtualHost>
주요 지시어 설명:
| 지시어 | 역할 | 주의점 |
|---|---|---|
ServerName | 기본 매칭 도메인 | VirtualHost당 하나 |
ServerAlias | 추가 매칭 도메인 (공백 구분) | 와일드카드 가능 (*.example.com) |
DocumentRoot | 파일 서빙 기준 디렉터리 | <Directory> 블록과 경로 일치 필요 |
Options -Indexes | 디렉터리 인덱스 비활성화 | 운영 환경 필수 보안 설정 |
AllowOverride None | .htaccess 파일 무시 | 성능상 유리, 중앙 집중 관리 |
Require all granted | 접근 허용 | Apache 2.4 문법 (2.2의 Allow from all과 다름) |
mod_proxy 리버스 프록시
ProxyPass — Apache를 리버스 프록시로 쓰기
Apache를 단순 파일 서버로만 쓰는 시대는 지났습니다. 레거시 환경에서 Apache는 앞단에서 요청을 받아 뒤에 있는 Tomcat, Node.js, Python 앱으로 프록시하는 역할을 합니다. 이 구조를 구현하는 것이 mod_proxy입니다.
모듈 활성화가 먼저입니다. 모듈 없이 ProxyPass를 쓰면 Invalid command 'ProxyPass' 에러가 납니다.
# CentOS/RHEL: 모듈 설정 파일 확인
cat /etc/httpd/conf.modules.d/00-proxy.conf
# 아래 줄들이 주석 해제되어 있어야 함:
# LoadModule proxy_module modules/mod_proxy.so
# LoadModule proxy_http_module modules/mod_proxy_http.so
# LoadModule headers_module modules/mod_headers.so
# Ubuntu/Debian: a2enmod로 활성화
sudo a2enmod proxy proxy_http headers
sudo systemctl reload apache2
모듈이 활성화됐다면 VirtualHost 블록 안에서 ProxyPass를 사용합니다:
<VirtualHost *:80>
ServerName api.example.com
# ProxyPreserveHost On: 원본 Host 헤더를 백엔드로 그대로 전달
# Off로 하면 백엔드는 127.0.0.1을 Host로 받음
ProxyPreserveHost On
# /api/ 로 들어온 요청을 백엔드 8080 포트로 전달
ProxyPass /api/ http://127.0.0.1:8080/api/
# 백엔드가 응답에 Location 헤더를 보낼 때 URL을 클라이언트 주소로 재작성
ProxyPassReverse /api/ http://127.0.0.1:8080/api/
# 실제 클라이언트 IP와 프로토콜을 백엔드에 전달
RequestHeader set X-Real-IP "%{REMOTE_ADDR}s"
RequestHeader set X-Forwarded-Proto "http"
</VirtualHost>
ProxyPass vs ProxyPassReverse:
ProxyPass: 요청을 백엔드로 포워딩하는 규칙ProxyPassReverse: 백엔드 응답의Location,Content-Location,URI헤더에 포함된 내부 URL을 외부 URL로 재작성
ProxyPassReverse가 없으면 백엔드가 302 Redirect를 보낼 때 클라이언트가 http://127.0.0.1:8080/api/login 같은 내부 주소로 리다이렉트되어 접근 실패가 납니다.
로그 분석과 운영 명령어
access_log 포맷 해석과 응답시간 로깅
Apache 로그를 읽지 못하면 장애 원인을 찾는 데 두 배 시간이 걸립니다. 기본 combined 포맷의 각 필드 의미와, 운영에서 반드시 추가해야 하는 응답시간 항목을 익힙니다.
combined 포맷 기본 필드:
%h %l %u %t "%r" %>s %b
IP ident user 시각 "메서드 URI 프로토콜" 최종상태 바이트수
실제 로그 예시 한 줄:
203.0.113.42 - jdoe [30/May/2026:14:23:05 +0900] "GET /api/users HTTP/1.1" 200 1523 "https://example.com" "Mozilla/5.0"
| 필드 | 값 | 의미 |
|---|---|---|
%h | 203.0.113.42 | 클라이언트 IP |
%l | - | identd 사용자 (거의 항상 -) |
%u | jdoe | 인증된 사용자 (-이면 미인증) |
%t | [30/May/2026:...] | 요청 수신 시각 |
%r | GET /api/users HTTP/1.1 | 요청 라인 전체 |
%>s | 200 | 최종 응답 상태코드 (%s와 달리 리다이렉트 후 최종값) |
%b | 1523 | 응답 바이트 수 (헤더 제외) |
응답시간 필드 추가 — 슬로우 리퀘스트 탐지 필수:
# /etc/httpd/conf/httpd.conf 또는 VirtualHost 블록의 CustomLog 수정
LogFormat "%h %l %u %t \"%r\" %>s %b %T %D" combined_time
CustomLog /var/log/httpd/example-access.log combined_time
| 필드 | 단위 | 예시 | 설명 |
|---|---|---|---|
%T | 초 (정수) | 0 | 150ms → 0으로 기록 (정밀도 낮음) |
%D | 마이크로초 | 150423 | 150ms → 150423으로 기록 (정밀) |
슬로우 리퀘스트 분석에는 %D를 사용합니다. %T는 1초 미만 요청은 모두 0으로 보여 실용성이 낮습니다.
# 응답시간 기준으로 느린 요청 상위 10개 추출 (%D가 마지막 필드인 경우)
awk '{print $NF, $7}' /var/log/httpd/example-access.log | sort -rn | head -10
apachectl 운영 명령어 — graceful vs restart
Apache 설정을 바꿀 때마다 서비스를 완전히 껐다 켜면 처리 중인 요청이 끊깁니다. graceful은 이 문제를 해결합니다.
확대
# 설정 문법 검사 (서비스 영향 없음)
sudo apachectl configtest
# 또는 동일하게
sudo apachectl -t
# 시작 / 중지 / 완전 재시작
sudo apachectl start
sudo apachectl stop
sudo apachectl restart # 프로세스 완전 종료 후 재시작 (순간 중단 발생)
# 무중단 재기동 (권장)
sudo apachectl graceful # 기존 요청 완료 후 worker 교체, 새 설정 적용
# 무중단 중지 (마지막 요청 완료 후 종료)
sudo apachectl graceful-stop
restart vs graceful 비교:
| 항목 | restart | graceful |
|---|---|---|
| 처리 중 요청 | 즉시 끊김 | 완료될 때까지 대기 |
| 설정 반영 | 즉시 | 새 요청부터 |
| 다운타임 | 수십ms~수초 | 없음 |
| 권장 상황 | 모듈 변경, 바이너리 교체 | 일반 설정 변경 |
# 운영 서버 표준 절차 (검증 후 무중단 반영)
sudo apachectl configtest && sudo apachectl graceful
# error_log 레벨 설정 (httpd.conf 또는 VirtualHost)
# LogLevel warn ← 기본값 (debug/info/notice/warn/error/crit/alert/emerg)
실습
먼저 DocumentRoot 디렉터리를 만들고, VirtualHost 설정 파일을 /etc/httpd/conf.d/example.conf에 작성합니다. CentOS/RHEL 기준입니다. Ubuntu는 경로를 /etc/apache2/sites-available/example.conf로 바꾸고 sudo a2ensite example을 추가로 실행합니다.
# /etc/httpd/conf.d/example.conf
<VirtualHost *:80>
ServerName www.example.com
ServerAlias example.com
DocumentRoot /var/www/example
<Directory /var/www/example>
Options -Indexes
AllowOverride None
Require all granted
</Directory>
ErrorLog /var/log/httpd/example-error.log
CustomLog /var/log/httpd/example-access.log combined
</VirtualHost>
파일을 저장한 뒤 apachectl configtest로 문법을 검증합니다. Syntax OK가 나오면 진행합니다.
sudo mkdir -p /var/www/example && sudo apachectl configtest- apachectl configtest 출력 먼저 확인, 그 다음 ls /etc/httpd/conf.d/example.conf 로 파일 존재 확인 — configtest가 Syntax OK여야만 graceful reload 안전
- configtest 기준: 마지막 줄이 "Syntax OK"여야 정상, "Syntax error on line N"이면 해당 줄 번호로 이동해 수정 — Apache는 Nginx와 달리 문법 오류 위치를 줄 번호까지 정확히 알려줌
- configtest Syntax OK인데 VirtualHost가 동작 안 하면 → ServerName이 실제 요청 Host 헤더와 다른 것 — curl -H "Host: www.example.com" http://localhost/ 로 Host 헤더를 지정해서 테스트
Apache worker 프로세스는 apache 유저(Ubuntu는 www-data)로 실행됩니다. DocumentRoot 소유자가 이 유저여야 파일을 읽을 수 있습니다.
# 테스트용 index.html 생성
sudo tee /var/www/example/index.html > /dev/null << 'HTMLEOF'
<h1>Apache VirtualHost 동작 확인</h1>
HTMLEOF
# 소유권 변경 (CentOS: apache, Ubuntu: www-data)
sudo chown -R apache:apache /var/www/example
sudo chmod -R 755 /var/www/example
# Apache 프로세스 유저 확인 (불확실한 경우)
grep '^User' /etc/httpd/conf/httpd.conf
sudo chown -R apache:apache /var/www/example- grep "^User" /etc/httpd/conf/httpd.conf 로 Apache 실행 계정 먼저 확인, 그 다음 ls -la /var/www/example/ 로 소유자가 해당 계정인지 비교 — 계정 불일치가 403 Forbidden의 주원인
- 권한 기준: 디렉터리 755=Apache가 읽기/탐색 가능(정상), 700=Apache 실행 계정이 아닌 소유자만 접근 가능(403 발생), 777=보안 위험 — 설정 파일 디렉터리는 644 이하, HTML 디렉터리는 755
- 소유자가 apache이고 권한도 755인데 403이 나오면 → SELinux가 차단 중 — ls -Z /var/www/example/ 로 SELinux 컨텍스트 확인 후 restorecon -Rv /var/www/example 실행
검증이 통과하면 graceful로 재기동합니다. 운영 서버에서는 항상 이 순서를 지킵니다.
# 문법 검사
sudo apachectl configtest
# 문제 없으면 graceful 재기동
sudo apachectl graceful
# Host 헤더를 지정해 VirtualHost 라우팅 확인
curl -H "Host: www.example.com" http://localhost/
sudo apachectl configtest && sudo apachectl graceful- apachectl configtest Syntax OK 확인 먼저, 그 다음 graceful 실행, 마지막으로 curl -H "Host: www.example.com" http://localhost/ 로 VirtualHost 라우팅 확인 — 이 3단계 순서 준수
- 응답 기준: curl에서 "Apache VirtualHost 동작 확인" 텍스트가 보이면 VirtualHost 라우팅 정상, 다른 서버의 기본 페이지가 나오면 ServerName 매칭 실패 — Host 헤더 지정 없이 curl http://localhost/ 만으로는 VirtualHost 확인 불가
- graceful 후 httpd 프로세스가 없어지면 → apachectl configtest 에서 Syntax OK였어도 런타임 오류 발생 — tail -20 /var/log/httpd/error_log 로 graceful 시점의 에러 메시지 확인
mod_proxy 관련 모듈이 로드됐는지 먼저 확인합니다.
# 로드된 모듈 중 proxy 관련 확인
httpd -M | grep proxy
# 출력 예시 (정상):
# proxy_module (shared)
# proxy_http_module (shared)
# CentOS: 모듈 설정 파일 직접 확인
cat /etc/httpd/conf.modules.d/00-proxy.conf
# Ubuntu: 모듈 활성화
sudo a2enmod proxy proxy_http headers
sudo systemctl reload apache2
모듈이 확인되면 리버스 프록시 VirtualHost를 작성합니다:
# /etc/httpd/conf.d/api-proxy.conf
<VirtualHost *:80>
ServerName api.example.com
ProxyPreserveHost On
ProxyPass /api/ http://127.0.0.1:8080/api/
ProxyPassReverse /api/ http://127.0.0.1:8080/api/
RequestHeader set X-Real-IP "%{REMOTE_ADDR}s"
RequestHeader set X-Forwarded-Proto "http"
ErrorLog /var/log/httpd/api-proxy-error.log
CustomLog /var/log/httpd/api-proxy-access.log combined
</VirtualHost>
httpd -M | grep proxy- httpd -M | grep proxy 로 모듈 로드 먼저 확인, 그 다음 apachectl configtest, 마지막으로 curl 로 프록시 동작 테스트 — proxy_module 없으면 ProxyPass 지시어 자체가 "Invalid command" 오류
- 모듈 기준: proxy_module과 proxy_http_module 두 개 모두 있어야 HTTP 리버스 프록시 가능 — proxy_module만 있으면 ProxyPass 로드는 되지만 HTTP 백엔드 연결 불가
- curl에서 백엔드 응답이 오는데 백엔드 로그에 X-Real-IP가 없으면 → RequestHeader set X-Real-IP 설정이 반영 안 된 것 — mod_headers 모듈 로드 여부를 httpd -M | grep headers 로 확인
트러블슈팅
원인: VirtualHost 설정 파일에 오타 또는 잘못된 지시어가 있습니다. Apache는 설정 오류 위치를 파일 경로와 줄 번호로 정확히 알려줍니다.
# apachectl configtest 전체 출력 확인
sudo apachectl configtest 2>&1
# 출력 예시:
# AH00526: Syntax error on line 8 of /etc/httpd/conf.d/example.conf:
# Invalid command 'Documnetroot', perhaps misspelled or defined by a
# module not included in the server configuration
# 해당 파일의 해당 줄 확인
sed -n '5,12p' /etc/httpd/conf.d/example.conf
# 수정 후 재검증
sudo apachectl configtest
# 포함된 모든 설정 파일 덤프 확인 (복잡한 include 구조일 때)
sudo apachectl -S 2>&1 | head -30
apachectl -S는 VirtualHost 파싱 결과를 요약해 보여줍니다. 어떤 도메인이 어느 설정 파일로 라우팅되는지 한눈에 확인할 수 있어 복잡한 멀티 VirtualHost 환경에서 유용합니다.
원인: <Directory> 블록이 없거나 Require 지시어가 누락됐습니다. Apache 2.4에서는 명시적으로 접근을 허용하지 않으면 기본 거부됩니다.
# error_log에서 정확한 경로 확인
sudo tail -20 /var/log/httpd/example-error.log
# 출력 예시:
# [Mon May 30 14:35:12 2026] [error] [pid 1234] [client 127.0.0.1:54321]
# AH01630: client denied by server configuration: /var/www/example/index.html
# 현재 설정에서 Directory 블록 확인
sudo apachectl -S 2>&1
grep -r "Directory" /etc/httpd/conf.d/example.conf
설정 파일에 <Directory> 블록을 추가합니다:
<VirtualHost *:80>
ServerName www.example.com
DocumentRoot /var/www/example
# 이 블록이 없으면 AH01630 발생
<Directory /var/www/example>
Options -Indexes
AllowOverride None
Require all granted
</Directory>
</VirtualHost>
Apache 2.2에서 마이그레이션한 설정이라면 Order allow,deny / Allow from all 문법이 남아 있을 수 있습니다. Apache 2.4에서는 Require all granted로 교체해야 합니다.
심화 — 워커가 고갈되면 CPU가 놀아도 서비스가 멈춘다
심화: MPM 용량 설계 — MaxRequestWorkers와 워커 고갈
MPM의 종류(prefork/worker/event)를 아는 것과, 그 MPM이 '동시에 몇 개의 요청까지' 처리할 수 있는지를 아는 것은 다른 문제입니다. 장애는 대부분 후자에서 터집니다.
- 동시 처리량의 상한은 워커 수입니다: prefork는 자식 프로세스 하나가 요청 하나를 처리하므로 동시 처리량 = 살아 있는 자식 수입니다. worker/event는 자식 프로세스 ×
ThreadsPerChild만큼의 스레드가 상한이고, 그 총합을MaxRequestWorkers(구MaxClients)가 제한합니다. 이 한도에 닿으면 새 요청은 처리되지 못하고 TCP 백로그에서 대기합니다. ServerLimit가 진짜 천장입니다:MaxRequestWorkers를 올려도ServerLimit(prefork) 또는ServerLimit × ThreadsPerChild(event/worker)를 넘길 수 없습니다. 두 값을 함께 올리지 않으면 설정 숫자만 크고 실제 상한은 그대로입니다.- 느린 백엔드가 워커를 오래 붙잡습니다:
ProxyPass로 넘긴 백엔드가 느리면 워커는 응답을 기다리는 동안 다른 요청을 받지 못합니다. 백엔드 지연이 곧 워커 고갈로 번지므로, 프록시에는ProxyPass ... timeout=과 백엔드 자체 타임아웃을 반드시 둡니다. - prefork에서 keep-alive는 양날의 검입니다: keep-alive로 연결을 유지하면 그 자식은 다음 요청을 기다리며 놀지만 다른 요청에는 못 쓰입니다. 동시 연결이 많은 환경에서 prefork + 긴
KeepAliveTimeout은 워커를 빠르게 소진합니다. event MPM은 keep-alive 유휴 연결을 전용 리스너 스레드가 비동기로 들고 있어 이 문제를 완화합니다 — 그래서 프록시·고동시 워크로드에 event가 유리합니다.
핵심 한계: 워커를 무작정 늘리면 프로세스/스레드가 RAM을 먹어 스왑이 시작되고, 그러면 오히려 전체가 느려집니다. 워커 1개 메모리 × MaxRequestWorkers < 가용 RAM을 먼저 계산한 뒤 올려야 합니다. 실시간 워커 사용률은 mod_status의 scoreboard로 봅니다.
상황: 이벤트로 트래픽이 몰리자 응답이 들쭉날쭉해집니다. 어떤 요청은 즉시 오고 어떤 요청은 수 초씩 걸리다 타임아웃 납니다. top을 봐도 CPU는 절반, 메모리도 남습니다 — 자원이 노는데 왜 못 받나 싶습니다.
원인: 자원이 아니라 워커가 고갈됐습니다. 살아 있는 워커(prefork면 자식 수, event면 스레드 수)가 전부 busy라 MaxRequestWorkers 상한에 닿았고, 남는 요청은 TCP 백로그에서 순서를 기다립니다. 흔한 방아쇠는 ProxyPass 백엔드(예: Tomcat)가 느려져 워커들이 응답을 기다리며 오래 붙잡혀 있는 경우입니다.
진단: mod_status를 켜고 apachectl status(또는 server-status?auto)의 scoreboard에서 W(응답 쓰는 중)로 꽉 찼는지, BusyWorkers가 MaxRequestWorkers와 같은지 확인합니다. 동시에 ss -tan | grep -c ESTAB로 열린 연결 수를, error_log에서 server reached MaxRequestWorkers setting 경고를 찾습니다.
해결: 단기로는 워커를 올리되 MaxRequestWorkers와 ServerLimit를 함께, RAM 계산 안에서 올립니다. 근본적으로는 keep-alive·프록시 워크로드라면 event MPM으로 전환하고, 느린 백엔드에는 ProxyPass timeout=을 걸어 워커가 무한정 붙잡히지 않게 합니다. 백엔드 지연 자체는 WAR 배포부터 server.xml 튜닝, 장애 대응까지 쪽 스레드풀·GC를 함께 봐야 근본 해결됩니다.
레거시 Java EE 환경 — Apache + Tomcat AJP 연결 구조
고객사 레거시 서버에서 가장 많이 만나는 구조는 Apache httpd가 앞단에서 요청을 받아 AJP 프로토콜로 뒤에 있는 Tomcat에 전달하는 형태입니다.
클라이언트 → Apache httpd (80/443) → AJP (8009) → Tomcat (8080)
AJP 연결 방식은 두 가지입니다:
| 방식 | 모듈 | 특징 |
|---|---|---|
mod_jk | 별도 설치 필요 | 레거시, workers.properties 설정 파일 별도 관리 |
mod_proxy_ajp | Apache 2.2+ 내장 | 설정 단순, 현재 권장 |
mod_proxy_ajp를 쓴다면 ProxyPass 경로만 ajp://로 바꾸면 됩니다:
<VirtualHost *:80>
ServerName legacy-app.example.com
ProxyPreserveHost On
# HTTP 대신 AJP 프로토콜로 Tomcat과 통신
ProxyPass / ajp://127.0.0.1:8009/
ProxyPassReverse / ajp://127.0.0.1:8009/
</VirtualHost>
진단 체크리스트 — 처음 보는 Apache 서버에 들어갔을 때:
# 1. 버전과 MPM 확인
httpd -V | grep -E "Server version|MPM"
# 2. 로드된 모듈 목록
httpd -M 2>/dev/null | sort
# 3. VirtualHost 파싱 결과 요약
sudo apachectl -S 2>&1
# 4. 활성화된 설정 파일 목록
ls /etc/httpd/conf.d/
# 5. 최근 에러 확인
sudo tail -30 /var/log/httpd/error_log
이 순서대로 5개 명령만 실행해도 낯선 Apache 서버의 전체 구조를 5분 안에 파악할 수 있습니다.
명령어·단축키 빠른 참조
이 모듈에서 다룬 Apache httpd 운영·진단 명령을 실전 옵션과 함께 모았습니다. 설정 변경은 항상 검증 후 무중단 반영이 원칙입니다.
| 명령어/단축키 | 용도 | 자주 쓰는 예 |
|---|---|---|
apachectl configtest | 설정 문법 검사(=apachectl -t) | apachectl configtest && apachectl graceful |
apachectl graceful | 무중단 재기동(처리 중 요청 유지) | 마지막 요청 후 종료는 apachectl graceful-stop |
apachectl -S | VirtualHost 매핑 요약 | 어느 도메인이 어느 .conf로 가는지 한눈에 |
httpd -M | 로드된 모듈 목록 | httpd -M | grep proxy (프록시 모듈 확인) |
httpd -V | 버전·MPM·컴파일 옵션 | httpd -V | grep -i mpm (event/prefork 확인) |
a2enmod / a2ensite | (Ubuntu) 모듈·사이트 활성화 | a2enmod proxy proxy_http headers |
systemctl reload apache2 | (Ubuntu) 무중단 반영 | reload=graceful, restart=순단 발생 |
curl -H "Host: ..." | VirtualHost 라우팅 테스트 | curl -H "Host: www.example.com" http://localhost/ |
tail error/access_log | 에러·접근 로그 추적 | tail -30 /var/log/httpd/error_log |
awk … | sort -rn | 슬로우 리퀘스트 추출 | awk '{print $NF,$7}' access.log | sort -rn | head (%D 마지막) |
ss -tan | 동시 연결 수 집계 | ss -tan | grep -c ESTAB |
grep '^User' httpd.conf | Apache 실행 계정 확인 | 403 시 DocumentRoot 소유자와 대조 |
관련 모듈로 더 깊이:
- Nginx 리버스 프록시와 로드밸런싱 설정 — 같은 리버스 프록시를 Nginx로 구성할 때의 설정과 비교 포인트
- WAR 배포부터 server.xml 튜닝, 장애 대응까지 — httpd가 프록시로 넘기는 백엔드 WAS(Tomcat) 운영
- HTTP 에러 코드 해석과 장애 원인 추적 — access/error_log를 패턴 단위로 더 깊이 분석하는 법
다음 모듈에서는 WAR 배포부터 server.xml 튜닝, 장애 대응까지 Tomcat WAS 운영 전반을 다룹니다.