// Developer · Debugging Reports

뉴스도 디버깅이 필요해

마주친 오류를 재현하고, 원인을 추적하고, 기록합니다. Java · Spring · DB · 실무 트러블슈팅.

debug — today.log
$ debug --trace "오늘의 뉴스"
> 헤드라인 파싱 중...
> 원인 추적: 근거 누락 · 출처 불명
> 패치 적용: 사실 확인 + 출처 보강
✓ RESOLVED
TOTAL
TODAY
YESTERDAY

Categories

웹개발/Spring

Spring MVC resources 404 에러 원인과 해결 방법 정리

자바를잡아2026. 6. 24. 23:40
반응형

Spring MVC resources 404 문제 상황

Spring MVC 프로젝트에서 화면은 뜨는데 CSS, JavaScript, 이미지 파일만 적용이 안 되는 경우가 있다. 브라우저 개발자도구 Network 탭을 열어 보면 해당 파일 요청이 모두 404로 찍혀 있다. JSP 자체는 정상적으로 렌더링되는데 정적 리소스만 못 찾는 전형적인 Spring MVC resources 404 상황이다.

로그에는 보통 이런 식으로 남는다.

WARN  o.s.web.servlet.PageNotFound - No mapping found for HTTP request with URI [/resources/css/style.css] in DispatcherServlet

여기서 핵심은 in DispatcherServlet이라는 문구다. 정적 파일 요청까지 DispatcherServlet이 받아서 처리하려다 매핑을 못 찾고 404를 낸 것이다. 원인은 대부분 설정 한두 군데에 몰려 있다.

원인 요약

원인 설명 확인 방법
리소스 핸들러 미설정 servlet-context.xml에 mvc:resources 또는 default-servlet-handler가 없음 설정 파일에 resources mapping 라인이 있는지 확인
DispatcherServlet / 매핑 url-pattern이 /라서 정적 파일 요청까지 전부 받음 web.xml의 servlet-mapping 확인
실제 파일 위치 불일치 location 경로와 파일이 실제로 놓인 위치가 다름 webapp 기준 실제 디렉토리 구조 확인
JSP 경로 문제 contextPath 누락으로 요청 URL이 어긋남 Network 탭에서 실제 요청 URL 확인
mapping 패턴 오타 /** 누락 등 패턴이 하위 경로를 못 잡음 mapping 값과 요청 경로 비교

해결 방법

1. servlet-context.xml에 리소스 매핑 추가

가장 흔한 원인이다. 정적 리소스를 DispatcherServlet이 처리하지 않고 그냥 파일로 내려주도록 매핑을 잡아 줘야 한다. XML 네임스페이스 설정 방식이라면 servlet-context.xml(또는 dispatcher 설정 파일)에 아래 라인을 추가한다.

<!-- servlet-context.xml -->
<!-- /resources/ 로 시작하는 요청은 webapp/resources/ 폴더에서 찾는다 -->
<mvc:resources mapping="/resources/**" location="/resources/" />

여기서 mapping은 요청 URL 패턴, location은 실제 파일이 있는 물리 경로다. /resources/**처럼 /** 를 빠뜨리면 하위 폴더의 파일을 못 잡으니 주의한다. 프로젝트가 어노테이션 기반(Java Config)이라면 WebMvcConfigureraddResourceHandlers에서 동일하게 잡는다.

// Java Config 방식
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/resources/**")
            .addResourceLocations("/resources/");
}

설정 방식이 XML인지 Java Config인지에 따라 위치가 다르므로, 프로젝트가 어느 쪽인지 먼저 확인하는 것이 좋다.

2. web.xml의 DispatcherServlet 매핑 점검

404가 나는 근본 배경은 DispatcherServlet의 매핑 범위다. web.xml에서 다음과 같이 /로 잡혀 있으면 모든 요청을 DispatcherServlet이 받는다.

<!-- web.xml -->
<servlet-mapping>
    <servlet-name>appServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

이 상태에서는 정적 파일 요청도 컨트롤러를 찾으려다 실패한다. 그래서 1번의 mvc:resources 설정이 반드시 같이 있어야 한다. 둘은 세트로 본다. *.do처럼 확장자 매핑을 쓰는 레거시 구조라면 정적 리소스가 애초에 DispatcherServlet을 안 거치므로 이 문제는 잘 안 생긴다.

3. mvc:resources가 없을 때 default-servlet-handler 사용

리소스 경로가 여러 개로 흩어져 있어 일일이 매핑하기 번거롭다면, WAS의 기본 서블릿에게 정적 파일 처리를 넘기는 방법도 있다.

<!-- servlet-context.xml -->
<mvc:default-servlet-handler/>

이 설정은 DispatcherServlet이 못 찾은 요청을 톰캣 기본 서블릿으로 위임한다. 다만 <mvc:annotation-driven/>이 함께 있어야 컨트롤러 매핑이 정상 동작하므로, 둘 중 하나만 넣고 끝내지 않도록 한다.

4. JSP에서 경로(contextPath) 확인

설정은 맞는데도 404가 난다면 요청 URL 자체가 어긋났을 가능성이 크다. JSP에서 /resources/...를 슬래시 없이 상대경로로 쓰면 현재 페이지 기준으로 경로가 꼬인다. context path를 명시해 주는 편이 안전하다.

<%-- JSP, contextPath 명시 --%>
<link rel="stylesheet" href="${pageContext.request.contextPath}/resources/css/style.css">

<%-- 또는 JSTL c:url 사용 --%>
<link rel="stylesheet" href="<c:url value='/resources/css/style.css'/>">

운영 서버는 context path가 /myapp처럼 붙고 로컬은 ROOT(/)로 뜨는 경우가 많다. 그래서 로컬에서는 되는데 운영에서만 404가 나는 일이 흔하다. Network 탭에서 실제 요청 URL을 보고, 앞에 context path가 빠지지 않았는지 확인한다.

5. 빌드·배포본에 리소스가 포함됐는지 확인

설정과 경로가 다 맞는데도 404가 난다면, 정적 파일이 실제 배포 결과물에 빠진 경우를 의심한다. IDE에서는 보이지만 WAR나 배포 디렉토리에는 포함되지 않는 일이 있다. 빌드 후 배포 디렉토리에서 resources/css/style.css가 실제로 존재하는지 직접 확인한다.

# 배포 디렉토리에서 파일 존재 여부 확인 (경로는 환경에 맞게 수정)
ls -al /usr/local/tomcat/webapps/myapp/resources/css/

운영 서버에서 경로를 확인할 때는 권한과 실제 배포 경로를 먼저 확인한다. 파일이 없다면 빌드 설정(리소스 포함 경로)이나 배포 절차를 점검해야 한다.

실무에서 자주 놓치는 부분

  • 설정 파일이 둘 이상인 경우가 있다. web.xml에서 ContextLoaderListener용 root context와 DispatcherServlet용 servlet context를 구분하는데, mvc:resourcesservlet context 쪽에 있어야 한다.
  • location 경로 끝의 슬래시를 빠뜨리는 실수가 잦다. location="/resources/"처럼 끝에 /를 붙인다.
  • 브라우저 캐시 때문에 옛 404 응답이 남아 있을 수 있다. 강력 새로고침으로 다시 확인한다.
  • 여러 명이 같은 설정 파일을 고치다 매핑이 중복되거나 덮어써진 경우가 있다. 형상 이력을 한 번 확인하면 원인이 빨리 보인다.

로그와 Network 탭으로 원인 좁히기

원인을 빨리 좁히려면 두 곳을 같이 본다. 먼저 브라우저 Network 탭에서 404 난 파일의 Request URL을 확인한다. 이 URL이 mvc:resources의 mapping 패턴과 맞는지, context path가 정상으로 붙었는지 비교한다. 그다음 톰캣 로그(catalina.out 또는 콘솔)에서 No mapping found 메시지의 URI를 본다. Network의 요청 URL과 로그의 URI가 일치한다면 설정 매핑 문제, JSP 화면 소스의 경로와 다르다면 경로 작성 문제로 방향을 잡을 수 있다.

FAQ

Q1. mvc:resources를 추가했는데도 여전히 404가 납니다.

mapping 패턴과 실제 요청 URL이 일치하는지부터 확인한다. mapping="/resources/**"인데 화면에서는 /static/...으로 요청한다면 당연히 안 잡힌다. 또한 해당 설정이 root context가 아니라 DispatcherServlet의 servlet context에 있는지 확인한다.

Q2. 로컬에서는 잘 되는데 운영 서버에서만 404가 납니다.

context path 차이일 가능성이 높다. 로컬은 ROOT로 뜨고 운영은 /앱이름으로 배포되는 경우가 많다. JSP에서 ${pageContext.request.contextPath}를 붙였는지 확인한다.

Q3. mvc:resources와 default-servlet-handler 중 무엇을 써야 하나요?

리소스 경로가 명확하면 mvc:resources로 명시하는 편이 통제하기 좋다. 경로가 여러 곳이고 톰캣 기본 처리에 맡겨도 되면 default-servlet-handler가 편하다. 환경에 따라 다를 수 있으니 한 가지로 정해 일관성을 유지하는 것을 권한다.

Q4. resources 폴더는 webapp 아래 어디에 둬야 하나요?

location="/resources/" 기준이면 src/main/webapp/resources/ 아래에 둔다. 빌드 후 배포 디렉토리에도 동일 경로로 포함되는지 확인한다.

Q5. JSP는 잘 뜨는데 CSS만 안 먹는 이유가 뭔가요?

JSP는 컨트롤러를 통해 정상 렌더링됐지만, 그 안의 정적 파일 요청은 별도 HTTP 요청으로 다시 들어온다. 이 요청이 리소스 매핑에 안 걸리면 화면은 나오는데 스타일만 깨진 상태가 된다.

정리

Spring MVC resources 404는 대부분 정적 리소스 매핑 설정DispatcherServlet 매핑 범위, 그리고 JSP의 경로 작성 세 가지에서 원인을 찾을 수 있다. 먼저 servlet-context.xmlmvc:resources가 있는지 보고, Network 탭에서 실제 요청 URL과 mapping 패턴이 맞는지 비교한다. 로컬과 운영이 다르게 동작하면 context path를 의심하고, 그래도 안 되면 배포본에 파일이 실제로 포함됐는지 확인한다. 설정이 XML인지 Java Config인지, 톰캣 버전이 무엇인지에 따라 세부 위치가 다를 수 있으니, 운영 반영 전에는 테스트 환경에서 먼저 확인하는 것이 안전하다.

반응형