스프링 #5 HTTP 요청 처리

함형주·2022년 10월 13일
0

spring

목록 보기
5/12

질문, 피드백 등 모든 댓글 환영합니다.

스프링에서 HTTP 요청을 처리하고 응답하는 방법을 정리하겠습니다.
Sevlet 같이 낮은 레벨에서 HTTP 요청을 다루는 방법보단 실제로 스프링(스프링부트)를 사용하여 개발할 때 요청을 처리하는 방법을 정리하겠습니다.

HTTP 구조

HTTP는 HyperText Transfer Protocol로 데이터를 전송하는 방식 중 하나입니다.
HTTP 요청은 크게 Start Line, Header, Message Body로 구분 할 수 있습니다.

Start-Line : HTTP 요청 메서드, 요청 경로, HTTP 버전 등을 포함하며 서버가 수행해야 할 동작을 지정

Header : - HTTP 전송에 필요한 부가 정보를 포함. ex) 메시지 바디의 내용, 크기, 압축, 인증, 브라우저 정보, 캐시 관리 정보 등

Message Body : Byte의 형태로 표현할 수 있는 모든 데이터

HTTP 요청과 응답은 대부분 컨트롤러를 통해 처리합니다.

HTTP 요청 처리

컨트롤러 관련 어노테이션

@Controller : 해당 클래스를 스프링빈으로 등록하고 컨트롤러 계층으로 인식 (컴포넌트 스캔의 대상)
@RequestMaaping : 요청 URL을 매핑, 등록된 URL이 호출되면 해당 필드의 메서드 실행
@GetMapping, PostMapping, PutMapping, PatchMapping, DeleteMapping, : 각 HTTP 메서드 요청을 처리

  • @GetMaaping("/get") = @RequestMapping(value = "/get", method = RequestMethod.GET) 이며 이 방식으로 대부분의 요청을 처리

@RequestParam : HTTP 요청 파라미터 매핑
@ModelAttribute : HTTP 요청 파라미터를 객체로 변환
@RequestBody : HTTP 요청 메시지 바디를 매핑
@ResponseBody : HTTP Message Body에 직접 입력 가능

정적 리소스 응답

스프링은 HTTP 요청에 대한 응답으로 브라우저에 정적인 html, css, js 등을 제공할 수 있습니다.

스프링 부트는 /static , /public , /resources , /META-INF/resources 경로에 있는 정적 리소스를 참고할 수 있으며 기본적으론 src/main/resources/static 을 제공합니다.

src/main/resources/static/spring/basic.html 경로의 정적 리소스를
http://..../spring/basic.html로 브라우저에서 접근 시 해당 리소스에 접근할 수 있습니다.

뷰 템플릿을 통한 동적 리소스 응답

스프링은 뷰 템플릿을 거쳐 동적인 리소스를 제공합니다. (자바 템블릿 엔진으로 jsp, thymeleaf 등이 존재. 이 블로그에선 Thymeleaf를 예시로 사용)
주로 동적으로 html을 생성하여 사용하지만 뷰 템플릿으로 생성 가능한 모든 것을 응답 결과로 제공할 수 있습니다.

타임리프를 사용하는 경우 classpath가 기본으로 아래의 경로로 설정 되어있습니다.
prefix=classpath:/templates/ suffix=.html

스프링부트의 기본 뷰 템플릿 경로는 src/main/resources/templates 이며
src/main/resources/templates/spring/basic.html 을 응답 결과로 제공하기 위해선 컨트롤러를 사용 해야합니다.

ModelAndView 반환

@Controller
public class ExController {

	@RequestMapping("/home")
    public ModelAndView home() {
    	return new ModelAndView("spring/basic");
    }
}

"/home" 경로로 요청이 발생하면 ModelAndView를 반환하여 spring/basic.html을 렌더링.

	@RequestMapping("/home")
    public ModelAndView home(Object object) {
    	ModelAndView modelAndView = new ModelAndView("spring/basic");
        modelAndView.addObject("object", object); // 임의의 객체
    	return modelAndView;
    }

addObject를 이용하여 Model에 데이터를 저장

하지만 이러한 방식은 개발자가 ModelAndView를 직접 반환하기 때문에 불편합니다. 스프링은 HandlerAdaptor를 통해 ModelAndView를 직접 반환하지 않고 뷰의 논리이름을 반환할 수 있도록 지원합니다. 때문에 실제 개발 시 아래의 방식을 주로 사용합니다.

String 반환, HTTP 메서드 매핑 어노테이션 분리

	@GetMapping("/home")
    public String home() {
    	return "spring/basic";
    }
    
    @PostMapping("/home")
	public String postHome(Model model, Object object) {
    	model.addAttribute("object", object); // 임의의 객체
        return "spring/basic";
    }

GetPost 요청을 위와 같이 분리하여 처리
디스패처 서블릿으로부터 Model을 파라미터로 받아 데이터를 저장, view 논리이름 반환 (만약 Model을 사용할 필요가 없다면 생략 가능)
이 경우 spring/basic.html을 전송


참고로 void를 반환하는 경우 요청 경로를 기반으로 view 이름을 결정합니다.

ex) request/hello -> templates/request/hello.html

하지만 이러한 방식은 가독성이 떨어질 뿐만 아니라 실제로 리소스의 경로가 요청 경로와 일치하는 경우도 거의 없기에 잘 사용하지는 않습니다.


본격적으로 HTTP 요청을 처리하는 방법을 알아보겠습니다.

HTTP FORM, 쿼리 파라미터 요청 처리

HTTP Form 요청이나 쿼리 파라미터 요청은 형식이 같으므로 같은 방식으로 처리 가능합니다.
ex) name=userA&age=20
차이가 있다면 쿼리파라미터의 경우 요청 경로에 데이터가 포함되므로 요청 메서드에 무관하게 사용 가능하지만 form은 Get 메서드 요청이 불가능 합니다.

@RequestParam

HTTP 요청 파라미터를 단순 타입으로 바인딩 할 경우 @RequestParam으로 처리 가능
ex) /home?name=userA 로 요청 시

    @GetMapping("/home")
	public String postHome(@RequestParam("name") String name) {
    	System.out.println(name); // 결과 : "userA"

        return "spring/basic";
    }

HTTP 요청 파라미터 이름과 변수 이름이 같으면 @RequestParamvalue("name") 생략 가능
String, Integet, int 등의 단순 타입일 경우 @RequestParam도 생략 가능

요청이 /home?name= 형식으로 온다면 String의 경우 null이 아닌 "" 이 넘어옴
숫자 타입의 경우 Integer 형식이라면 null을 받을 수 있지만 int는 받지 못하므로 예외 발생
required 속성 true로 설정 시 예외 발생
defaultValue 속성으로 기본값 설정 가능 (기본값이 있으므로 required는 의미 없음)

@ModelAttribute

만약 요청 파라미터를 기반으로 객체를 생성해야 한다면 @ModelAttribute 사용할 수 있습니다.

public class User {
	String name;
    int age;
}

/home?name=userA&age=20 요청 시

    @PostMapping("/home")
	public String postHome(@ModelAttribute("user") User user) {
        return "spring/basic";
    }

@ModelAttribute는 파라미터를 객체로 변환시키는 기능 뿐만 아니라 그 객체를 모델에 저장하는 기능을 포함하고 있습니다. ( model.addAtrbute(user) )

@ModelAttribute는 기본적으로 기본 생성자를 통해 객체를 생성합니다. single unique constructor가 있을 경우엔 해당 생성자를 호출하지만 만약 생성자가 여러개거나 없다면 기본생성자를 호출합니다.
생성자가 정의되있다면 해당 생성자를 통해 값을 바인딩하지만 기본생성자의 경우 값을 바인딩 할 수 없기 때문에 프로퍼티 접근법(setter)를 통해 값을 바인딩합니다.

  • @ModelAttributevalue("user")는 파라미터 이름과 변수 이름이 같을 경우 생략 가능
    객체일 경우 @ModelAttribute 생략 가능

  • 어노테이션 생략 시 기본 타입은 @RequestParam으로 매핑, 객체 타입은 @ModelAttribute로 매핑

HTTP 요청 - JSON 처리

현재 대부분의 API 통신에 JSON을 사용합니다. HttpServletRequest, HttpEntity 등으로 직접 message body를 읽어 json으로 변환할 수도 있지만 스프링은 편리하게 json을 처리할 수 있는 기능을 제공합니다.

@RequestBody

@RequestBody는 HTTP 요청의 메시지 바디를 매핑하는 어노테이션으로 기본적으론 String 타입으로 매핑하지만 객체 타입으로도 매핑이 가능합니다.

	@ResponseBody 
    @PostMapping("/homeApi")
	public String postHome(@RequestBody User user) {
        return "message body 직접 입력";
    }

@RequestBody를 사용하면 마치 @ModelAttribute를 사용한 것 처럼 JSON 데이터를 분석하여 객체로 매핑
@RequestBody는 생략 불가능 (기본 타입일 경우 @RequestParam, 객체 타입일 경우 @ModelAttribute 적용)

@RequestBody은 HTTP 요청의 content-typeapplication/json 인 경우에만 동작합니다.
@PostMapping(value = "/homeApi", consumes = "application/json)
@PostMapping(value = "/homeApi", consumes = MediaType.APPLICATION_JSON_VALUE)
를 이용하여 검증할 수 있고 맞지 않는다면 415 상태코드 반환합니다.

@ResponseBody

@ResponseBody를 통해 HTTP Message Body에 직접 데이터를 입력할 수 있습니다.
String 타입을 반환하여 JSON 형식으로 데이터를 입력하거나 HttpEntity를 상속받은 ResponseEntity를 이용하여 JSON 형식으로 반환할 수도 있지만 여러 번거로움이 존재합니다.
스프링은 반환된 객체를 JSON으로 변환하여 응답 결과를 전송할 수 있는 기능을 제공합니다.

	@ResponseBody 
    @PostMapping("/homeApi")
	public User postHome(@RequestBody User user) {
        return user;
    }

반환된 JSON 형태

{
  "name":"user"
  "age":20
}

스프링 MVC가 데이터를 다루는 방법

이렇게 HTTP 요청을 처리하기 위해 컨트롤러를 사용하다보면 여러가지 궁금증이 생길 수 있습니다.
스프링이 제공하는 @RequestParam, @RequestBody, @ModelAttribute 등이 어떤 원리로 데이터를 변환하고 처리하는지, 또한 컨트롤러가 매개변수로 받는 Model, HttpServletRequest, 객체 등은 분명 어디선가 생성하여 넘겨주는 것일텐데 우리는 그러한 기능을 구현하거나 세부적인 사항을 지정해준 적이 없습니다.

스프링 MVC는 이런 기능을 HttpMessageConverter, HandlerMethodArgumentResolver 등으로 구현해 놓았습니다.

그 내용은 다음 블로그에 이어서 작성하겠습니다.

profile
평범한 대학생의 공부 일기?

0개의 댓글