idea·blog

고른다, 합친다, 정렬한다 — nginx·Apache·Caddy 의 요청 매칭

같은 요청 경로 하나를 넣어도 세 웹서버는 서로 다른 설정 블록을 고릅니다. 각 서버 공식 문서에 적힌 규칙을 따라가며 탈락 과정까지 눌러봅니다.

2026.08.05작성 idea-blog 운영자· 약 5
고른다, 합친다, 정렬한다 — nginx·Apache·Caddy 의 요청 매칭 스크린샷

웹서버 설정은 "맞는 규칙을 찾는다"가 아닙니다. nginx 는 우선순위로 하나를 고르고, Apache 는 맞는 섹션을 전부 합치고, Caddy 는 설정을 먼저 정렬한 뒤 첫 매칭 하나만 실행합니다.

세 서버 모두 "경로에 규칙을 붙인다"는 점은 같습니다. 그런데 규칙이 여러 개 겹칠 때 무슨 일이 일어나는지는 완전히 다릅니다. 이 차이를 모르면 분명히 써 둔 설정이 왜 동작하지 않는지 알 수 없습니다.

같은 경로를 세 서버에 넣어보기

nginx — 긴 게 이기는 게 아닙니다

가장 흔한 오해입니다. "더 구체적인 규칙이 이긴다"고 생각하기 쉽지만, nginx 의 순서는 그렇게 단순하지 않습니다. 공식 문서가 설명하는 순서는 이렇습니다.

  1. = 로 시작하는 정확히 일치를 먼저 본다. 걸리면 그 자리에서 끝난다.
  2. 전위(prefix) 규칙 중 가장 긴 것을 기억해 둔다.
  3. 기억해 둔 것이 ^~정규식은 검사조차 하지 않는다.
  4. 아니면 정규식을 설정 파일에 쓴 순서대로 검사하고, 첫 매칭에서 멈춘다.
  5. 맞는 정규식이 없을 때만 2번에서 기억해 둔 전위를 쓴다.

핵심은 4번입니다. 정규식이 하나라도 걸리면, 그것이 더 긴 전위 규칙을 이깁니다.

데모의 설정에 /api/v1/users.php 를 넣어보면 이렇게 됩니다.

location /api/v1/        → 후보 (가장 긴 전위지만 진다)
location ~ \.php$        → 적용됨  ← 승자
location ~* \.(js|css)$  → 검사 안 함 (정규식 검색이 이미 끝남)

/api/v1/ 이 여덟 글자로 가장 길지만, ~ \.php$ 정규식에 밀립니다. nginx 공식 문서의 예제도 같은 구조입니다 — /documents/1.jpg 요청은 location /documents/ 가 아니라 location ~* \.(gif|jpg|jpeg)$ 로 갑니다.

반대로 ^~ 를 붙이면 정규식을 아예 막을 수 있습니다. 데모에 /static/app.js 를 넣으면 ^~ /static/ 이 걸리고, ~* \.(js|css)$분명히 맞는데도 검사되지 않습니다.

Apache — 고르는 게 아니라 합칩니다

Apache 는 승자를 뽑지 않습니다. 매칭되는 섹션이 전부 적용되고, 문서화된 순서로 합쳐집니다.

  1. <Directory> (정규식 아닌 것) 와 .htaccess
  2. <DirectoryMatch>
  3. <Files><FilesMatch>
  4. <Location><LocationMatch>
  5. <If>

<Directory> 는 한 가지가 더 특이합니다. 설정 파일에 쓴 순서가 아니라 짧은 경로부터 처리됩니다. <Directory "/var/web/dir"><Directory "/var/web/dir/subdir"> 보다 항상 먼저입니다.

데모에 /api/v1/users.php 를 넣으면 여섯 개 섹션이 병합됩니다. 하나가 이기는 게 아니라 여섯 개가 겹겹이 쌓입니다.

여기서 또 하나 주의할 점 — 합치는 방식은 지시어마다 다릅니다. 공식 문서는 매칭된 다음 섹션이 나오면 "각 모듈이 자신의 설정을 병합할 기회를 받는다"고 설명합니다. 즉 무조건 나중 것이 앞의 것을 덮는 게 아닙니다. 데모가 쓰는 두 지시어만 봐도 규칙이 다릅니다.

  • Header set같은 이름의 헤더를 교체합니다. <Location "/api"> 가 정한 X-Space<LocationMatch "^/api/v1/"> 가 덮어씁니다.
  • Options접두사가 있느냐에 따라 갈립니다. 이름만 쓰면 상속받은 목록을 통째로 교체하고, + / - 를 붙이면 상속과 합칩니다. 데모에서는 Indexes FollowSymLinks 로 깔아둔 뒤 -Indexes 로 빼서 FollowSymLinks 만 남습니다.

그리고 무엇에 대고 맞춰보는지가 섹션마다 다릅니다. <Directory>파일시스템 경로, <Files>파일 이름, <Location>URL 입니다. 그래서 <Location> 은 대응 파일이 없어도 걸립니다. 데모에 /healthz 를 넣으면 그런 파일이 없는데도 <Location "/healthz"> 가 적용됩니다.

여섯 개 섹션이 어떻게 겹치는지 보기

Caddy — 쓴 순서가 실행 순서가 아닙니다

Caddy 는 앞의 둘과 또 다릅니다. Caddyfile 을 읽어서 실행 구조로 바꿀 때, 어댑터가 먼저 순서를 다시 정합니다.

그래서 이렇게 써도

handle /api/* {
	reverse_proxy api:3000
}
handle /api/v1/* {
	reverse_proxy v1:3000
}

실제 실행 순서는 /api/v1/*앞으로 옵니다. 더 구체적인 경로가 먼저 오도록 정렬해 주기 때문입니다. caddy adapt 로 변환 결과를 직접 확인할 수 있습니다.

즉 "먼저 쓴 게 이긴다"가 아닙니다. 오히려 Caddy 는 순서를 신경 쓰지 않아도 되게 도와줍니다.

그런데 route 로 감싸는 순간 그 도움이 사라집니다. 공식 문서는 route 에 대해 "위의 모든 규칙을 무시하고, 디렉티브가 나타난 순서를 보존한다"고 못 박습니다.

route {
	handle /api/* { ... }
	handle /api/v1/* { ... }
}

이제 /api/* 가 먼저 걸리고, handle 은 상호배타적이라 /api/v1/*영원히 실행되지 않습니다. 같은 내용인데 route 하나로 결과가 뒤집힙니다. 데모의 Caddy 화면에서 두 형태를 전환해 보면 바로 보입니다.

마지막으로 대소문자입니다. Caddy 의 path matcher 는 공식 문서에 따르면 "정확히 일치하되 대소문자를 구분하지 않습니다". 반면 nginx 의 전위 매칭은 Linux 에서 대소문자를 구분합니다. 그래서 /Static/app.js 하나로 두 서버의 결과가 갈립니다.

/static/app.js/Static/app.js
nginx^~ /static/~* \.(js|css)$
Caddyhandle /static/*handle /static/*

세 서버 한눈에 보기

같은 /api/v1/users.php 를 넣었을 때입니다.

서버방식결과
nginx선택location ~ \.php$ 하나가 선택됨
Apache병합6개 섹션이 병합됨 (승자가 아니라 합계)
Caddy (일반)정렬 후 분기handle /api/v1/* 가 실행됨
Caddy (route)순서 보존handle /api/* 가 실행됨

같은 경로, 같은 의도로 쓴 설정인데 결말이 넷으로 갈립니다.

이 데모의 한계

데모는 실제 서버를 실행하지 않습니다. 각 서버 공식 문서에 적힌 매칭 규칙만 재현한 교육용 모델입니다.

다루지 않는 것들이 있습니다. Alias, .htaccess, nginx 의 try_files 재진입, rewrite 이후 재매칭, 가상호스트 선택, 그리고 %XX 인코딩·..·연속 슬래시의 정규화는 범위 밖입니다. 서버마다 URI 정규화 방식이 달라서 하나의 모델로 흉내내면 오히려 틀린 답을 주기 때문에, 데모 입력은 안전한 ASCII 경로로 제한했습니다.

nginx 의 전위 매칭 대소문자는 Linux 기준입니다. 공식 문서는 macOS·Cygwin 같은 대소문자를 구분하지 않는 운영체제에서는 전위 매칭도 대소문자를 무시한다고 밝히고 있습니다.

Caddy 정렬은 한 가지 단서가 더 있습니다. 길이가 같은 matcher 사이의 타이브레이크 규칙까지는 문서로 확정할 수 없어서, 데모에 실린 설정에 대해 caddy adapt(v2.11.4)로 확인한 순서를 그대로 씁니다. 규칙을 지어내지 않았다는 뜻이기도 하고, 다른 설정에는 그대로 적용되지 않는다는 뜻이기도 합니다.

실제 설정을 검증하려면 이 데모가 아니라 서버에 직접 물어야 합니다. 주의할 점은 nginx -Tapachectl -S특정 요청의 승자를 보여주지 않는다는 겁니다. 앞의 것은 설정 덤프, 뒤의 것은 가상호스트 목록입니다. 확실한 방법은 각 블록이 서로 다른 응답 헤더나 본문을 내도록 임시로 바꿔두고 curl 로 확인하는 것입니다. Caddy 는 caddy adapt 로 정렬 결과까지 볼 수 있습니다.

참고 자료

#웹서버#nginx#Apache#Caddy#학습도구#네트워크

관련 글