💋 Spring MVC란?
Spring MVC는 스프링 프레임워크의 수많은 기술들 중 웹 기술에 관련된 것이다.
핵심기술 : 스프링 DI 컨테이너, AOP, 이벤트, 기타
웹기술 : 스프링 MVC, 스프링 WebFlux
데이터 접근 기술 : 트랜잭션, JDBC, ORM 지원, XML 지원
기술 통합 : 캐시, 이메일, 원격 접근, 스케줄링
테스트 : 스프링 기반 테스트 지원
스프링은 자바 언어를 사용해, 자바의 가장 큰 장점인 객체 지향이라는 특징을 잘 살려서 개발할 수 있도록 도와주는 프레임워크다.
웹에서 View와 Controller, Domain의 역할이 섞이게 되면 유지보수가 굉장히 힘들어진다는 문제점이 있었다.
Business Logic(Controller, Domain)과 Presentation Logic(View)을 효과적으로 분리하면서 웹을 만들 수 있도록 스프링 프레임워크에서 지원하는 내용이 Spring MVC이다.
Spring MVC에 들어있는 다양한 개념들을 Spring 공식 문서를 바탕으로 정리해 보았다.
💋 Request Mapping
클라이언트는 서버로 요청(Request)을 보내고, 응답(Response)을 받는다.
이때 서버는 백엔드 개발자가 작성할 코드가 되는데, 각 요청에 대해서 어떤 일을 해야 할 지에 대해서 설정해 놓아야 한다. 이것을 위해 사용되는 것이 Request Mapping이다.
요청에는 URL, HTTP Method(GET, POST 등등), Request 파라미터, Header, Media Type과 같이 다양한 정보가 담길 수 있다. 이 정보들은 @RequestMapping 어노테이션의 attribute(속성)으로 지정할 수 있다. @RequestMapping 어노테이션은 GET, POST, PUT, DELETE, PATCH 등 모든 HTTP Method를 표현할 수 있다. 아래에서 설명하는 모든 어노테이션을 붙인 곳에서 스프링은 어떤 요청이 들어왔을 때, 요청의 조건과 일치하는 어노테이션이 존재한다면 매핑해준다.
@RequestMapping 어노테이션보다 더 직접적이고 빠른 길도 있다.
- @GetMapping
- @PostMapping
- @PutMapping
- @DeleteMapping
- @PatchMapping
이렇게 조금 더 specific한 어노테이션을 사용해서 HTTP Method를 표현하는 것은 Custom Annotation이다. 이게 더 편리하기 때문에 더 많이 사용하게 되는데, @RequestMapping 어노테이션은 class 단위로 공통적인 부분을 지정할 수 있기 때문에 분명히 사용하는 곳이 있다.
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
위 코드에 따라서, /persons라는 URL로 들어온 모든 요청에 대해서 PresonController가 다루게 된다.
/persons/1과 같은 URL로 들어온 GET 요청에 대해서는 @GetMapping이 붙은 getPerson()이 id에 1을 넣어서 다뤄준다.
💋 URI Pattern
변경 가능성이 있는 URL을 사용해서 Request Mapping을 하기 위해 사용한다.
@GetMapping("users/{id}")
public ResponseEntity<User> pathVariable(@PathVariable("id") Long id) {
User user = new User(id, "이름", "email");
return ResponseEntity.ok().body(user);
}
@PathVariable 어노테이션으로 작성한 파라미터 변수는 위의 @GetMapping 어노테이션의 {}로 적힌 {id}로 들어간다.
Pattern | Description | Example |
? | Matches one character | "/pages/t?st.html" matches "/pages/test.html" and "/pages/t3st.html" |
* | Matches zero or more characters within a path segment | "/resources/*.png" matches "/resources/file.png" "/projects/*/versions" matches "/projects/spring/versions" but does not match "/projects/spring/boot/versions" |
** | Matches zero or more path segments until the end of the path | "/resources/**" matches "/resources/file.png" and "/resources/images/file.png" "/resources/**/file.png" is invalid as ** is only allowed at the end of the path. |
{name} | Matches a path segment and captures it as a variable named "name" | "/projects/{project}/versions" matches "/projects/spring/versions" and captures project=spring |
{name:[a-z]+} | Matches the regexp "[a-z]+" as a path variable named "name" | "/projects/{project:[a-z]+}/versions" matches "/projects/spring/versions" but not "/projects/spring1/versions" |
{*path} | Matches zero or more path segments until the end of the path and captures it as a variable named "path" | "/resources/{*file}" matches "/resources/images/file.png" and captures file=/images/file.png |
공식문서에 들어가면 모든 위 내용을을 확인할 수 있다.
💋 Media Type
예를 들어 POST 요청을 통해서, 사용자가 작성한 form이 submit되었다고 생각해보자!
클라이언트가 서버로 넘겨주는 정보는 서버에서 소비(consume)될 것이다.
이와 반대로 나가는 정보는 서버에서 클라이언트에게 제공(produce)해줄 것이다.
이 정보들의 유형을 Media Type이라고 한다. 예시로는 application/json, text/plain 등등이 있다.
이 정보를 Request Mapping 어노테이션에 적어서 서버와 클라이언트가 주고받는 정보의 유형을 제한할 수 있다.
@PostMapping(path = "/pets", consumes = "application/json") (1)
public void addPet(@RequestBody Pet pet) {
// ...
}
application/json을 제외한 다른 모든 유형이었으면 좋겠다..도 !application/json으로 작성하면 된다.
MediaType 클래스에서는 자주 사용하는 APPLICATION_JSON_VALUE, APPLICATION_XML_VALUE같은 유형은 상수로 제공한다.
[개인적 궁금증] consumes와 produces를 동시에 사용할 수 있는지?가 궁금했다.
@PostMapping(value = "/example", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> exampleMethod(@RequestBody ExampleRequest request) {
// method implementation
}
💋 Parameters, headers
파라미터와 헤더가 특정 값이거나, 특정 값이 아닐 때의 요청과 매핑하도록 조건을 추가할 수 있다.
언제 사용될지 생각해 보았다. 예를 들어서 검색창에 특정 단어를 적어서 GET 요청을 보내면 parameter에 검색어가 포함된 요청이 들어가게 된다. 이 검색어에 따라서 청소년에게 유해한 결과를 띄운다던가... 같은 URL이더라도 parameter에 따라 다양한 작업을 할 수 있게 된다.
@RestController
@RequestMapping("/param-header")
public class ParamHeaderController {
@GetMapping(path = "message")
public ResponseEntity<String> message() {
return ResponseEntity.ok().body("message");
}
@GetMapping(path = "message", params = "name=hello")
public ResponseEntity<String> messageForParam() {
return ResponseEntity.ok().body("hello");
}
@GetMapping(path = "message", headers = "HEADER=hi")
public ResponseEntity<String> messageForHeader() {
return ResponseEntity.ok().body("hi");
}
}
message(): parameter 중 name이 값이 hello가 아닌 경우 실행되는 메서드
@DisplayName("Parameter Header")
@Test
void message() {
RestAssured.given().log().all()
.accept(MediaType.APPLICATION_JSON_VALUE)
.when().get("/param-header/message")
.then().log().all()
.statusCode(HttpStatus.OK.value())
.body(is("message"));
}
messageForParam(): parameter 중 name의 값이 hello인 경우 실행되는 메서드
@DisplayName("Parameter Header - Params")
@Test
void messageForParam() {
RestAssured.given().log().all()
.accept(MediaType.APPLICATION_JSON_VALUE)
.when().get("/param-header/message?name=hello")
.then().log().all()
.statusCode(HttpStatus.OK.value())
.body(is("hello"));
}
messageForHeader(): header의 HEADER가 hi인 경우 실행되는 메서드
@DisplayName("Parameter Header - Headers")
@Test
void messageForHeader() {
RestAssured.given().log().all()
.accept(MediaType.APPLICATION_JSON_VALUE)
.header("HEADER", "hi")
.when().get("/param-header/message")
.then().log().all()
.statusCode(HttpStatus.OK.value())
.body(is("hi"));
}