웹 경로 구조 정리: 정적 리소스, 뷰 템플릿 경로 구분

2025. 5. 9. 14:10Spring/MVC

1. 상대경로 vs 절대 경로

1) 상대경로 vs 절대경로

  • 상대경로(relative path): 현재 파일의 위치를 기준으로 경로를 지정하는 방식
    예: ../css/style.css, ./image/logo.png
  • 절대경로(absolute path): 웹 루트(/)를 기준으로 경로를 지정하는 방식
    예: /css/style.css, /images/logo.png

2) 상대경로 문법 

문법 의미
./file  현재 폴더의 file
../file 부모 폴더의 file
file 기본적으로 ./file처럼 동작 (현재 폴더 기준)

3) 언제 어떤 경로를 써야 할까?

  • 브라우저가 HTML을 로컬 파일로 직접 열 경우: 상대경로 사용 가능
  • 서버가 HTML을 응답으로 렌더링해줄 경우 (ex. 타임리프): 절대경로 사용이 안전함
<!-- 상대경로 -->
<link href="../css/style.css" rel="stylesheet">

<!-- 절대경로 (웹 루트 기준) -->
<link th:href="@{/css/style.css}" rel="stylesheet">

2. 정적 리소스와 뷰 템플릿 파일 경로의 분리 

1) 파일 경로 분리

서버에서는 웹 브라우저에서 직접 접근이 가능한 리소스와 그렇지 않은 리소스의 경로를 따로 구분해야한다. 

  • 정적 리소스(이미지, CSS, JS 등): 웹 브라우저에서의 직접 접근 가능
  • 템플릿 파일(HTML, JSP 등)
    : 보안을 위해 직접 접근을 막고,
    반드시 Controller를 통해 서버 내부에서 렌더링하여 전달해야 한다.

2) 정적 리소스: URL 요청 

  • 각 프로젝트는 웹 루트 기준의 정적 리소스 경로를 webapp/, static/ 등으로 정해두고 
  • 이 디렉토리들을 기준으로 브라우저가 리소스를 요청한다.
  • 예시: src/main/resources/static/image.jpg에 위치 → http://localhost:8080/image.jpg로 직접 요청 

3) 템플릿 파일: 컨트롤러 처리 후 렌더링되어 전달 

//jsp 파일 forward
RequestDispatcher dispatcher 
		= request.getRequestDispatcher("/WEB-INF/views/items.jsp");
dispatcher.forward(request, response);

//스프링 사용해서 렌더링
@GetMapping("/items")
public String items(Model model) {
    model.addAttribute("items", itemService.findAll());
    return "basic/items"; // → templates/basic/items.html 렌더링
}

3. WAR 구조와 JAR 구조의 차이

1) .war와 .jar

  • .war 파일 
    • JSP, HTML, 서블릿 클래스들, 설정파일(web.xml) 등이 압축된 웹 애플리케이션 패키지
    • Tomcat 같은 WAS 내부에 .war 파일이 위치해 있어 톰캣이 해당 .war 파일을 해석하고 실행한다. 
  • .jar 파일
    • 애플리케이션 안에 WAS가 들어있는 형태
    • Spring Boot는 .jar로 전체 웹앱을 구성 가능 → 별도 WAS에 배포할 필요 없이 java -jar로 실행

2) 순수 서블릿으로 개발 시 경로 구조 (WAR 구조) 

  • WAR 구조는 외부 WAS에 배포하는 앱의 구조
  • 정적 리소스 경로: src/main/webapp 
  • 뷰 템플릿 경로: src/main/webapp/WEB-INF
  • JSP는 WAR 구조와 잘 맞는 템플릿 엔진
    • 서블릿 스펙 기반이라 WAS가.jsp 파일을 JSP 엔진으로 직접 처리할 수 있음
    • 따라서 JSP 파일은 보통 src/main/webapp/WEB-INF/views/에 위치한다.
    • JAR 구조(Spring Boot)에서는 webapp/이 기본적으로 없기 때문에 JSP 쓰려면 추가 설정 필요 

3) 스프링 부트 사용시 경로 구조 (JAR 구조)

  • JAR 구조는 내장 WAS 포함한 실행형 앱의 구조
  • 스프링 부트는 클래스패스 리소스 디렉토리를 자동으로 매핑해준다.
    webapp/ 없이도 static/에서 바로 정적 리소스를 제공할 수 있게 해준다.
  • 정적 리소스 경로: src/main/resources/static/
  • 뷰 템플릿 경로: src/main/resources/templates/
  • 타임리프는 JAR 구조와 잘 맞는 템플릿 엔진
    • 타임리프 파일은 보통 resources/templates/에 위치함 
    • 별도 WAS 없이도 내장 톰캣 + JAR 구조로 실행 가능

4) 스프링 부트에서 정적 리소스 경로 추가 설명 

  • 스트링 부트에서 제공하는 정적 리소스의 경로는 사실 아래와 같이 총 4가지이다. 
src/main/resources/META-INF/resources/
src/main/resources/resources/
src/main/resources/static/
src/main/resources/public/
  • 정적 리소스 경로 우선순위
    META-INF/resources/ → resources/ → static/ → public/
    위 디렉토리들을 순서대로 뒤지다가 처음 발견된 이미지를 반환하고 나머지는 무시된다. 
  • 스프링 부트는 과거와 호환성 + 유연성 + 라이브러리 호환 등을 위해 여러 경로를 기본적으로 지원해주지만, 
    실무에서는 헷갈리지 않도록 static 폴더 하나만 사용한다.

4. 스프링 MVC에서 URL이 처리되는 흐름 

1) 웹 브라우저에서 URL 요청 

1. 웹 브라우저에서 GET 요청 들어옴
[예시] 
/css/bootstrap.min.css
/members

2. DispatcherServlet 앞단에서 정적 리소스 핸들러가 먼저 동작함
DispatcherServlet 타기 전에 정적 리소스를 찾을 수 있는지 먼저 확인

아래 경로를 순서대로 검색 (classpath 기준)
/META-INF/resources/, /resources/, /static/, /public/
→ 만약 이 중에 있다면? 바로 서빙하고 응답 종료
→ 없으면? DispatcherServlet에게 요청 전달 

[예시]
/css/bootstrap.min.css → /static/css/에 존재 → 바로 서빙 후 응답 종료
/members → 해당 정적 리소스 없음 → DispatcherServlet으로 넘어감

3. DispatcherServlet이 Controller 매핑 시도
[예시] 
/members → HandlerMapping이 요청 URL에 맞는 @RequestMapping 메서드를 찾고 실행

 

2) 컨트롤러 처리 결과 응답 

1. 컨트롤러에서 뷰 이름 반환 
@GetMapping("/items")
public String items(Model model) {
    model.addAttribute("items", itemService.findAll());
    return "basic/items"; 
}

2. DispatcherServlet에서 ViewResolver 호출 

3. ViewResolver가 논리 뷰 이름 → 물리 뷰 경로로 변환
- JSP 사용 시: "basic/items" → "webapp/WEB-INF/basic/items.jsp"
- Thymeleaf 사용 시: "basic/items" → "templates/basic/items.html"
그리고 렌더링 역할을 담당하는 View 객체(InternalResourceView, ThymeleafView 등) 반환

4. View 객체의 render() 메서드 호출
JSP: 내부적으로 RequestDispatcher.forward() 호출
Thymeleaf: 템플릿 파일 직접 읽고 HTML 렌더링

5. WAS가 클라이언트에게 응답 전송
View 객체가 생성한 HTML을 response.getWriter().write(...)로 작성
DispatcherServlet 종료 → WAS(Tomcat 등)가 클라이언트에게 응답 전송