[Spring Master] 입문 이론 - Spring MVC
Spring MVC
Spring MVC
MVC란 Model - View - Controller의 약자로, 소프트웨어 디자인 패턴 중 하나입니다.
MVC 패턴은 소프트웨어를 구성하는 요소들을 Model, View, Controller로 구분하여 각각의 역할을 분리합니다.
이는 코드의 재사용성과 유지보수성을 높이고, 개발자들 간의 협업을 용이하게 합니다. 따라서 소프트웨어 개발 시 MVC 패턴을 적용해 구조를 잘 설계하는 것이 중요합니다.
- Model: 데이터와 비즈니스 로직 담당 / DB와 연동하여 데이터를 저장하고 불러오는 등의 작업을 수행합니다.
- View: 사용자 인터페이스 담당 / 사용자가 보는 화면, 버튼, 폼 등을 디자인하고 구현합니다.
- Controller: Model과 View 사이의 상호작용을 조정하고 제어합니다. 사용자의 입력을 받아 Model에 전달하고, Model의 결과를 바탕으로 View를 업데이트합니다.
Spring Web MVC는 Servlet API를 기반으로 구축된 독창적인 웹 프레임워크입니다. 이는 처음부터 Spring 프레임워크에 포함되어 왔으며, 정식 명칭인 "Spring Web MVC"는 소스 모듈(spring-webmvc)의 이름에서 따왔으나, "Spring MVC"로 더 일반적으로 알려져 있습니다.
...
Spring MVC는 중앙에 있는 DispatcherServlet이 요청을 처리하기 위한 공유 알고리즘을 제공하는 Front Controller 패턴을 중심으로 설계되어 있으며 이 모델은 유연하고 다양한 워크 플로우를 지원합니다.
https://docs.spring.io/spring-framework/reference/web/webmvc.html
즉, Spring에서는 MVC 디자인 패턴을 적용하여 HTTP 요청을 효율적으로 처리하고 있습니다.
Servlet(서블릿)은 자바를 사용하여 웹 페이지를 동적으로 생성하는 서버 측 프로그램 혹은 그 사양을 말합니다
- 사용자가 Client(브라우저)를 통해 서버에 HTTP Request 즉, API 요청을 합니다.
- 요청을 받은 Servlet 컨테이너는 HttpServletRequest, HttpServletResponse 객체를 생성합니다.
- 약속된 HTTP의 규격을 맞추면서 쉽게 HTTP에 담긴 데이터를 사용하기 위한 객체입니다.
- 설정된 정보를 통해 어떠한 Servlet에 대한 요청인지 찾습니다.
- 해당 Servlet에서 service 메서드를 호출한 뒤 브라우저의 요청 Method에 따라 doGet 혹은 doPost 등의 메서드를 호출합니다.
- 호출한 메서드들의 결과를 그대로 반환하거나 동적 페이지를 생성한 뒤 HttpServletResponse 객체에 응답을 담아 Client(브라우저)에 반환합니다.
- 응답이 완료되면 생성한 HttpServletRequest, HttpServletResponse 객체를 소멸합니다.
하지만 모든 API 요청을 이러한 서블릿 동작 방식에 맞춰 코드를 구현한다면, 무수히 많은 서블릿 클래스를 구현해야 합니다. 따라서 Spring은 DispatcherServlet을 사용하여 Front Controller 패턴 방식으로 API 요청을 효율적으로 처리합니다.
- Client(브라우저)에서 HTTP 요청이 들어오면 DispatcherServlet 객체가 요청을 분석합니다.
- DispatcherServlet 객체는 분석한 데이터를 토대로 Handler mapping을 통해 Controller를 찾아 요청을 전달해 줍니다.
- [Sample] GET /api/hello → HelloController 의 hello() 함수
- Handler mapping 에는 API path와 Controller 메서드가 매칭되어 있습니다.
@RestController public class HelloController { @GetMapping("/api/hello") public String hello() { return "Hello World!"; } }
- API path 즉, URL을 Controller에 작성하는 방법은 @Controller 어노테이션이 달려있는 클래스를 생성한 뒤 @GetMapping 처럼 요청한 HTTP Method 와 일치하는 어노테이션을 추가한 메서드를 구현합니다.
- URL은 @GetMapping("/api/hello") 이처럼 해당 어노테이션의 속성값으로 전달해주면 됩니다.
- 해당 메서드명은 URL을 매핑하는데 영향을 미치지 않음으로 자유롭게 정해도 상관 없습니다.
- 이제는 직접 Servlet을 구현하지 않아도 DispatcherServlet에 의해 간편하게 HTTP 요청을 처리할 수 있습니다.
- Controller → DispathcerServlet
- 해당 Controller는 요청에 대한 처리를 완료 후 처리에 대한 결과 즉, 데이터('Model')와 'View' 정보를 전달합니다.
- DispatcherServlet → Client
- ViewResolver 통해 View에 Model을 적용하여 View를 Client에게 응답으로 전달합니다.
Controller
Spring MVC는 효율적인 API 처리를 위해 Front Controller 패턴을 만들었습니다. 따라서 API마다 서블릿 클래스를 만들지 않습니다. 보통 하나의 Controller에 모든 API를 넣지는 않으며, 유사한 성격의 API를 하나의 Controller로 관리합니다.
@Controller
public class HelloController {
@GetMapping("/api/hello")
@ResponseBody
public String hello() {
return "Hello World!";
}
}
@Controller는 해당 클래스가 Controller의 역할을 수행할 수 있도록 등록해 줍니다.
@GET, @POST, @PUT, @DELETE 는 각각의 HTTP 메서드에 매핑되는 어노테이션입니다.
@RequestMapping은 중복되는 URL를 단축시켜줄 수 있습니다.
정적 페이지와 동적 페이지
정적 페이지 처리하기
- static 폴더: 'http://localhost:8080/hello.html' (파일 경로: /resources/static/hello.html)
SpringBoot 서버에 html 파일을 바로 요청하면 해당 html 파일을 static 폴더에서 찾아서 반환해 줍니다.
- Redirect: ' http://localhost:8080/html/redirect' (파일 경로: /resources/static/hello.html)
@GetMapping("/html/redirect")
public String htmlStatic() {
return "redirect:/hello.html";
}
Templete 엔진을 적용한 상태에서 static 폴더의 html 파일을 Controller를 통해서 처리하고 싶을 경우, "redirect:/hello.html" redirect 요청을 문자열로 반환하면 http://localhost:8080/hello.html 요청이 재수행되면서 static 폴더의 파일을 반환합니다.
- Templete 엔진에 View 전달: 'http://localhost:8080/html/templates' (파일 경로: /resources/templetes/hello.html)
@GetMapping("/html/templates")
public String htmlTemplates() {
return "hello";
}
static 폴더에 있는 html 파일을 바로 호출하는 방법이 가장 간단하지만, 외부(브라우저)에서 바로 접근하지 못하게 하고 싶거나 특정 상황에 Controller를 통해서 제어하고 싶을 경우, templates 폴더에 해당 정적 html 파일을 추가하고 해당 html 파일명인 "hello" 문자열을 반환하여 처리할 수 있습니다. (.html은 생략 가능합니다.)
동적 페이지 처리하기
- Client 의 요청을 Controller에서 Model 로 처리합니다.
- DB 조회가 필요하다면 DB 작업 후 처리한 데이터를 Model에 저장합니다.
- Template engine(Thymeleaf) 에게 View, Model 전달합니다.
- View: 동적 HTML 파일
- Model: View 에 적용할 정보들
- Template engine
- View에 Model을 적용 → 동적 웹페이지 생성
- 예) 로그인 성공 시, "로그인된 사용자의 Nickname"을 페이지에 추가
- Template engine 종류: 타임리프(Thymeleaf), Groovy, FreeMarker, Jade, JSP 등
- View에 Model을 적용 → 동적 웹페이지 생성
- Client(브라우저)에게 View(동적 웹 페이지, HTML)를 전달 해줍니다.
데이터를 Client에 반환하는 방법
웹 생태계가 고도화 되는 과정 중에 프론트엔드와 백엔드가 각각 발전하면서 느슨하게 결합하는 방식을 더 많이 채택하게 되었습니다. 따라서 최근에는 서버가 직접 View(html, css, js)를 반환하기보다는 요청에 맞는 특정한 정보만 반환하는 것을 더 선호하기도 합니다.
@Controller 어노테이션이 달린 Controller에서 문자열을 반환할 경우, templetes 폴더에서 해당 문자열의 .html 파일을 찾아서 반환해 줍니다. 따라서 html 파일이 아닌, JSON 데이터를 브라우저에 반환하고 싶다면 해당 메서드에 @ResponseBody 어노테이션을 추가해줘야 합니다.
JSON 데이터 반환 방법
- 반환값: String
@GetMapping("/response/json/string")
@ResponseBody
public String helloStringJson() {
return "{\"name\":\"Robbie\",\"age\":95}";
}
Java는 JSON 타입을 지원하지 않기 때문에 JSON 형태의 String 타입으로 변환해서 사용해야 합니다.
- 반환값: String 외 자바 클래스
@GetMapping("/response/json/class")
@ResponseBody
public Star helloClassJson() { // Star는 java 클래스입니다.
return new Star("Robbie", 95);
}
Spring에서 자동으로 Java의 객체를 JSON으로 변환해줍니다.
@RestController는 @Controller + @ResponseBody로, 해당 클래스의 모든 메서드에 @ResponseBody 어노테이션이 추가되는 효과를 부여할 수 있습니다. 백엔드 구현 시 html이 아닌 데이터만을 반환하는 경우가 많으므로 많이 사용합니다.
Jackson
Jackson은 JSON 데이터 구조를 처리해주는 라이브러리입니다. Spring은 3.0버전 이후로 Jackson과 관련된 API를 제공함으로써 자동으로 처리해주고 있습니다. 따라서 Spring Boot의 starter-web에서는 default로 Jackson 관련 라이브러리들을 제공하고 있으며, 직접 JSON 데이터를 처리해야 할 때는 Jackson 라이브러리의 ObjectMapper를 사용할 수 있습니다.
Object(Java의 객체) To JSON
@Test
@DisplayName("Object To JSON : get Method 필요")
void test1() throws JsonProcessingException {
Star star = new Star("Robbie", 95);
ObjectMapper objectMapper = new ObjectMapper(); // Jackson 라이브러리의 ObjectMapper
String json = objectMapper.writeValueAsString(star);
System.out.println("json = " + json);
}
objectMapper의 writeValueAsString 메서드를 사용하여 변환할 수 있습니다.
파라미터에는 JSON으로 변환시킬 Object의 객체를 줍니다.
Object를 JSON 타입의 String으로 변환하기 위해서는 해당 Object에 get 메서드(@Getter)가 필요합니다.
JSON To Object(Java의 객체)
@Test
@DisplayName("JSON To Object : 기본 생성자 & (get OR set) Method 필요")
void test2() throws JsonProcessingException {
String json = "{\"name\":\"Robbie\",\"age\":95}"; // JSON 타입의 String
ObjectMapper objectMapper = new ObjectMapper(); // Jackson 라이브러리의 ObjectMapper
Star star = objectMapper.readValue(json, Star.class);
System.out.println("star.getName() = " + star.getName());
}
objectMapper의 readValue 메서드를 사용하여 변환할 수 있습니다.
첫 번째 파라미터는 JSON 타입의 String을 주고, 두 번째 파라미터에는 변환할 Object의 class 타입을 줍니다.
JSON 타입의 String을 Object로 변환하기 위해서는 해당 Object에 기본 생성자와 get 혹은 set 메서드가 필요합니다.
Path Variable과 Request Param
Client 즉, 브라우저에서 서버로 HTTP 요청을 보낼 때 데이터를 함께 보낼 수 있습니다.
서버에서는 해당 데이터를 받아서 사용해야 하는데, Client가 데이터를 보내는 방식이 여러가지입니다.
- @PathVariable 방식: 서버에 보내려는 데이터를 URL 경로에 추가합니다.
// [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);
}
- /star/{name}/age/{age}: 데이터를 받고자 하는 위치의 경로에 {data} 중괄호를 사용합니다.
- (@PathVariable String name, @PathVariable int age):해당 요청 메서트 파라미터에 @PathVariable 어노테이션과 함께 중괄호에 선언한 변수명과 변수 타입을 선언하면 해당 경로의 데이터를 받아올 수 있습니다.
- @RequestParam(Query String) 방식: 서버에 보내려는 데이터를 URL 경로 마지막에 ?와 &를 사용하여 추가합니다.
// [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);
}
- ?name=Robbie&age=95: key 부분에 선언한 name과 age를 사용하여 value에 선언된 Robbie, 95 데이터를 받아올 수 있습니다.
- (@RequestParam String name, @RequestParam int age): 해당 요청 메서드 파라미터에 @RequestParam 어노테이션과 함께 key 부분에 선언한 변수명과 변수 타입을 선언하면 데이터를 받아올 수 있습니다.
- form 태그 POST: HTML의 form 태그를 사용해 POST 방식으로 HTTP 요청을 보낼 수 있습니다.
// [Request sample]
// POST http://localhost:8080/hello/request/form/param
// Header
// Content type: application/x-www-form-urlencoded
// Body
// name=Robbie&age=95
@PostMapping("/form/param")
@ResponseBody
public String helloPostRequestParam(@RequestParam String name, @RequestParam int age) {
return String.format("Hello, @RequestParam.<br> name = %s, age = %d", name, age);
}
이때 해당 데이터는 HTTP Body에 'name=Robbie&age=95' 형태로 담겨져 서버로 전달됩니다. 따라서 @RequestParam 어노테이션을 사용하여 받아올 수 있습니다.
// [Request sample]
// GET http://localhost:8080/hello/request/form/param?name=Robbie&age=95
@GetMapping("/form/param")
@ResponseBody
public String helloGetRequestParam(@RequestParam(required = false) String name, int age) {
return String.format("Hello, @RequestParam.<br> name = %s, age = %d", name, age);
}
- age 앞에 @RequestParam이 생략된 것처럼, @RequestParam 어노테이션은 생략이 가능합니다.
- @PathVariable과 @RequestParam은 모두 (required = false) 옵션을 통해 해당 값을 Client에게서 전달 받지 못해도 오류가 발생하지 않게 할 수 있습니다.
HTTP 데이터를 객체로 처리하는 방법
- @ModelAttribute 어노테이션을 사용한 뒤 Body 데이터를 받아올 Star star 객체를 선언합니다.
[ form 태그 POST ]
// [Request sample]
// POST http://localhost:8080/hello/request/form/model
// Header
// Content type: application/x-www-form-urlencoded
// Body
// name=Robbie&age=95
@PostMapping("/form/model")
@ResponseBody
public String helloRequestBodyForm(@ModelAttribute Star star) {
return String.format("Hello, @ModelAttribute.<br> (name = %s, age = %d) ", star.name, star.age);
}
[ Query String 방식 ]
// [Request sample]
// GET http://localhost:8080/hello/request/form/param/model?name=Robbie&age=95
@GetMapping("/form/param/model")
@ResponseBody
public String helloRequestParam(@ModelAttribute Star star) {
return String.format("Hello, @ModelAttribute.<br> (name = %s, age = %d) ", star.name, star.age);
}
- ?name=Robbie&age=95 처럼 데이터가 두 개만 있다면 괜찮지만, 여러 개 있다면 @RequestParam 어노테이션으로 하나씩 받아오기 힘들 수 있습니다.
- 이때 @ModelAttribute 어노테이션을 사용해 Java의 객체로 데이터를 받아올 수 있습니다. 파라미터에 선언한 Star 객체가 생성되고, 오버로딩된 생성자 혹은 Setter 메서드를 통해 요청된 name & age 의 값이 담겨집니다.
- @ModelAttribute 어노테이션은 생략이 가능합니다.
- Spring은 해당 파라미터(매개변수)가 SimpleValueType이라면 @RequestParam으로 간주하고, 아니라면 @ModelAttribute라고 판단합니다.
- SimpleValueType은 원시타입(int), Wrapper타입(Integer), Date등의 타입을 의미합니다.
- [ JSON 형태 ]로 데이터가 서버에 전달되었을 때 @RequestBody 어노테이션으로 데이터를 객체 형태로 받아옵니다.
// [Request sample]
// POST http://localhost:8080/hello/request/form/json
// Header
// Content type: application/json
// Body
// {"name":"Robbie","age":"95"}
@PostMapping("/form/json")
@ResponseBody
public String helloPostRequestJson(@RequestBody Star star) {
return String.format("Hello, @RequestBody.<br> (name = %s, age = %d) ", star.name, star.age);
}
- 데이터를 Java의 객체로 받아올 때 주의할 점: 해당 객체의 필드에 데이터를 넣어주기 위해 set or get 메서드 또는 오버로딩된 생성자가 필요합니다.