목차
Config Server에 간단한 관리용 페이지를 하나 붙이려 했는데, /tools/properties-manager.html을 요청하자 HTML 대신 JSON 설정 응답이 내려왔어요. Spring REST Docs로 API 문서까지 깔끔하게 정리해뒀는데, 정작 그 문서를 보여줄 페이지도 열리지 않는 상황이었습니다.
처음엔 정적 리소스 경로 설정 문제인 줄 알았습니다. classpath:/static/에 파일도 잘 있고, Spring Boot의 정적 리소스 서빙 설정도 건드린 게 없는데 왜 동작하지 않는 걸까요?
이 글은 그 원인을 Spring MVC의 요청 처리 흐름부터 차근차근 파고든 기록입니다.
Spring Cloud Config Server의 HTTP API 구조
먼저 Config Server가 어떤 경로를 점유하고 있는지 알아야 합니다.
Config Server는 @EnableConfigServer 어노테이션 하나로 활성화되지만, 내부적으로는 EnvironmentController라는 @RestController가 여러 경로 패턴을 등록해요. GitHub 소스를 보면 클래스 선언부가 이렇게 되어 있어요.
@RestController
@RequestMapping(method = RequestMethod.GET, path = "${spring.cloud.config.server.prefix:}")
public class EnvironmentController {
...
}
클래스 레벨에서 prefix를 잡고, 메서드마다 아래와 같은 경로들이 추가로 붙습니다.
/{name}/{profiles}
/{name}/{profiles}/{label}
/{name}-{profiles}.properties
/{name}-{profiles}.yml
/{label}/{name}-{profiles}.properties
/{label}/{name}-{profiles}.yml
...
{name}과 {profiles}는 경로 변수(path variable)이기 때문에, 어떤 문자열이 들어와도 매칭될 수 있습니다.
즉, /tools/config-manager.html을 요청하면 Config Server 입장에서는 이렇게 해석할 수 있어요.
/{name}/{profiles}패턴과 매칭되어name=tools,profiles=config-manager.html로 인식
그런데 왜 정적 리소스보다 이쪽이 먼저 선택되는 걸까요?
DispatcherServlet의 동작 방식
Spring MVC에서 HTTP 요청이 들어오면 DispatcherServlet이 가장 먼저 받습니다. DispatcherServlet은 요청을 직접 처리하지 않고, 등록된 HandlerMapping 목록을 순서대로 순회하면서 이 요청을 처리할 수 있는 핸들러를 찾아요.
마치 공항의 탑승구 안내 직원 같은 역할이에요. 티켓(요청 URL)을 보고 “몇 번 게이트(핸들러)로 가세요”를 알려주는 거죠.
HandlerMapping은 Ordered 인터페이스를 구현해서 우선순위를 가질 수 있습니다. 숫자가 낮을수록 먼저 확인해요. 주요 구현체의 기본 order 값은 아래와 같습니다.
| HandlerMapping | Order | 역할 |
|---|---|---|
RequestMappingHandlerMapping | 0 | @RequestMapping 처리 |
SimpleUrlHandlerMapping (정적 리소스) | Integer.MAX_VALUE - 1 | 정적 파일 서빙 |

@RequestMapping으로 등록된 컨트롤러가 항상 정적 리소스보다 먼저 확인된다는 의미예요. Config Server의 EnvironmentController도 @RestController + @RequestMapping으로 등록된 빈이기 때문에 같은 규칙을 따라요.
깊이 1 경로는 어떨까?
여기서 한 가지 실험을 해봤어요.
/tools/index.html→ Config 응답 반환 (실패)/index.html→ 정적 페이지 응답 (성공)
같은 RequestMappingHandlerMapping이 먼저 확인하는데, 왜 결과가 다를까요?
이건 EnvironmentController가 등록한 경로 패턴의 구조에 달려 있습니다.
Config Server의 패턴들을 보면 대부분 /{variable1}/{variable2} 또는 /{variable1}-{variable2}.properties 와 유사한 형태예요. 특정 파일명 패턴에 완전히 일치하거나 최소한 깊이 2 이상의 경로 패턴입니다.
/tools.html은 슬래시 없이 단일 세그먼트이기 때문에, /{name}/{profiles} 같은 패턴에 매칭되지 않아요. Config Server의 어떤 패턴도 이 경로를 잡지 못하면, DispatcherServlet은 다음 HandlerMapping으로 넘어가고 결국 정적 리소스 핸들러가 응답하게 되죠.
반면 /tools/index.html은 두 세그먼트(tools, properties-manager.html)로 쪼개지기 때문에 /{name}/{profiles} 패턴에 매칭됩니다. 그래서 Config Server 패턴이 먼저 이 요청을 낚아채는 거예요.
실제로 curl로 요청해서 응답을 확인해보면 차이가 명확해요.
# 깊이 2 경로 — Config Server가 가로챔
$ curl http://localhost:8888/tools/index.html
{
"name": "tools",
"profiles": ["index.html"],
"label": null,
"version": null,
"state": null,
"propertySources": []
}
# 깊이 1 경로 — 정적 리소스 핸들러가 응답
$ curl http://localhost:8888/tools.html
<!DOCTYPE html>
<html>
<head><title>Tools</title></head>
<body>...</body>
</html>
/docs/index.html에서 name=docs, profiles=index.html로 파싱된 걸 볼 수 있어요. Config Server 입장에서는 완벽히 유효한 Properties 조회 요청이에요. propertySources가 비어있는 건 요청에 해당하는 설정 값이 실제로 없기 때문입니다.
참고: Spring Boot 2.6부터 경로 매칭 전략이 기존
AntPathMatcher에서PathPatternParser로 변경되었어요. 두 방식 모두 패턴 특이성(specificity) 기반으로 더 구체적인 패턴을 우선 선택하지만, 이 경우처럼 경로 변수가 포함된 패턴은 와일드카드처럼 넓은 범위로 매칭될 수 있어요.
해결 방향과 트레이드오프
원인을 알았으니, 해결 방법을 살펴볼게요. 각각 장단점이 있습니다.
1. 단일 세그먼트 경로로 타협
/tools/properties-handler.html 대신 /properties-handler.html처럼 경로 깊이를 1로 유지하는 방법이에요.
| 장점 | 단점 |
|---|---|
| 별도 설정 없이 즉시 동작 | URL 구조가 제한되어 관리가 불편할 수 있음 |
| 파일이 많아지면 루트에 HTML이 쌓이는 구조가 됨 |
2. prefix 설정 적용
Config Server의 모든 엔드포인트에 prefix를 붙이는 방법이에요.
spring:
cloud:
config:
server:
prefix: /config
공식 문서에 따르면 이 설정은 EnvironmentController를 포함한 Config Server의 모든 @RequestMapping에 적용돼요. 즉 기존 /{name}/{profiles} 패턴이 /config/{name}/{profiles}로 이동하기 때문에, /docs/index.html 같은 경로가 Config Server 패턴과 충돌하지 않게 돼요.
단, Config Client들도 접속 주소를 바꿔줘야 해요.
# Config Client 설정 변경
spring:
config:
import: "optional:configserver:http://localhost:8888/config"
| 장점 | 단점 |
|---|---|
| 경로 충돌을 구조적으로 해결 | 기존 Config Client 설정 변경 필요 (운영 중 변경은 부담될 수 있음) |
| URL 구조를 자유롭게 설계 가능 |
3. 정적 페이지 서버 분리
Config Server와 정적 페이지 서빙을 아예 다른 서버로 분리하는 방법이에요. Config Server는 설정 전용으로만 쓰고, 관리 UI는 별도 서버(혹은 Nginx)에서 서빙해요.
| 장점 | 단점 |
|---|---|
| 관심사 분리로 구조가 깔끔해짐 | 운영 복잡도 증가 |
| Config Server에 불필요한 의존성이 없어짐 | 작은 관리용 페이지를 위해 서버를 늘리는 건 과한 선택일 수 있음 |
정리
Spring Cloud Config Server에 정적 페이지를 올릴 때 발생하는 경로 충돌은 사실 Spring MVC의 HandlerMapping 우선순위 구조 때문에 발생하는 현상이에요.
@RequestMapping(order=0)은 정적 리소스 핸들러(order=MAX-1)보다 항상 먼저 확인된다. Config Server의 경로 변수 패턴이 예상치 못한 URL을 매칭할 수 있다.
상황에 따른 선택 기준은 다음과 같습니다.
- 빠른 해결이 필요하다 → 단일 세그먼트 경로로 타협
- URL 구조를 자유롭게 쓰고 싶다 →
spring.cloud.config.server.prefix설정 - Config Server는 Properties 서빙 전용이어야 한다 → 서버 분리 고려
