고객사 서버 점검 의뢰를 받아 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 — 기존 설정 자산
- 레거시 유지: 수십 년 된 배포를 건드리지 않는 조직 다수
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 configtestapachectl configtest출력 마지막 줄에Syntax OK가 나왔는가/etc/httpd/conf.d/example.conf파일이 생성됐고ServerName/DocumentRoot지시어가 포함됐는가/var/www/example디렉터리가 존재하는가 (ls /var/www/example)
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/var/www/example/index.html파일이 존재하고 내용이 올바른가 (cat /var/www/example/index.html)ls -la /var/www/example/에서 소유자가apache(또는www-data)로 나왔는가stat /var/www/example에서 권한이 755로 설정됐는가
검증이 통과하면 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'가 나왔는가
- curl -H 'Host: www.example.com' http://localhost/ 응답에 'Apache VirtualHost 동작 확인'이 포함됐는가
- /var/log/httpd/example-access.log 파일이 생성됐고 요청이 기록됐는가
- apachectl graceful 후 httpd 프로세스가 계속 실행 중인가 (ps aux | grep httpd)
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 출력에 proxy_module과 proxy_http_module이 모두 있는가
- apachectl configtest 후 'Syntax OK'가 나왔는가
- curl -H 'Host: api.example.com' http://localhost/api/health 요청이 백엔드로 전달됐는가
- 백엔드 로그에서 X-Real-IP 헤더로 실제 클라이언트 IP가 찍혔는가
트러블슈팅
원인: 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로 교체해야 합니다.
레거시 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분 안에 파악할 수 있습니다.
다음 모듈에서는 WAR 배포부터 server.xml 튜닝, 장애 대응까지 Tomcat WAS 운영 전반을 다룹니다.