스프링 MVC 모델의 구조와 동작원리
1. MVC 모델이란
스프링 MVC는 자바 웹 애플리케이션 개발에 있어 가장 널리 사용되는 소프트웨어 디자인 패턴 중 하나이다.
MVC 패턴은 애플리케이션을 세 가지 핵심 구성 요소로 분리한다.
1. Model
- 데이터와 비즈니스 로직을 담당
- 데이터베이스와 연동하여 데이터를 저장하고 불러오는 등의 작업을 수행
2. View
- 사용자 인터페이스를 표현
- 사용자가 보는 화면과 버튼, 폼 등을 디자인하고 구현
3. Controller
- 사용자 입력을 처리하고 Model과 View 사이의 상호작용을 조정
- 사용자의 입력을 받아 Model에 전달하고, Model의 결과를 바탕으로 View를 업데이트
스프링 프레임워크는 MVC 패턴을 효과적으로 구현하여, 개발자가 각 구성 요소를 독립적으로 개발하고 유지보수할 수 있게 해준다.
2. 서블릿이란
서블릿은 자바 웹 프로그래밍에서 클라이언트의 요청을 처리하고 그 결과를 반환하는 서버 측 프로그램이다.
좀 더 기술적으로 말하자면, 서블릿은 Java EE(Enterprise Edition) 스펙의 일부로, 동적 웹 컨텐츠를 생성하기 위한 서버 측 자바 프로그램을 말한다.
서블릿의 기본 구조는 다음과 같다.
public class MyServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// GET 요청 처리 로직
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// POST 요청 처리 로직
}
}
다음 이미지는 컨테이너(서블릿 컨테이너)에서 서블릿의 동작 방식을 보여준다. 클라이언트의 요청이 서블릿 컨테이너에 의해 처리되고, 적절한 서블릿에 의해 비즈니스 로직이 실행되어 응답이 생성되는 과정이다.
- Request: 클라이언트가 웹 서버에 HTTP 요청을 보낸다.
- Web Container (Servlet): 웹 애플리케이션 서버의 일부로, 서블릿의 생명주기를 관리한다.
- 객체 생성 (HttpServletRequest, HttpServletResponse):웹 컨테이너는 클라이언트의 요청을 받아 HttpServletRequest와 HttpServletResponse 객체를 생성한다. 이 객체들은 요청 정보를 담고 있으며, 응답을 생성하는 데 사용된다.
- Servlet 분석 (web.xml): web.xml 파일(배포 서술자)을 분석하여 어떤 서블릿이 요청을 처리해야 하는지 결정한다.
- 찾은 Servlet: web.xml 분석 결과에 따라 적절한 서블릿이 선택된다.
- service(), doGet(), doPost() 등: HTTP 메서드에 따라 doGet(), doPost() 등이 호출된다.
- Response: 서블릿이 처리한 결과가 HttpServletResponse 객체를 통해 클라이언트에게 반환된다.
3. DispatcherServlet의 역할
DispatcherServlet은 스프링 MVC의 핵심 컴포넌트로, 프론트 컨트롤러 패턴을 구현한 서블릿이다.
이 서블릿의 주요 역할은 클라이언트의 모든 요청을 중앙에서 처리하고, 요청을 적절한 핸들러(컨트롤러)에게 분배하는 것이다.
이미지는 스프링 MVC의 DispatcherServlet을 중심으로 한 요청 처리 흐름을 보여준다.
- Request: 클라이언트로부터 HTTP 요청이 DispatcherServlet에 도착한다.
- Handler mapping: DispatcherServlet은 Handler mapping을 통해 요청을 처리할 적절한 컨트롤러를 찾는다.
- Controller: 선택된 컨트롤러로 요청이 전달되어 비즈니스 로직이 실행된다.
- Model and logical view name: 컨트롤러는 처리 결과를 Model 객체에 담고, 뷰 이름을 반환한다.
- ViewResolver: DispatcherServlet은 ViewResolver를 사용하여 논리적 뷰 이름을 실제 View 객체로 변환한다.
- View: ViewResolver로부터 받은 View 객체를 사용하여 결과를 렌더링한다.
- Response: 최종적으로 렌더링된 결과가 HTTP 응답으로 클라이언트에게 반환된다.
[Handler mapping의 예시]
GET /api/hello → HelloController 의 hello() 함수
GET /user/login → UserController 의 login() 함수
GET /user/signup → UserController 의 signup() 함수
POST /user/signup → UserController 의 registerUser() 함수
4. 주요 어노테이션
@Controller
- 클래스를 스프링 MVC 컨트롤러로 지정
- 컴포넌트 스캔의 대상이 되어 자동으로 빈으로 등록
@RequestMapping
- 요청 URL을 특정 메서드나 클래스에 매핑
- HTTP 메서드, 파라미터, 헤더 등 다양한 조건으로 매핑 가능
@RequestParam
- 쿼리 파라미터나 폼 데이터를 가져올 때 사용.
- 쉽게 말해 URL에 붙어 오는 데이터들을 받아오는 역할을 해줌
@GetMapping("/greet")
public String greet(@RequestParam String name) {
return "Hello, " + name;
}
만약 사용자가 http://yourapp.com/greet?name=Alice 이런 식으로 요청하면, @RequestParam이 name이라는 파라미터의 값을 받아서 Alice로 처리해줌.
@ModelAttribute
- 폼 데이터를 객체에 자동으로 바인딩 해줌.
- 주로 폼 데이터를 Model 객체로 바인딩할 때 사용
@PostMapping("/submitForm") public String submitForm(@ModelAttribute User user) {
// User 객체에 폼 데이터가 자동으로 바인딩됨
return "Form submitted by " + user.getName();
}
폼에 name, age 같은 데이터가 있으면, User 객체의 name, age 필드에 자동으로 값이 들어가게 된다.
@RequestBody
- HTTP 요청 본문에 있는 데이터를 자바 객체로 변경해줌.
- 주로 JSON 같은 포맷의 데이터를 처리할 때 사용.
@PostMapping("/createUser")
public String createUser(@RequestBody User user) {
// 요청 본문의 JSON 데이터가 User 객체로 변환됨
return "User created with name " + user.getName();
}
클라이언트가 JSON 형식으로 {"name": "Alice", "age": 25} 이런 데이터를 POST 요청으로 보내면, @RequestBody가 이 JSON을 User 객체로 변환해줌
5. 사용 예시
@Controller
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public String getUser(@PathVariable Long id, Model model) {
User user = userService.findById(id);
model.addAttribute("user", user);
return "userDetails";
}
@PostMapping
public String createUser(@ModelAttribute User user) {
userService.save(user);
return "redirect:/users";
}
}
// [Request sample]
// GET http://localhost:8080/hello/request/star/Robbie/age/95
@GetMapping("/star/{name}/age/{age}")
@ResponseBody
public String helloRequestPath(@PathVariable String name, @PathVariable int age) {
return String.format("Hello, @PathVariable.<br> name = %s, age = %d", name, age);
}
// [Request sample]
// GET http://localhost:8080/hello/request/form/param?name=Robbie&age=95
@GetMapping("/form/param")
@ResponseBody
public String helloGetRequestParam(@RequestParam String name, @RequestParam int age) {
return String.format("Hello, @RequestParam.<br> name = %s, age = %d", name, age);
}