📋 목차
프로젝트 구조
Spring MVC 프로젝트의 전형적인 디렉토리 구조를 먼저 살펴보겠습니다. 각 폴더와 파일의 역할을 이해하는 것이 Spring MVC 동작 원리를 파악하는 첫 걸음입니다.
sp1/
├── src/main/
│ ├── java/
│ │ └── org/zerock/
│ │ ├── controller/
│ │ │ └── HelloController.java # 컨트롤러 (요청 처리)
│ │ ├── service/
│ │ │ └── HelloService.java # 서비스 레이어
│ │ └── dto/
│ │ └── SampleDTO.java # 데이터 전송 객체
│ └── webapp/
│ └── WEB-INF/
│ ├── web.xml # 웹 애플리케이션 설정
│ ├── spring/
│ │ ├── root-context.xml # 루트 컨텍스트 (서비스, DB)
│ │ └── servlet-context.xml # 서블릿 컨텍스트 (MVC)
│ └── views/
│ └── sample/
│ ├── ex1.jsp
│ ├── ex3Result.jsp
│ └── success.jsp
주요 디렉토리 설명
- controller: 사용자 요청을 처리하는 컨트롤러 클래스들이 위치합니다.
- service: 비즈니스 로직을 담당하는 서비스 클래스들이 위치합니다.
- dto: 데이터 전송 객체(Data Transfer Object)로, 계층 간 데이터 전달에 사용됩니다.
- WEB-INF/spring: Spring 설정 파일들이 위치합니다.
- WEB-INF/views: JSP 뷰 템플릿 파일들이 위치합니다.
Spring MVC 아키텍처
Spring MVC의 핵심은 이중 컨텍스트 구조입니다. 이는 웹 계층과 비즈니스 계층을 명확하게 분리하여 애플리케이션의 유지보수성과 확장성을 높입니다.
1. 이중 컨텍스트 구조
graph TB
subgraph "Root ApplicationContext"
RC[Root Context<br/>root-context.xml]
S[Service Layer<br/>HelloService]
DS[DataSource<br/>Database 연결]
RC --> S
RC --> DS
end
subgraph "Servlet ApplicationContext"
SC[Servlet Context<br/>servlet-context.xml]
C[Controller<br/>HelloController]
VR[ViewResolver<br/>JSP 처리]
SC --> C
SC --> VR
end
SC -.부모 참조.-> RC
style RC fill:#e1f5ff
style SC fill:#fff4e1
style S fill:#c8e6c9
style DS fill:#c8e6c9
style C fill:#ffe0b2
style VR fill:#ffe0b2
Root ApplicationContext (root-context.xml)
- 용도: 비즈니스 로직, 서비스, 데이터베이스 관련 Bean
- 스캔 패키지:
org.zerock.service - 주요 Bean:
HelloService(서비스 레이어)hikariConfig(HikariCP 설정)dataSource(데이터베이스 연결)
- 생성 시점: 웹 애플리케이션 시작 시 (ContextLoaderListener)
- 특징: 애플리케이션 전체에서 공유되는 Bean을 관리합니다.
Servlet ApplicationContext (servlet-context.xml)
- 용도: 웹 관련 Bean (컨트롤러, 뷰 리졸버)
- 스캔 패키지:
org.zerock.controller - 주요 Bean:
HelloController(컨트롤러)InternalResourceViewResolver(JSP 뷰 리졸버)
- 생성 시점: DispatcherServlet 초기화 시
- 특징: 웹 요청 처리에 필요한 Bean을 관리하며, Root Context를 부모로 참조할 수 있습니다.
2. 컨텍스트 계층 구조의 이점
graph LR
A[Root Context] --> B[Servlet Context]
B -.참조 가능.-> A
style A fill:#e1f5ff
style B fill:#fff4e1
- Servlet Context는 Root Context의 Bean을 참조할 수 있습니다.
- 이를 통해 컨트롤러에서 서비스 레이어를 주입받아 사용할 수 있습니다.
- 역으로 Root Context는 Servlet Context의 Bean을 참조할 수 없어 계층 분리가 명확합니다.
요청 처리 흐름
Spring MVC의 요청 처리 과정을 단계별로 자세히 살펴보겠습니다.
전체 흐름도
sequenceDiagram
participant B as 브라우저
participant T as Tomcat 서버
participant DS as DispatcherServlet
participant HM as HandlerMapping
participant HA as HandlerAdapter
participant C as Controller
participant VR as ViewResolver
participant V as View (JSP)
B->>T: HTTP GET /ex1
T->>DS: 요청 전달
DS->>HM: 핸들러 조회
HM-->>DS: HelloController.ex1()
DS->>HA: 핸들러 어댑터 조회
HA->>C: ex1() 실행
C-->>HA: "sample/ex1" 반환
HA-->>DS: ModelAndView
DS->>VR: 뷰 이름 전달
VR-->>DS: /WEB-INF/views/sample/ex1.jsp
DS->>V: JSP 렌더링
V-->>DS: HTML 응답
DS-->>T: 응답 전달
T-->>B: HTTP Response
이 다이어그램은 실제 Spring MVC의 요청 처리 과정을 시간 순서대로 보여줍니다. 각 단계마다 어떤 컴포넌트가 관여하는지 명확하게 파악할 수 있습니다.
단계별 상세 설명
1단계: 요청 수신 (DispatcherServlet)
<!-- web.xml -->
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern> <!-- 모든 요청을 DispatcherServlet이 처리 -->
</servlet-mapping>
- 모든 HTTP 요청(
/로 시작하는 모든 경로)이 DispatcherServlet으로 전달됩니다. DispatcherServlet은 Spring MVC의 프론트 컨트롤러(Front Controller) 역할을 합니다.- 모든 요청을 중앙에서 받아 적절한 핸들러로 분배합니다.
2단계: 핸들러 매핑 (HandlerMapping)
@Controller
public class HelloController {
@GetMapping("/ex1") // URL 패턴 매핑
public String ex1() { ... }
}
HandlerMapping이 요청 URL(/ex1)에 매핑된 컨트롤러 메서드를 찾습니다.@GetMapping("/ex1")어노테이션을 통해 URL과 메서드를 연결합니다.servlet-context.xml의<mvc:annotation-driven/>이 어노테이션 기반 매핑을 활성화합니다.
3단계: 핸들러 어댑터 (HandlerAdapter)
- 찾아낸 핸들러(컨트롤러 메서드)를 실행하기 위한 어댑터를 선택합니다.
@Controller+@GetMapping조합은RequestMappingHandlerAdapter가 처리합니다.- 어댑터 패턴을 사용하여 다양한 종류의 핸들러를 일관된 방식으로 실행할 수 있습니다.
4단계: 컨트롤러 실행
@GetMapping("/ex1")
public String ex1() {
log.info("========== /ex1 컨트롤러 호출됨 ==========");
return "sample/ex1"; // 뷰 이름 반환
}
- 실제 비즈니스 로직을 처리합니다.
- 처리 결과와 함께 뷰 이름을 반환합니다.
- 필요한 경우 Model에 데이터를 담아 뷰로 전달할 수 있습니다.
5단계: 뷰 리졸버 (ViewResolver)
<!-- servlet-context.xml -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jsp" />
</bean>
- 반환된 뷰 이름
"sample/ex1"을 실제 JSP 파일 경로로 변환합니다:- Prefix:
/WEB-INF/views/ - 뷰 이름:
sample/ex1 - Suffix:
.jsp - 최종 경로:
/WEB-INF/views/sample/ex1.jsp
- Prefix:
6단계: 뷰 렌더링
- JSP 파일을 컴파일하고 동적 데이터를 삽입하여 최종 HTML을 생성합니다.
- 생성된 HTML이 HTTP 응답 본문으로 브라우저에 전송됩니다.
설정 파일 분석
Spring MVC의 동작을 제어하는 주요 설정 파일들을 자세히 살펴보겠습니다.
1. web.xml - 웹 애플리케이션 배포 설명자
<!-- 루트 컨텍스트 설정 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<!-- DispatcherServlet 설정 -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
주요 구성 요소:
- ContextLoaderListener: 서버 시작 시 Root ApplicationContext를 생성합니다.
- DispatcherServlet: 모든 HTTP 요청을 받아 처리하는 프론트 컨트롤러입니다.
- load-on-startup=“1”: 서버 시작 시 즉시 서블릿을 초기화합니다.
- url-pattern=”/”: 정적 파일을 제외한 모든 요청을 DispatcherServlet이 처리합니다.
2. servlet-context.xml - 웹 계층 설정
<!-- 어노테이션 기반 MVC 활성화 -->
<mvc:annotation-driven/>
<!-- 뷰 리졸버 설정 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jsp" />
</bean>
<!-- 컨트롤러 스캔 -->
<context:component-scan base-package="org.zerock.controller" />
역할:
- mvc:annotation-driven:
@Controller,@GetMapping등의 어노테이션을 활성화합니다. - InternalResourceViewResolver: JSP 파일의 위치를 자동으로 조합하는 뷰 리졸버를 설정합니다.
- component-scan: 지정된 패키지에서
@Controller어노테이션이 붙은 클래스를 자동으로 Bean으로 등록합니다.
3. root-context.xml - 비즈니스 계층 설정
<!-- 서비스 레이어 스캔 -->
<context:component-scan base-package="org.zerock.service" />
<!-- 데이터베이스 연결 설정 -->
<bean name="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver" />
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/springdb?serverTimezone=Asia/Seoul" />
...
</bean>
<bean name="dataSource" class="com.zaxxer.hikari.HikariDataSource">
<constructor-arg ref="hikariConfig" />
</bean>
역할:
- 서비스 레이어 스캔:
@Service어노테이션이 붙은 클래스를 Bean으로 등록합니다. - 데이터베이스 연결 풀: HikariCP를 사용한 효율적인 DB 연결 관리를 설정합니다.
컨트롤러 엔드포인트 상세
각 엔드포인트의 동작 방식과 특징을 예제와 함께 살펴보겠습니다.
1. /ex1 - 명시적 뷰 이름 반환
@GetMapping("/ex1")
public String ex1() {
log.info("========== /ex1 컨트롤러 호출됨 ==========");
return "sample/ex1"; // 명시적 뷰 이름 반환
}
요청: GET http://localhost:8080/ex1
처리 흐름:
graph LR
A[요청: /ex1] --> B[DispatcherServlet]
B --> C[@GetMapping 매핑]
C --> D[ex1 메서드 실행]
D --> E[뷰 이름 반환: sample/ex1]
E --> F[ViewResolver]
F --> G[JSP: /WEB-INF/views/sample/ex1.jsp]
G --> H[HTML 응답]
style A fill:#e1f5ff
style H fill:#c8e6c9
특징:
- 가장 명확하고 권장되는 방식입니다.
- 반환할 뷰의 이름을 명시적으로 지정합니다.
- 뷰 이름과 URL이 달라도 상관없습니다.
2. /ex2 - 암시적 뷰 이름 반환
@GetMapping("/ex2")
public void ex2() {
log.info("sample/ex2");
// 반환값이 없음 (void)
}
요청: GET http://localhost:8080/ex2
처리 과정:
- 메서드가
void를 반환하면 Spring은 요청 URL을 기반으로 뷰 이름을 추론합니다. - URL:
/ex2→ 뷰 이름:ex2 - ViewResolver가
/WEB-INF/views/ex2.jsp로 변환합니다. - 주의: 해당 경로에 JSP 파일이 반드시 존재해야 합니다.
특징:
- URL과 뷰 이름이 1:1로 매칭될 때 간결하게 사용할 수 있습니다.
- URL이 변경되면 뷰 파일 위치도 함께 변경해야 하므로 유연성이 떨어집니다.
3. /ex3re - 일반 뷰 반환
@GetMapping("/ex3re")
public String ex3Re() {
return "sample/ex3Result";
}
요청: GET http://localhost:8080/ex3re
특징:
- URL(
/ex3re)과 뷰 이름(sample/ex3Result)이 다른 경우에 사용합니다. - 하나의 컨트롤러에서 여러 뷰를 선택적으로 반환할 수 있습니다.
4. /ex4 - 요청 파라미터 바인딩
@GetMapping("/ex4")
public void ex4(@RequestParam(name="n1", defaultValue = "1") int num,
@RequestParam("name") String name) {
log.info("num :" + num);
log.info("name : " + name);
}
요청 예시:
GET http://localhost:8080/ex4?n1=10&name=홍길동GET http://localhost:8080/ex4?name=홍길동(n1 생략 시 기본값 1 사용)
파라미터 바인딩 과정:
graph LR
A[URL: /ex4?n1=10&name=홍길동] --> B[@RequestParam]
B --> C[타입 변환]
C --> D1[num: int = 10]
C --> D2[name: String = 홍길동]
D1 --> E[메서드 실행]
D2 --> E
style A fill:#e1f5ff
style E fill:#c8e6c9
@ RequestParam 속성:
- name: URL 파라미터 이름을 지정합니다.
- defaultValue: 파라미터가 없을 때 사용할 기본값을 지정합니다.
- required: 파라미터 필수 여부 (기본값: true)
파라미터 바인딩 규칙:
@RequestParam("name")(기본): 필수 파라미터 (없으면 400 Bad Request)@RequestParam(required=false): 선택적 파라미터 (없으면 null)@RequestParam(defaultValue = "1"): 선택적 파라미터 (없으면 기본값 사용)
5. /ex5 - 객체 바인딩 (DTO)
@GetMapping("/ex5")
public void ex5(SampleDTO dto) {
log.info(dto);
}
SampleDTO 클래스:
@Setter
@ToString
public class SampleDTO {
private String name;
private int age;
}
요청 예시:
GET http://localhost:8080/ex5?name=홍길동&age=25
객체 바인딩 과정:
graph TD
A[URL 파라미터<br/>name=홍길동&age=25] --> B[Spring MVC]
B --> C{필드 이름 매칭}
C -->|name| D[dto.setName 호출]
C -->|age| E[dto.setAge 호출<br/>String→int 변환]
D --> F[SampleDTO 객체 생성]
E --> F
F --> G[메서드에 전달]
style A fill:#e1f5ff
style G fill:#c8e6c9
자동 바인딩 규칙:
- HTTP 파라미터 이름과 DTO 필드 이름이 일치하면 자동으로 매핑됩니다.
- Spring이 타입 변환을 자동으로 처리합니다 (String → int, String → Date 등).
@RequestParam없이도 객체로 바인딩 가능합니다.- Lombok의
@Setter가 setter 메서드를 자동 생성하여 바인딩을 가능하게 합니다. @ToString은 로그 출력 시 객체 내용을 문자열로 변환합니다.
객체 바인딩의 장점:
- 여러 파라미터를 하나의 객체로 깔끔하게 관리할 수 있습니다.
- 파라미터가 많아질수록 코드가 간결해집니다.
- 유효성 검증(
@Valid)을 객체 단위로 적용할 수 있습니다.
데이터 바인딩
Spring MVC의 다양한 데이터 바인딩 방식을 비교해보겠습니다.
바인딩 방식 비교
graph TB
subgraph "1. 단순 파라미터 바인딩"
A1[URL: ?name=홍길동] --> B1[@RequestParam String name]
B1 --> C1[메서드 파라미터로 바로 사용]
end
subgraph "2. 객체 바인딩"
A2[URL: ?name=홍길동&age=25] --> B2[SampleDTO dto]
B2 --> C2[dto.name = 홍길동<br/>dto.age = 25]
end
subgraph "3. 기본값 설정"
A3[URL: /ex4 파라미터 없음] --> B3[@RequestParam defaultValue=1]
B3 --> C3[num = 1 기본값 사용]
end
style C1 fill:#c8e6c9
style C2 fill:#c8e6c9
style C3 fill:#c8e6c9
1. 단순 파라미터 바인딩
@GetMapping("/ex4")
public void ex4(@RequestParam("name") String name) { ... }
사용 시기: 파라미터가 1~2개로 적을 때
2. 객체 바인딩
@GetMapping("/ex5")
public void ex5(SampleDTO dto) { ... }
사용 시기: 파라미터가 3개 이상이거나, 관련된 데이터를 그룹화할 때
3. 기본값 설정
@RequestParam(name="n1", defaultValue = "1") int num
사용 시기: 선택적 파라미터에 기본값을 제공할 때
전체 흐름 요약
애플리케이션 시작 시
graph TD
A[Tomcat 서버 시작] --> B[ContextLoaderListener 실행]
B --> C[root-context.xml 로드]
C --> D[Root ApplicationContext 생성]
D --> E[HelloService Bean 등록]
D --> F[DataSource Bean 등록]
B --> G[DispatcherServlet 초기화]
G --> H[servlet-context.xml 로드]
H --> I[Servlet ApplicationContext 생성]
I --> J[HelloController Bean 등록]
I --> K[ViewResolver Bean 등록]
I -.부모 참조.-> D
style A fill:#e1f5ff
style D fill:#fff4e1
style I fill:#ffe0b2
요청 처리 시
- 요청 수신:
GET /ex1 - DispatcherServlet: 요청 처리 시작
- HandlerMapping:
@GetMapping("/ex1")찾기 - HandlerAdapter:
HelloController.ex1()실행 - Controller: 비즈니스 로직 처리, 뷰 이름 반환
- ViewResolver: 뷰 이름 → JSP 경로 변환
- View: JSP 렌더링
- 응답: HTML 반환
주요 어노테이션 설명
| 어노테이션 | 용도 | 위치 | 설명 |
|---|---|---|---|
@Controller | 컨트롤러 클래스 표시 | 클래스 | 웹 요청을 처리하는 컴포넌트 |
@GetMapping | GET 요청 매핑 | 메서드 | HTTP GET 요청과 메서드 연결 |
@RequestParam | 요청 파라미터 바인딩 | 메서드 파라미터 | URL 파라미터를 메서드 인자로 매핑 |
@Service | 서비스 레이어 표시 | 클래스 | 비즈니스 로직 컴포넌트 |
@Setter | Setter 메서드 자동 생성 | 클래스/필드 | Lombok 어노테이션 |
@ToString | toString() 메서드 자동 생성 | 클래스 | Lombok 어노테이션 |
체크리스트
✅ 설정 확인 사항
-
web.xml에 DispatcherServlet 설정 -
servlet-context.xml에<mvc:annotation-driven/>설정 -
servlet-context.xml에 ViewResolver 설정 -
servlet-context.xml에 컨트롤러 패키지 스캔 설정 -
root-context.xml에 서비스 패키지 스캔 설정 - 컨트롤러에
@Controller어노테이션 - 메서드에
@GetMapping어노테이션 - JSP 파일이
/WEB-INF/views/경로에 존재
⚠️ 주의사항
[!IMPORTANT] 뷰 이름 반환 방식
- String 반환: 명시적 뷰 이름 (권장)
- void 반환: URL 기반 암시적 뷰 이름 (URL과 뷰 이름이 일치할 때만 사용)
[!TIP] 파라미터 바인딩 권장사항
- 파라미터가 1~2개:
@RequestParam사용- 파라미터가 3개 이상: DTO 객체 바인딩 사용
- 타입 변환은 Spring이 자동 처리하므로 신경 쓰지 않아도 됩니다.
[!NOTE] 패키지 구조
- 컨트롤러:
org.zerock.controller- 서비스:
org.zerock.service- DTO:
org.zerock.dto