서블릿과 스프링 MVC

zunzero·2022년 8월 9일
0

스프링, JPA

목록 보기
3/23

서블릿

서블릿 만들기

HttpServlet을 상속(extend)받아 만든 클래스에 @WebServlet 애노테이션을 붙이면 서블릿 컨테이너에 서블릿 객체가 생성되어 저장된다.
HTTP 요청 시, WAS는 request와 response 객체를 새로 만들어 해당 url에 mapping된 서블릿 객체를 호출한다.
개발자는 request 객체에서 HTTP 요청 정보를 편리하게 꺼내 사용하고 response 객체에 HTTP 응답 정보를 편리하게 입력한다.
WAS는 response 객체에 담겨있는 내용으로 HTTP 응답 정보를 생성한다.

HttpServletRequest, HttpServletResponse

HTTP 요청 메시지를 개발자가 직접 파싱해서 사용해도 되지만, 이는 무척이나 불편하다.
서블릿은 개발자가 HTTP 요청 메시지를 편리하게 사용할 수 있도록 개발자 대신에 HTTP 요청 메시지를 파싱하여, 그 결과를 HttpServletRequest 객체에 담아 제공한다.
이는 HTTP 요청 메시지의 start line, 헤더, 바디를 편리하게 조회하는 기능 뿐 아니라, 임시저장소기능, 세션관리 기능과 같은 여러 부가기능도 제공한다.

HttpServletResponse 는 기본적으로 HTTP 응답 코드를 지정할 수 있게 하고, 헤더와 바디를 생성하여 편리하게 HTTP 응답 메시지를 생성할 수 있게 하는 등 여러 편의 기능을 제공한다.

서블릿 컨테이너

톰캣처럼 서블릿을 지원하는 WAS를 서블릿 컨테이너라고 한다.
서블릿 컨테이너는 서블릿 객체를 생성, 초기화, 호출, 종료하는 생명주기를 관리한다.
서블릿 객체는 싱글톤으로 관리된다.
또한 동시요청을 위한 멀티 쓰레드 처리를 지원한다.
서블릿은 아래 그림의 밑줄 그어진 부분을 개발자 대신 해줌으로써 개발자는 의미있는 비즈니스 로직에만 집중할 수 있도록 한다.

동시요청 - 멀티쓰레드

자바 메인 메서드를 처음 실행하면 main이라는 이름의 쓰레드가 실행된다.
쓰레드는 한 번에 하나의 코드라인만 순차적으로 수행하는데, 동시 처리가 필요하면 쓰레드를 추가로 생성한다.
HTTP 요청이 들어오면 해당 url에 매핑된 서블릿을 호출해야 하는데, 이 때 서블릿을 호출하기 위한 쓰레드가 필요하다.
동시에 많은 요청이 들어오게 되었을 때, 쓰레드를 하나만 사용하게 되면 요청1에 대한 처리가 지연될 경우 요청2는 쓰레드를 대기하는 시간이 생겨, 클라이언트 입장에서는 요청1과 요청2 모두 지연된다고 판단할 수 밖에 없다.
이럴 때, 요청마다 쓰레드를 생성하면 된다고 생각할 수 있는데 이는 장단점이 있다.
동시 요청에 대해 리소스(CPU, 메모리)가 허용될 때까지 처리할 수 있다는 장점이 있다.
반면, 쓰레드 생성 비용은 매우 비싸고 생성시간 때문에 응답 속도가 늦어지고, 쓰레드 컨텍스트 스위칭 비용이 추가로 발생하며, 클라이언트 요청이 너무 많을 시 CPU, 메모리 임계점을 넘겨 서버가 죽을 수 있다는 단점이 있다.

쓰레드 풀

쓰레드 풀은 미리 쓰레드를 몇 개 생성해서 보관해둔다.
톰캣은 최대 200개가 기본 설정이며, 이는 변경 가능하다.
쓰레드가 미리 생성되어 있으므로 쓰레드를 생성하고 종료하는 비용이 절약되고 응답 시간이 빠르다.
또한 생성 가능한 쓰레드의 최대치가 있으므로, 너무 많은 요청이 들어와도 기존 요청은 안전하게 처리할 수 있다.
WAS의 이러한 멀티 쓰레드 지원 덕분에 개발자는 멀티 쓰레드 관련 코드를 크게 신경 쓰지 않아도 된다.

스프링 MVC 구조

스프링 MVC의 핵심은 Dispatcher Servlet이다.
스프링부트는 DispatcherServlet을 서블릿으로 자동으로 등록하면서 모든 경로(urlPatterns="/")에 대해서 매핑한다.
위 그림에서 핸들러라고 하는 것은 Controller라고 보는 것이 이해에 더 도움이 된다.
DispatcherServlet은 부모 클래스에서 HttpServlet을 상속 받아서 사용하고, 서블릿으로 동작한다.

DispatcherServlet에는 HandlerMapping List와 HandlerAdapter List가 있다.

HandlerMapping 핸들러 매핑에서 컨트롤러(핸들러)를 찾을 수 있어야 한다.

0. RequestMappingHandlerMapping: 애노테이션 기반으로 @RequestMapping 애노테이션이 붙은 핸들러(컨트롤러)를 조회한다.
1. BeanNameUrlHandlerMapping: 스프링 빈의 이름으로 핸들러(컨트롤러)를 조회한다.

핸들러 매핑 정보에서 핸들러(컨트롤러)를 찾으면, 해당 핸들러(컨트롤러)를 처리할 수 있는 핸들러 어댑터를 조회한다.

HandlerAdpater 핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.

0. RequestMappingHandlerAdapter: 애노테이션 기반으로 @RequestMapping 애노테이션이 붙은 핸들러를 처리할 수 있는 핸들러 어댑터를 조회한다.
1. HttpRequestHandlerAdapter: HttpRequestHandler를 처리할 수 있는 핸들러 어댑터를 조회한다.

나는 스프링으로 주로 API 서버를 만들기 때문에 viewResolver나 View에 대해선 깊게 들어가지 않았다.
컨트롤러에 @ResponseBody 애노테이션을 사용하거나, @Controller와 @ResponseBody 애노테이션을 모두 포함한 @RestController 애노테이션을 사용해 JSON 형식으로 response를 보낼 것이다.
이 때, jackson 라이브러리를 통해 자바 객체를 자동으로 JSON 문자로 변경할 수 있다.

스프링 MVC 기능

@RestController와 @Controller

위의 두 애노테이션은 모두 @Component 애노테이션이 들어있어 컴포넌트 스캔의 대상이 되고, 컴포넌트 스캔의 대상 범위에 포함된다면 해당 클래스를 스프링 빈으로 등록한다.
두 애노테이션의 차이점은 @RestController는 @ResponseBody 애노테이션을 내부에 포함하고 있어, 반환 값으로 뷰를 찾지 않고, HTTP 메시지 바디에 바로 입력한다.
@ResponseBody는 View 조회를 무시하고, HTTP message body에 직접 해당 내용을 입력한다.
@Controller 애노테이션은 반환값이 String이면 뷰를 찾고 뷰가 렌더링된다.

@RequestMapping("/hello/spring")

@RequestMapping 애노테이션은 해당 url이 호출되면 메서드가 실행되도록 매핑한다.
따로 HTTP 메서드를 지정하지 않으면 HTTP 메서드와 무관하게 호출되기 때문에 method 속성을 별도로 지정해주어야 한다.

@RequestMapping(value = "/hello/spring", method = RequestMethod.GET)

위와 같은 방식을 사용해도 되지만 HTTP 메서드에 맞춰, @GetMapping, @PostMapping 등을 사용하는 것이 더 편리하다.

HTTP 요청 url과 body 파싱하기

  1. PathVariable 경로변수
@ResponseBody
@GetMapping("/hello/{userId}")
public String mappingPath(@PathVariable("userId") String data) {
	return "ok";
}

@ResponseBody
@GetMapping("/hello2/{userId}/{orderId}")
public String mappingPath2(@PathVariable String userId, @PathVariable Long orderId) {
	return "ok";
}    

첫 번째 메서드는 @PathVariable의 이름과 파라미터의 이름이 다른 경우이고,
두 번째 메서드는 두 개의 이름이 같은 경우이다.

  1. 쿼리 파라미터
    GET 방식의 쿼리 파라미터 url의 예시로
    http://localhost:8080/request-param?username=hello&age=20
    위의 url에서 쿼리 파라미터는 username과 age이다.
    이 두개를 컨트롤러에서 어떻게 꺼내 사용할 수 있는 지 알아보자.
@GetMapping("/request-param")
public void requestParamV1(HttpServletRequest request) throws IOException {
	String username = request.getParameter("username");
    int age = Integer.parseInt(request.getParameter("age"));
}    

위의 소스 코드 같은 경우, 순전히 Servlet을 이용해 HTTP 요청의 쿼리 파라미터를 분석했는데, 이는 좀 번거롭다.

스프링이 제공하는 @RequestParam을 사용하면 요청 파라미터를 매우 편리하게 사용할 수 있다.

@RequestParam

@GetMapping("/request-param")
public void requestParamV2(@RequestParam("username") String MemberName, @RequestParam int age) {
	log.info("MemberName={}, age={}", MemberName, age);
}    

첫 번째 쿼리파라미터인 username은 소스코드에서 MemberName으로 사용되고, 두 번째 쿼리파라미터인 age는 소스코드에서도 그대로 사용된다.
String, int, Integer 등의 단순 타입의 경우 @RequestParam도 생략가능 하지만, 선호하지 않는다.

@RequestParam에는 required 옵션이 있는데, 이는 파라미터 필수 여부를 가리기 위한 것으로 기본 값은 true이다. 반면 @RequestParam 애노테이션을 생략하면 스프링 mvc는 내부에서 required=false를 적용한다.

@RequestParam(required=true, defaultValue="guest") String username,
@RequestParam(required=false) int age

1. /request-param -> 400 오류
2. /request-param?username=	-> 빈 문자로 통과

@ModelAttribute

@ModelAttribute를 사용하면 쿼리 파라미터를 멤버 변수로 갖는 객체로 쿼리파라미터들을 받을 수 있다.

@Data
public class HelloData {
	private String username;
    private int age;
}

@Data 애노테이션은 롬복을 통해 사용할 수 있다.
@Getter, @Setter, @ToString, @RequiredArgsConstructor 등을 자동으로 적용해준다.

@GetMapping("/model-attribute") 
public void modelAttribute(@ModelAttribute HelloData helloData) {
	log.info("username={}, age={}", helloData.getUsername(), helloData.getAge());
}    

스프링 MVC는 @ModelAttribute가 있으면 다음을 실행한다.

1. HelloData 객체 생성
2. 요청 파라미터 이름으로 HelloData 객체의 프로퍼티를 찾고, 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩)한다.

@ModelAttribute 또한 생략할 수 있으나 선호하지 않는다.

요청 파라미터를 조회하는 기능: @RequestParam, @ModelAttribute
HTTP 메시지 바디를 직접 조회하는 기능: @RequestBody

뒤에 나올 @RequestBody와 헷갈릴 수 있는데
@RequestBody는 HTTP 메시지 바디를 직접 조회하는 기능이고, 요청 파라미터를 조회하는 기능으로는 @RequestParam과 @ModelAttribute가 있다.
@RequestBody는 생략할 수 없으며, 만약 생략한다면 @ModelAttribute가 적용되어 HTTP 메시지 바디를 조회하는 것이 아니라 요청 파라미터를 조회하기 때문에 오류가 발생할 수 있다.

@RequestBody를 통한 HTTP 메시지 바디 조회

HTTP 메시지 바디를 조회하는 방법은 여러 가지가 있는데 @RequestBody를 사용하는 것이 가장 간편한 방법인 것 같다.
참고로 헤더 정보가 필요할 땐 @RequestHeader를 사용하면 편리하다.

@PostMapping("/request-body-string")
public void requestBodyString(@ReqeustBody String messageBody) {
	log.info("messageBody={}, messageBody);
}

HTTP 요청 메시지 - JSON

HTTP API에서 주로 사용하는 데이터 형식은 JSON이다.
이 때에도 다양한 방법이 있지만 @RequestBody를 사용하는 것이 가장 편리하다.

@ResponseBody
@PostMapping("/request-body-json")
public String requestBodyJson(@RequestBody HelloData data) {
	log.info("username={}, age={}", data.getUsername(), data.getAge());
    return "ok";
}    

이처럼 문자나 JSON을 객체로 변환해주는 역할을 수행하는 것은 HTTP 메시지 컨버터이다.
반대로 객체를 JSON 응답으로 전송할 수도 있다.

@ResponseBody
@PostMapping("/request-response-body-json")
public HelloData requestResponseBOdyJson(@ReqeustBody HelloData data) {
	log.info("username={}, age={}", data.getUsername(), data.getAge()):
    return data;
}    

이 때의 응답은
{
"username": "",
"age": "
"
}
와 같은 JSON 형식이 된다.

@RequestBody 요청: JSON 요청 -> HTTP 메시지 컨버터 -> 객체
@ResponseBody 응답: 객체 -> HTTP 메시지 컨버터 -> JSON 응답

@ResponseBody를 사용하면 HTTP 응답 코드를 동적으로 설정하기 까다롭다.
@ResponseStatus(HttpStatus.OK) 애노테이션을 통해 정적으로 설정하는 수 밖에 없다.
HTTP 응답 코드를 동적으로 설정하고자 할 때는 ResponseEntity<>를 사용한느 방법이 있다.

@PostMapping("/response-body-json-v1")
public ResponseEntity<HelloData> responseBodyJsonV1() {
	HelloData helloData = new HelloData();
    helloData.setUsername("UserA");
    helloData.setAge(20);
    
    return new ResponseEntity<>(helloData, HttpStatus.OK);
}

@ResponseStatus(HttpStatus.OK)
@ResponseBody
@PostMapping("response-body-json-v2")
public HelloData responseBOdyJsonV2() {
	HelloData helloData = new HelloData();
    helloData.setUsername("UserA");
    helloData.setAge(20);
    
    return helloData;
}    

HTTP 메시지 컨버터

뷰 템플릿으로 HTML을 생성해서 응답하는 것이 아니라, HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다.

@ResponseBody를 사용하게 되는 원리는 아래 그림과 같다.
@ResponseBody를 사용하게 되면 아래와 같은 과정이 일어난다.

1. HTTP의 Body에 문자 내용을 직접 반환
2. viewResolver 대신에 HttpMessageConverter가 동작
3. 기본 문자처리: StringHttpMessageConverter
4. 기본 객체처리: MappingJackson2HttpMessageConverter
기타: byte 처리 등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있음

스프링 MVC는 다음의 경우에 HTTP 메시지 컨버터를 적용한다.

HTTP 요청: @RequestBody, HttpEntity(RequestEntity)
HTTP 응답: @ResponseBody, HttpEntity(ResponseEntity)

참고

인프런 김영한 강사님의 스프링 MVC 1편 - 웹 개발 핵심기술 강의를 참고하여 포스팅하였습니다.

profile
나만 읽을 수 있는 블로그

0개의 댓글