이번 포스트에서는 `스프링 MVC 동작 원리와 구현`을 다룹니다.
학습 순서
- 웹 애플리케이션 기본 원리
- 스프링 MVC 동작 원리와 구현 📍
- 객체 지향 설계 원칙 (다음 주 예정)
📚 세부 학습 순서
- Spring MVC 동작 원리
- DispatcherServlet의 역할 & 동작 방식
- Controller, View Resolver와의 관계
- Spring MVC 요청 처리 흐름 (Controller → Service → Repository)
- Spring을 활용한 REST API 구현
- REST의 특징 (HTTP 메서드, REST 제약 조건)
- @RestController와 REST API 구현
Spring MVC 동작 원리
DispatcherServlet의 역할 & 동작 방식
Spring MVC의 핵심 컴포넌트라고 할 수 있는 DispatcherServlet은 `프론트 컨트롤러 패턴`을 구현한 서블릿이다. 애플리케이션의 모든 HTTP 요청을 가로채서 적절한 컨트롤러로 전달하고, 최종적으로 응답을 생성하는 역할을 한다. (*컴포넌트는 각각의 독립된 모듈 정도로 생각하자.)
DispatcherServlet의 주요 역할
1. 요청을 받아 적절한 컨트롤러로 라우팅
2. 핸들러(컨트롤러) 실행 및 처리 결과를 반환
3. 뷰 선택 및 렌더링
▼ DispatcherServlet 흐름 ▼
[클라이언트 요청]
↓
[DispatcherServlet] → [HandlerMapping] (컨트롤러 찾기)
↓
[HandlerAdapter] (컨트롤러 실행)
↓
[컨트롤러] (비즈니스 로직 처리, ModelAndView 반환)
↓
[ViewResolver] (뷰 선택)
↓
[View] (HTML 생성)
↓
[응답 반환]
DispatcherServelt,
상세 과정 알아보기
1. 클라이언트가 `http://example.com/reservations`와 같은 요청을 보낸다.
2. DispatcherServlet이 모든 요청을 가로채고 처리하기 시작한다.
*스프링 부트에서는 DispatcherServlet이 자동 설정되므로, 따로 등록할 필요가 없다.
@WebServlet(name = "dispatcherServlet", urlPatterns = "/")
public class DispatcherServlet extends FrameworkServlet {
...
}
3. DispatcherServlet이 `HandlerMapping`을 사용해 요청을 처리할 컨트롤러를 찾는다.
*스프링에서는 여러 종류의 HandlerMapping을 제공한다. (아래는 `/reservations/{id} 요청을 처리하는 @RequestMapping 기반 매핑 예시다.
@Controller
@RequestMapping("/reservations")
public class ReservationController {
@GetMapping("/{id}")
public String getReservation(@PathVariable Long id, Model model) {
model.addAttribute("reservation", reservationService.getReservation(id));
return "reservationDetail";
}
}
4. 컨트롤러가 실행될 수 있도록 HandlerAdapter가 이를 호출한다.
*일반적으로 RequestMappingHandlerAdapter가 사용된다.
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler);
}
5. 컨트롤러는 비즈니스 로직을 처리한 후 ModelAndView를 반환한다.
*아래 예시에서는 `reservationService.getReservation(id)`에서 데이터를 가져와 Model에 담고, View를 반환한다.
@GetMapping("/{id}")
public String getReservation(@PathVariable Long id, Model model) {
model.addAttribute("reservation", reservationService.getReservation(id));
return "reservationDetail"; // View 이름 반환
}
6. DispatcherServlet은 `ViewResolver`를 사용하여 렌더링할 View를 찾는다.
*참고로 스프링 부트에서 기본적으로 설정된 ViewResolver는 `ThymeleafViewResolver`다. (JSP 사용 시 또 다르다.)
7. 최종적으로 View에서 HTML을 생성하여 클라이언트에게 반환한다.
Controller, View Resolver와의 관계
- Controller: 클라이언트의 요청을 처리하는 역할을 하며, 주로 비즈니스 로직을 수행한다. 이때 반환되는 모델과 뷰 이름을 DispatcherServlet에 전달한다.
- View Resolver: 컨트롤러에서 반환된 뷰 이름을 실제 뷰 템플릿(JSP, Thymeleaf 등)으로 변환하는 역할을 한다. 이 과정에서 DispatcherServlet은 뷰 리졸버를 통해 실제 뷰 파일을 찾고, 렌더링한다.
Spring MVC 요청 처리 흐름 (Controller → Service → Repository)
Spring MVC에서는 요청을 처리할 때, `Controller → Service → Repository` 계층 구조로 분리하여 관리한다. 이 구조는 비즈니스 로직과 데이터 접근 로직을 분리하여 코드의 유지보수성과 확장성을 높이는 데 기여한다.
- Controller: 클라이언트의 요청을 받으며, 요청에 맞는 비즈니스 로직을 서비스 계층에 위임한다.
- Service: 비즈니스 로직을 처리하는 계층으로, 필요한 경우 여러 레포지토리로부터 데이터를 가져와서 처리한다.
- Repository: 데이터베이스와의 직접적인 상호작용을 담당하며, JPA나 MyBatis와 같은 ORM(Object-Relational Mapping) 기술을 사용해 데이터베이스에서 데이터를 조회하거나 수정한다.
Spring을 활용한 REST API 구현
REST의 특징 (HTTP 주요 메서드, REST 제약 조건, RESTful)
- HTTP 메서드: REST는 HTTP 메서드를 기반으로 동작한다.
- GET: 데이터를 조회
- POST: 데이터를 생성
- PUT: 데이터를 수정
- DELETE: 데이터를 삭제
- REST 제약 조건: 아래 조건을 모두 갖춘 것을 `RESTful`하다고 표현한다.
- 무상태성 (Stateless): 각 요청은 독립적이어야 하며, 서버는 클라이언트의 상태를 저장하지 않는다.
- 일관된 인터페이스 (Uniform Interface): 클라이언트와 서버 간의 통신은 일관된 방식으로 이루어져야 한다.
- 캐시 처리 가능 (Cacheable): 응답은 캐시할 수 있어야 한다.
- 계층화된 시스템 (Layered System): 서버는 여러 계층으로 분리될 수 있다.
- 클라이언트-서버 구조 (Client-Server): 클라이언트와 서버는 명확히 분리되어야 한다.
일부 이해하기 어려운 개념만 추가로 설명하겠다.
▼ 캐시 처리 가능 ▼
REST 아키텍처에서는 서버와 클라이언트 간의 효율적인 데이터 전송을 위해 캐시 처리가 가능해야 한다.
캐시 처리가 가능하다는 것은 클라이언트가 서버에서 받은 데이터를 로컬에서 저장하고, 그 데이터를 재사용할 수 있도록 허용하는 방식이며, 이를 통해 불필요한 서버 요청을 줄이고 응답 시간을 단축시킬 수 있다.
HTTP 헤더를 이용해 캐시를 제어함으로써 구현할 수 있다.
Cache-Control: 캐시의 유효 기간을 설정하거나 캐시가 가능한지 여부를 명시할 수 있다.
Expires: 특정 시간이 지나면 캐시된 데이터가 만료되도록 설정할 수 있다.
ETag: 서버에서 제공한 리소스의 버전을 식별할 수 있는 값으로, 클라이언트가 해당 리소스를 캐시할 수 있게 해 준다.
Cache-Control: public, max-age=3600
*본 예시는 캐시된 데이터가 1시간(3600초) 동안 유효함을 의미한다.
@RestController와 REST API 구현
@RestController는 스프링에서 `RESTful`한 서비스를 쉽게 구현할 수 있도록 도와주는 어노테이션이다. 이 어노테이션은 `@Controller`와 `@ResponseBody`를 결합한 것으로, 클라이언트에 데이터를 직접 반환하는 역할을 한다.
@RestController
@RequestMapping("/api")
public class MyRestController {
@GetMapping("/greeting")
public String greet() {
return "Hello, World!";
}
@PostMapping("/create")
public ResponseEntity<String> create(@RequestBody MyObject object) {
// 비즈니스 로직 처리
return ResponseEntity.status(HttpStatus.CREATED).body("Object created");
}
@PutMapping("/update/{id}")
public ResponseEntity<String> update(@PathVariable Long id, @RequestBody MyObject object) {
// 데이터 수정 로직
return ResponseEntity.ok("Object updated");
}
@DeleteMapping("/delete/{id}")
public ResponseEntity<String> delete(@PathVariable Long id) {
// 데이터 삭제 로직
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
}
- @GetMapping: HTTP GET 요청 처리
- @PostMapping: HTTP POST 요청 처리
- @PutMapping: HTTP PUT 요청 처리
- @DeleteMapping: HTTP DELETE 요청 처리
- @RequestBody: 요청 본문에 있는 데이터를 객체로 변환
- @PathVariable: URL 경로 변수에 바인딩
- ResponseEntity: HTTP 응답을 구성할 때 사용하며, 상태 코드 및 헤더 설정
몇 가지 낯선 개념만 추가로 설명하겠다.
@RequestBody는 HTTP 요청의 바디에 포함된 데이터를 자바 객체로 변환하는 어노테이션이다.
주로 POST, PUT, PATCH 요청에서 클라이언트가 보낸 데이터를 서버에서 객체로 받기 위해 사용한다.
*클라이언트가 서버로 JSON, XML 등의 데이터를 HTTP 요청 바디(본문)에 담아서 보낼 때, 해당 어노테이션을 통해 그 데이터를 자바 객체로 자동 변환한다. 스프링은 기본적으로 JSON 변환을 위한 라이브러리(Jackson)을 사용해 변환해 준다.
@PostMapping("/users")
public ResponseEntity<String> createUser(@RequestBody User user) {
// user는 요청 바디에 포함된 JSON 데이터로부터 변환된 User 객체
userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body("User created successfully");
}
{
"name": "june",
"email": "funczun@gmail.com"
}
@PathVariable은 URL 경로에 포함된 변수를 자바 메서드의 파라미터로 바인딩하는 어노테이션이다.
보통 RESTful API에서 리소스를 구분하기 위해 URL 경로에 변수를 사용하는 경우에 사용된다.
*클라이언트가 보낸 URL 경로에서 값을 추출하여, 해당 값을 메서드 파라미터에 바인딩해 준다. 바인딩이라고 함은 값을 객체의 필드에 매핑하는 과정을 의미한다.
@GetMapping("/users/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.findUserById(id);
if (user == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
return ResponseEntity.ok(user);
}
ID 같은 자원식별자를 URL 경로에서 추출하고 처리할 때 유용하다. 위 예시에서는 `/users/{id}`로 요청이 들어오면, {id} 경로 변수의 값을 메서드의 id 파라미터로 바인딩한다. (예를 들어 `/users/1`로 요청을 보내면, id 파라미터는 1로 바인딩되고, getUserById() 메서드가 호출된다.)
ResponseEntity는 HTTP 응답의 본문(body), 상태 코드(status code), 헤더(headers)를 모두 커스터마이즈할 수 있는 클래스로, 클라이언트에게 정확한 HTTP 응답을 반환하고자 할 때 사용된다.
*빌더 패턴을 사용해 응답을 구성하며, 이를 통해 동적으로 응답을 생성할 수 있게 된다.
@PostMapping("/createUser")
public ResponseEntity<String> createUser(@RequestBody User user) {
boolean isCreated = userService.createUser(user);
if (isCreated) {
return ResponseEntity.status(HttpStatus.CREATED)
.header("Location", "/users/" + user.getId())
.body("User created successfully");
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("User creation failed");
}
}
`결합도`와 `응집도`는 OOP를 위한 기초 개념이다. 관련 내용은 다음 주 포스트에서 알아보자.
'멋쟁이사자처럼 > 정기 세션' 카테고리의 다른 글
[🦁3] 객체 지향 설계: OOP (2) | 2025.03.28 |
---|---|
[🦁2] Web App & Spring MVC #1 (0) | 2025.03.21 |
[🦁1] Git & GitHub (0) | 2025.03.14 |