스프링 부트 핵심 가이드 - 04, 05장.
이번 장에서는 API를 작성하는 방법 중 Controller 계층에서의 작성 방법에 대한 내용이다.
컨트롤러를 보기 전에 먼저 웹에서 통신하는 방법부터 알아야 할 것 같다.
HTTP
웹 상에서는 모든 것이 HTTP로 이루어 진다.
HTTP는 Server/Client 모델로 클라이언트가 Request(요청)를 보내면 서버가 Response(응답)를 내려주는 방식으로 동작한다.
HTTP Request/Response
- Request
GET / HTTP/1.1 # 요청 메소드 / HTTP 버전
Host: www.fun-coding.org
Connection: keep-alive
- Response
HTTP/1.1 200 OK
Server: Apache # 웹 서버 정보
Content-type: text/heml # MIME 타입
Content-length: 107 # HTTP Body 길이
<html><head></head> # HTML 데이터
이러한 통신 규약은 전 세계적으로 정해진 규칙이고 따라야한다.
그렇다면 자바 기반의 웹 애플리케이션에서는 클라이언트가 보내는 요청을 어떻게 받고 응답을 줘야 할까?
Servlet
바로 서블릿이 그 역할을 담당한다.
서블릿은 클라이언트의 요청을 받고 그에 맞는 응답을 내려주는 자바 웹 프로그래밍 기술이다.
예를 들어 유저가 회원가입을 위해 이름과 나이 정보를 입력하여 서버에 요청을 보내면
서버는 이를 확인하여 데이터를 저장하고 결과 메시지를 보내거나 다음 페이지를 띄워주는 등의 응답을 줘야하는데,
이 역할을 수행하는 것이다.
Servlet은 위와 같은 Request/Response를 파싱하여 HttpServletRequest/HttpServletResponse로 만들어주는데,
이를 통해 개발자는 HTTP 스펙을 편리하게 사용할 수 있게 된다.
하지만 스프링을 쓰다가 순수 서블릿을 사용해보면 알겠지만, 개발을 하는데 불편한 점이 한두 가지가 아니다.
요청마다 서블릿 클래스를 새로 생성하여 반복적인 작업을 해야하고, 응답할 내용을 직접 작성하여 보내주어야 한다.
이때 응답할 내용인 Html인 View를 동적으로 생성하기 위해 JSP를 사용하게 되는데 결국 한계에 부딪히게 된다.
화면을 보여주기 위한 뷰 영역에 비즈니스 로직이 섞여 들어가면서 뷰가 점차 방대해지고 여러 역할이 섞여 있어서 유지보수 헬의 끝판왕을 찍게 되는 것이다.
그래서 등장한게 MVC 패턴이다.
Model View Controller 라는 영역으로 역할을 나눈 것을 말한다.
Spring MVC
하지만 MVC 패턴을 적용한다고 해서 완성되는 것은 아니었다.
서로의 역할만 명확하게 나뉘었을 뿐 서블릿의 불편한 점은 그대로 유지되기 때문이다.
위에서 언급했었지만, 각 요청 서블릿마다 동일하게 작성해줘야 하는 코드들을 중복적으로 작성하게 되는데,
이 말은 공통 부분을 처리하기 힘들다는 것이다.
이를 해결하기 위해 마치 프록시 서버처럼 모든 요청을 먼저 받아 보는 FrontController를 만들게 된다.
프론트 컨트롤러의 역할을 요약하여 살펴보자면,
- 프론트 컨트롤러는 들어온 요청을 살펴 본 뒤 이를 처리할 수 있는 컨트롤러를 찾아서 호출한다.
- 호출 된 컨트롤러는 요청에 맞는 처리를 한 뒤 응답의 내용에 상응하는 주요 알맹이(ModelAndView)를 프론트 컨트롤러에게 보낸다.
- 응답 내용을 받은 프론트 컨트롤러는 최종적으로 뷰를 생성하여 HTML을 응답한다.
*여기서 자세히 적진 않았지만 프론트 컨트롤러는 시키는 역할만 할 뿐 어떤 것도 처리하지 않는다.
즉, 요청을 수행할 수 있는 컨트롤러를 찾는 것도 또다른 객체인 핸들러 어뎁터가 하고, 뷰를 만드는 것도 뷰리졸버가 처리한다. (이는 구조를 유연하게 가져가기 위한 OOP적인 설계이다.)
이러한 FrontController 패턴을 적용한 것이 스프링 MVC이다.
스프링에서 FrontController의 역할을 DispatcherServlet이 맡는다.
길었다.. 위 개념을 바탕으로 애노테이션을 사용하여 더욱 편리한 컨트롤러를 만든게 지금의 스프링에서 제공하는 컨트롤러이기 때문에 현재 컨트롤러의 기능을 이해하려면 알아야 한다고 생각했다.
이 배경 지식은 이전에 들었던 김영한님 강의를 바탕으로 흐름 정도만 요약해봤다.
이제 컨트롤러 계층에서 API를 작성하는 방법을 알아보자.
API 작성
먼저 HTTP 메소드 매핑 방법부터 보자.
HTTP method mapping
@RequestMapping
RequestMapping은 모든 종류의 HTTP 요청을 받을 수 있다.
@RequestMapping("/hello")
public String helloBasic() {
return "ok";
}
이때 HTTP 메소드에 따라 달리 처리하기위해 특정 메소드만 받도록 지정할 수 있다.
@RequestMapping("/hello", method = RequestMethod.GET)
public String helloBasic() {
return "ok";
}
그런데.. 매번 method 속성으로 지정해주는게 여간 귀찮은게 아니다.
그래서 각 메소드별 애노테이션이 존재한다. (@GetMapping, @PostMapping, …)
@GetMapping
@GetMapping("/hello")
public String helloBasic() {
return "ok";
}
@GetMapping에 들어가보면 사실 @RequestMapping에 method를 지정해 놓은 것이다.
단지 사용하기 편리하게 스프링에서 애노테이션으로 한번 더 감싸서 제공하는 것!
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {
...
}
이제 요청을 받는 방법은 알겠고, 해당 요청에 대한 데이터는 어떻게 받을까?
Parameter 값 받기
GET 요청은 body가 없기 때문에 파라미터를 통해 데이터를 전달 받는다.
@RequestParam
@RequestParam을 사용하면 파라미터의 값을 가져올 수 있다.
// localhost:8080/hello?name=kim
@GetMapping("/hello")
public String helloBasic(@RequestParam("name") String name) {
log.info("name -> {}", name);
return "ok";
}
이때 파라미터의 이름과 파라미터를 받을 변수의 이름이 같다면 @RequestParam("name")에서 ("name")을 생략할 수 있다.
@PathVariable
@PathVariable을 사용하면 URL에 값을 담아 요청할 때 전달되는 값을 받을 수 있다.
// localhost:8080/hello/kim
@GetMapping("/hello/{name}")
public String helloBasic(@PathVariable String name) {
log.info("name -> {}", name);
return "ok";
}
책에서는 이 방법을 더 많이 사용한다고 하는데, get 요청 시 상황에 따라 둘다 많이 사용하는 것 같다.
Body 정보 받기
대표적으로 POST 요청은 body 정보가 있다.
HTTP Body 영역의 값을 @RequestBody를 사용하여 Object 형태로 데이터를 받을 수 있다.
HTTP Request의 message body
{
"name": "kim",
"age": 20
}
Controller
@PostMapping("/hello")
public String helloBasic(@RequestBody BasicRequest request) {
return "ok";
}
BasicRequest
public class BasicRequest {
private String name;
private Integer age;
}
json의 키 값이 데이터를 받을 requestDto의 필드 명과 일치 한다면, 별다른 설정없이 매핑된다.
만약 필드명이 다르다면 @JsonProperty로 설정해줄 수 있다. -> @JsonProperty("userName") String name;
PUT, PATCH, DELETE에서도 body 정보를 받는 방법은 동일하고, 매핑 애노테이션만 달리 해주면 된다.
이제 응답 객체를 만들어보자.
Response 생성
@ResponseBody 사용
@ResponseBody
@GetMapping
public UserDto getUser() {
UserDto userDto = userService.getUser();
return userDto;
}
@ResponseBody는 자바 객체를 응답 본문에 매핑하는 역할을 한다.
스프링의 메세지 컨버터가 return의 클래스 타입과 미디어 타입을 확인하여
응답 HTTP 메시지 바디에 데이터를 생성해주는 것이다.
(여기서 의문이 있을 수 있다. RSET API를 작성할 때 메소드 마다 @ResponseBody를 붙여주지 않아도 잘 되던데? 라고.. 그건 @RestController에 @ResponseBody가 포함되어 있기 때문이다.)
하지만 @ResponseBody는 응답 코드를 변경하기가 번거롭다. (@ResponseStatus를 사용해도 되지만 보기 좋진 않음)
이를 위해 스프링에서 표준 응답을 준수할 수 있도록 응답 객체를 제공한다.
ResponseEntity 사용
@PostMapping
public ResponseEntity<UserDto> createUser(@RequestBody createUserRequest request) {
createUserResponse response = userService.createUser();
return ResponseEntity.status(HttpStatus.CREATED).ok(response);
}
ResponseEntity를 사용하면 상태 코드 등을 빌더 패턴으로 통해 편리하게 생성하여 전달할 수 있게 된다.
마무리
여기까지 API를 작성할 때 기본적인 요청과 응답에 대해 알아보았다.
강의를 통해 들어오는 지식은 많았는데 이렇게라도 가볍게 정리해보니 좀 더 명확하게 이해하게 된다.
그리고 책에서는 Swagger와 Logging에 대한 내용도 포함하고 있지만 기능 개발 파트 후에 해도 될 것 같다.
'spring & java' 카테고리의 다른 글
[Spring] Spring Security + OAuth2 + JWT (25) | 2024.01.22 |
---|---|
[Spring] SSE + Redis pub/sub (1) | 2024.01.18 |
[Spring] 스프링 부트 핵심 가이드 - Spring 기초 지식 (0) | 2023.10.22 |
[Spring] Spring AOP 적용 (0) | 2023.06.19 |
[Java] 비동기 + multi threading 구현 (0) | 2023.03.01 |