스프링 MVC 핵심 기술
HTTP 요청 맵핑하기
HTTP Method
GET 요청
- 클라이언트가 서버의 리소스를 요청할 떄 사용
- 캐싱 가능 (조건적 GET 가능)
- 브라우저의 기록에 남고 북마크가 가능
- URL이 다 보이므로 민감한 데이터에는 맞지 않음
- idempotent
POST 요청
- 클라이언트가 서버의 리소스를 수정하거나 새로만들 떄 사용
- 서버에 보내는 데이터를 POST 요청 본문에 담는다
- 캐싱 불가능
- 브라우저 기록에 남지 않고 북마크가 불가능
- 데이터 길이 제한이 없다
PUT 요청
- URI에 해당하는 데이터를 새로 만들거나 수정할 떄 사용
- POST와 다른 점은 "URI"에 대한 의미에 있다
- POST의 URI는 보내는 데이터를 처리할 리소스
- PUT의 URI는 보내는 데이터에 해당하는 리소스
- idempotent
PATCH 요청
- PUT과 비슷하지만, 기존 엔티티와 새 엔티티의 차이점만 보낸다는 차이가 있다.
- idempotent
DELETE 요청
- URI에 해당하는 리소스를 삭제할 떄 사용
- idempotent
스프링 웹 MVC에서 HTTP Method 맵핑하기
@RequestMapping : http 요청 맵핑 어노테이션, 메서드 종류를 지정하지 않으면 기본적으로 모든 메서드를 허용하게 된다.
즉, 메서드 종류를 지정한다는 것은 허용하는 요청의 종류를 제한하는 것과 같다.
@ResponseBody : 리턴을 응답 본문으로 전달하는 어노테이션
@GetMapping : @RequestMapping(method=RequestMethod.GET)
URI 패턴 맵핑
@RequestMapping에서 지원하는 패턴
- ? : 한 글자
- * : 여러 글자
- ** : 여러 패스
- 정규표현식 가능 (ex: "/{name: [a-z]+}")
만약 패턴이 중복되는 경우, 가장 구체적으로 맵핑되는 핸들러가 선택됨
컨텐츠 타입 맵핑
특정 타입의 데이터를 담고있는 요청만 처리하도록 핸들러 설정
@RequestMapping(consumes=MediaType.APPLICATION_JSON_UTF8_VALUE)
Content-Type 헤더로 필터링특정 타입의 응답을 만드는 핸들러 설정
@RequestMapping(produces=MediaType.APPLICATION_JSON_UTF8_VALUE)
Accept 헤더로 필터링, 만약 요청 헤더에 accept가 비워진 경우에는 그냥 처리해줌
consumes/produces를 클래스의 @RequestMapping 어노테이션에서도 설정할 수 있는데,
이때 메서드(핸들러)의 @RequestMapping의 consumes/produces가 설정되면 클래스의 것은 무시된다.
헤더와 파라미터 맵핑
- @RequestMapping(headers = "key") : 특정 헤더가 있는 요청만 처리
- @RequestMapping(headers = "!key") : 특정 헤더가 없는 요청만 처리
- @RequestMapping(headers = "key=value") : 특정 헤더/키 쌍이 있는 요청만 처리
- @RequestMapping(params = "a") : 특정 요청 매개변수 키를 가지고 있는 요청만 처리
- @RequestMapping(params = "!a") : 특정 요청 매개변수가 없는 요청만 처리
- @RequestMapping(params = "a=b") : 특정 요청 매개변수 키/값 쌍을 가지고 있는 요청만 처리
HEAD와 OPTIONS 요청 처리
HEAD와 OPTIONS http method는 스프링 웹 MVC가 기본으로 제공해주는 기능이다.
- HEAD : GET 요청과 동일하지만 응답 본문을 받아오지 않고 응답 헤더만 받아온다.
- OPTIONS : 응답 헤더 ALLOW로 사용할 수 있는 HTTP Method 목록을 받아온다.
커스텀 애노테이션
메타 애노테이션
애노테이션에 사용할 수 있는 애노테이션
스프링이 제공하는 대부분의 애노테이션은 메타 애노테이션으로 사용할 수 있다조합 애노테이션
한개 혹은 여러개의 메타 애노테이션을 조합해서 만든 애노테이션
코드를 간결하게 줄일 수 있다
보다 구체적인 의미를 부여할 수 있다@Retantion
해당 애노테이션 정보를 언제까지 유지할 것인가 결정- Source : 소스 코드까지만 유지, 컴파일하면 해당 애노테이션 정보는 사라짐
- Class : 컴파일 한 .class 파일에도 유지, 런타임시 클래스를 메모리로 읽어오면 해당 정보는 사라짐 (default)
- Runtime : 클래스를 메모리로 읽어왔을때까지 유지, 코드에서 이 정보를 바탕으로 특정 로직을 실행할 수 있다
@Target
해당 애노테이션을 어디에 사용할 수 있는지 결정@Documented
해당 애노테이션을 사용한 코드의 문서에 그 애노테이션에 대한 정보를 표기할 지 결정
@RequestMapping(method = RequestMethod.GET, value = "/hello")
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GetHelloMapping {
}
핸들러 메소드
핸들러 메소드 아규먼트와 리턴 타입
- 핸들러 메소드 아규먼트
: 주로 요청 그 자체 또는 요청에 들어있는 정보를 받아오는데 사용
WebRequest, NativeWebRequest, ServletRequest, HttpServletRequest : 요청 또는 응답 자체에 접근 가능한 API
InputStream, Reader, OutputStream, Writer : 요청 본문을 읽어오거나, 응답 본문을 쓸 때 사용할 수 있는 API
PushBuilder : HTTP/2 리소스 푸쉬에 사용 (스프링 5)
HttpMethod : 요청의 Http 메서드에 대한 정보
Locale, TimeZone, ZoneId : LocaleResolver가 분석한 요청의 Locale 정보
@PathVariable : URL 템플릿 변수 읽을 때 사용
@MatrixVariable : URL 경로 중에 키/값 쌍을 읽어올 때 사용
@RequestParam : 서블릿 요청 매게변수 값을 선언한 메소드 아규먼트 타입으로 변환, 단순 타입에서는 생략 가능
@RequestHeader : 요청 해더 값을 선언한 메소드 아규먼트 타입으로 변환
@RequestBody : 요청 본문을 HttpMessageConverter를 사용해 특정 타입으로 변환
- 핸들러 메소드 리턴
: 주로 응답 또는 모델을 렌더링할 뷰에 대한 정보를 제공하는데 사용
@ResponseBody : 리턴 값을 HttpMessageConverter를 사용해 응답 본문으로 사용한다.
HttpEntity, ResponseEntity : 응답 본문 뿐 아니라 헤더 정보까지 전체 응답을 만들 때 사용한다.
String : ViewResolver를 사용해서 뷰를 찾을 떄 사용할 뷰 이름
View : 암묵적인 모델 정보를 랜더링할 뷰 인스턴스
Map, Model : 암묵적으로 판단한 뷰를 랜더링할 때 사용할 모델 정보
@ModelAttribute : 암묵적으로 판단한 뷰를 랜더링할 때 사용할 모델 정보에 추가한다.
URI 패턴을 아규먼트로
@PathVariable
요청 URL 패턴의 일부를 핸들러 메소드 아규먼트로 받는 방법
타입 변환 지원
값이 반드시 있어야 한다.
Optional 지원@MatrixVariable
@PathVariable과 유사하나 아규먼트로 키/값쌍을 받는다.
별도의 활성화가 필요하다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
//세미콜론을 제거하지 않도록 설정
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}
@GetMapping("/events/{id}")
@ResponseBody
public Event getEvent(@PathVariable("id") Long id, @MatrixVariable String name) {
//key = "name", value = ?
Event event = new Event();
event.setId(id);
event.setName(name);
return event;
}
요청 매개변수를 아규먼트로
- @RequestParam
요청 매개변수에 있는 단순 타입 데이터를 메서드 아규먼트로 받을 수 있다. ("/events?id=20" 또는 폼 데이터)
값이 반드시 있어야 한다. (또는 required=false, Optional 사용하여 기본값 설정 등)
String이 아닌 값들은 타입 변환을 지원한다.
Map을 사용하여 모든 매개변수를 받아올 수 있다.
@GetMapping("/events")
@ResponseBody
public Event getEvent(@RequestParam("id") Long id) {
Event event = new Event();
event.setId(id);
return event;
}
@ModelAttribute
여러 곳에 있는 단순 타입 데이터를 복합 타입 객체로 받아오거나 해당 객채를 새로 만들 때 사용
@GetMapping("/events")
@ResponseBody
public Event getEvents(@ModelAttribute Event event) {
return event;
}
복합 타입 객체의 각 데이터를 바인딩 하는 방법은 URL, 요청 매개변수, 세션 등이 있다.
값을 바인딩할 수 없는 경우 400 에러가 발생하는데,
이 에러를 직접 다루고 싶은 경우 BindingResult 타입의 아규먼트를 추가한다.
@GetMapping("/events")
@ResponseBody
public Event getEvents(@ModelAttribute Event event, BindingResult bindingResult) {
if(bindingResult.hasErrors()) {
System.out.println("================");
bindingResult.getAllErrors().forEach(c -> {
System.out.println(c);
});
}
return event;
}
에러 발생 없이 에러정보는 BindingResult에 들어가고, 바인딩 되지 않은 값은 null이 된다.
만약 바인딩 이후에 추가적인 검증작업이 필요한 경우 @Valid 또는 @Validated 애노테이션 사용
(ex) Event 객체의 limit 속성은 항상 1이상이어야 한다.
@Min(1)
private Integer limit;
@GetMapping("/events")
@ResponseBody
public Event getEvents(@Valid @ModelAttribute Event event, BindingResult bindingResult) {
if(bindingResult.hasErrors()) {
System.out.println("================");
bindingResult.getAllErrors().forEach(c -> {
System.out.println(c);
});
}
return event;
}
이때 @Valid 검증에 의해 bindingResult에 에러가 들어감에도 불구하고
해당 값은 바인딩 됨에 유의.
@Validated
@Valid와는 다르게 validation group이라는 힌트를 사용하여 그룹 클래스를 지정할 수 있다.
그룹을 지정하지 않은 경우 @Valid와 동일하게 동작한다.
두개의 validation group 생성
interface ValidateLimit {}
interface ValidateName {}
private Long id;
@NotBlank(groups = ValidateName.class)
private String name;
@Min(value = 1,groups = ValidateLimit.class)
private Integer limit;
특정 그룹만 검증하기
@GetMapping("/events")
@ResponseBody
public Event getEvents(@Validated(Event.ValidateName.class) @ModelAttribute Event event, BindingResult bindingResult) {
if(bindingResult.hasErrors()) {
System.out.println("================");
bindingResult.getAllErrors().forEach(c -> {
System.out.println(c.toString());
});
}
return event;
}
@SessionAttributes
모델 정보를 HTTP 세션에 저장해주는 애노테이션
HttpSession을 직접 사용할 수도 있지만 @SessionAttributes를 사용하면
설정한 이름에 해당하는 모델 정보를 자동으로 세션에 저장해준다.
여러 화면 또는 요청에서 사용되는 객체를 공유할 때 사용한다.
@Controller
@SessionAttributes("event")
public class EventController {
SessionStatus를 사용해서 특정 폼 처리 완료 후에 세션 데이터를 비우도록 할 수 있다.
@PostMapping("/events")
public String createEvent(@Validated @ModelAttribute Event event,
BindingResult bindingResult,
SessionStatus sessionStatus) {
if(bindingResult.hasErrors()) {
return "/events/form";
}
//todo save
sessionStatus.setComplete();
return "redirect:/events/list";
}
@SessionAttribute
HTTP 세션에 들어있는 데이터를 참조할 떄 사용한다.
타입 컨버전을 지원, 데이터 수정을 위해서는 HttpSession 사용
@SessionAttributes는 해당 컨트롤러 내에서만 특정 모델 객체를 공유할때 사용하는 한편,
@SessionAttribute는 컨트롤러 밖(인터셉터 또는 필터 등)에서 만들어준 세션 데이터에 접근할 때 사용한다.
접속 시간을 기록하여 Session에 저장하는 인터셉트를 생성하고 (등록 필요)
핸들러 메서드에서 사용하기
public class VisitTimeInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
if (session.getAttribute("visitTime") == null) {
session.setAttribute("visitTime", LocalDateTime.now());
}
return true;
}
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new VisitTimeInterceptor());
}
@GetMapping("/events/list")
public String getEvents(@ModelAttribute Event event,
Model model,
SessionStatus sessionStatus,
@SessionAttribute LocalDateTime visitTime) {
System.out.println(visitTime);
List<Event> eventList = new ArrayList<>();
eventList.add(event);
model.addAttribute("eventList",eventList);
sessionStatus.setComplete();
return "/events/list";
}
RedirectAttributes
리다이렉트 할 때는 Model에 들어있는 primitive type 데이터가 URI 쿼리 매개변수에 추가된다.
(ex localhost:8080/events/list?name=mvc&limit=50)
스프링부트에서 기능 활성화를 위해서는 Ignore-default-model-on-redirect 프로퍼티를 사용해야 한다.
application.properties
spring.mvc.ignore-default-model-on-redirect=false
리다이렉트할 때 원하는 데이터만 전달하고 싶을 때는 RedirectAttributes에 명시적으로 추가하여 사용할 수 있다.
@PostMapping("/events/form/limit")
public String eventsFormLimitSubmit(@Validated @ModelAttribute Event event,
BindingResult bindingResult,
SessionStatus sessionStatus,
RedirectAttributes attributes) {
if(bindingResult.hasErrors()) {
return "/events/form-limit";
}
//todo save
attributes.addAttribute("name", event.getName());
attributes.addAttribute("limit", event.getLimit());
sessionStatus.setComplete();
return "redirect:/events/list";
}
리다이렉트 요청을 처리하는 곳에서 쿼리 매개변수를 @RequestParam 또는 @ModelAttribute로 받을 수 있다.
@GetMapping("/events/list")
public String getEvents(@ModelAttribute("newEvent") Event event,
Model model,
@SessionAttribute LocalDateTime visitTime) {
System.out.println(visitTime);
List<Event> eventList = new ArrayList<>();
eventList.add(event);
model.addAttribute("eventList",eventList);
return "/events/list";
}
요청 매개변수를 복합객체로 받기 위해 @ModelAttribute를 사용할 때,
@SessionAttributes에서 세션에 저장한 이름과 동일하게 받는다면
우선적으로 객체를 세션에서 찾게 되기 때문에 에러가 발생할 수 있다.
그러므로 SessionAttributes와는 다른 이름으로 받아야 요청 매개변수를 통해 객체로 바인딩할 수 있다.
Flash Attributes
주로 리다이렉트 중에 데이터를 전달하는 목적으로 사용한다.
데이터가 URI에 노출되지 않고 임의의 객체를 사용할 수 있다.
RedirectAttributes의 addFlashAttribute 메서드를 사용하여 객체를 HTTP 세션에 저장하여 넘겨준다.
리다이렉트 요청을 처리 한 다음 세션에서 제거된다.
@PostMapping("/events/form/limit")
public String eventsFormLimitSubmit(@Validated @ModelAttribute Event event,
BindingResult bindingResult,
SessionStatus sessionStatus,
RedirectAttributes attributes) {
if(bindingResult.hasErrors()) {
return "/events/form-limit";
}
//todo save
attributes.addFlashAttribute("newEvent",event);
sessionStatus.setComplete();
return "redirect:/events/list";
}
넘겨받은 객체는 Model에 자동으로 들어가기 때문에
리다이렉트 요청을 처리하는 곳에서는 @ModelAttribute로 선언하여 객체를 받을 필요 없이
Model에서 꺼내어 쓰면 된다.
@GetMapping("/events/list")
public String getEvents(Model model,
@SessionAttribute LocalDateTime visitTime) {
System.out.println(visitTime);
List<Event> eventList = new ArrayList<>();
eventList.add((Event) model.asMap().get("newEvent"));
model.addAttribute("eventList",eventList);
return "/events/list";
}
MultipartFile
파일 업로드에 사용하는 메소드 아규먼트
MultipartResolver 빈이 설정되어 있어야 사용할 수 있다.
(스프링부트에서는 MultipartAutoConfiguration에 의해 자동으로 설정된다.)
POST mulripart/form-data 요청이 들어있는 파일을 참조할 수 있다.
List
파일 업로드 폼 예시
<body>
<div th:if="${message}">
<h2 th:text="${message}"/>
</div>
<form method="POST" enctype="multipart/form-data" action="#" th:action="@{/file}">
File : <input type="file" name="file"/>
<input type="submit" name="Upload"/>
</form>
@GetMapping("/file")
public String fileUploadForm(Model model) {
return "files/index";
}
@PostMapping("/file")
public String fileUpload(@RequestParam MultipartFile file,
RedirectAttributes attributes) {
//todo file storage service
String message = file.getOriginalFilename() + " is uploaded.";
attributes.addFlashAttribute("message",message);
return "redirect:/file";
}
File Download
스프링 ResourceLoader 사용
- ResouceLoader
: 리소스를 읽어오는 기능을 제공하는 인터페이스
Resource getResource(String location);
파일 다운로드 응답 헤더에 설정 할 내용
- CONTENT_DISPOSITION : 사용자가 파일을 받을 때 사용할 파일 이름
- CONTENT_TYPE : 파일의 미디어 타입
- CONTENT_LENGTH : 파일의 크기
@Autowired
private ResourceLoader resourceLoader;
@GetMapping("/file/{filename}")
public ResponseEntity<Resource> fileDownload(@PathVariable String fileName) throws IOException {
Resource resource = resourceLoader.getResource("classpath:"+fileName);
File file = resource.getFile();
Tika tika = new Tika();
String mediaType = tika.detect(file);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""+resource.getFilename()+"\"")
.header(HttpHeaders.CONTENT_TYPE, mediaType)
.header(HttpHeaders.CONTENT_LENGTH, file.length()+"")
.body(resource);
}
- 리턴 타입 ResponseEntity : 응답 본문, 응답 헤더, 응답 상태 코드를 설정할 수 있는 리턴 타입이다. <>에는 응답 본문의 타입을 넣어준다.
- Tika : File의 미디어 타입을 알아낼 수 있다.
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>1.20</version>
</dependency>
@RequestBody & HttpEntity
- @RequestBody
요청 본문에 들어있는 데이터를 HttpMessageConverter를 통해 변환한 객체로 받을 수 있다.
@Valid 또는 @Validated와 같이 사용하여 검증을 진행할 수 있다.
BindingResult 아규먼트를 사용해 코드로 바인딩 또는 검증 에러를 확인할 수 있다.
@RestController
@RequestMapping("/api/events")
public class EventApi {
@PostMapping
public Event createEvent(@RequestBody Event event) {
return event;
}
}
- HttpEntity
@RequestBody와 비슷하지만 추가적으로 요청 헤더 정보를 사용할 수 있다.
@RestController
@RequestMapping("/api/events")
public class EventApi {
@PostMapping
public Event createEvent(HttpEntity<Event> request) {
System.out.println(request.getHeaders().getContentType());
return request.getBody();
}
}
@ResponseBody & ResponseEntity
- @ResponseBody
핸들러 메서드의 리턴 데이터를 HttpMessageConverter를 통해 변환 후에 응답 본문 메시지로 보낼때 사용
@RestController를 사용하면 모든 핸들러 메서드에 적용된다.
@PostMapping
@ResponseBody
public Event createEvent(@RequestBody @Valid Event event) {
return event;
}
- ResponseEntity
응답 헤더 상태 코드 본문을 직접 다루고 싶은 경우에 사용한다.
@PostMapping
public ResponseEntity<Event> createEvent(@RequestBody @Valid Event event,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest().build();
}
return ResponseEntity.ok(event);
}
Controller 설정
@ModelAttribute의 다른 사용법
- 해당 컨트롤러의 모든 요청에서 공통적으로 사용하는 모델 초기화하기
@ModelAttribute
public void categories(Model model) {
model.addAttribute("categories",List.of("study", "seminar", "hobby", "social"));
}
- 메서드에 붙이면 리턴하는 객체를 모델에 넣어준다.
이 때는 RequestToViewNameTranslator에 의해 URL 이름으로 view 선택
@GetMapping("/events/form-name")
@ModelAttribute
public Event newEvent() {
return new Event();
}
DataBinder : @InitBinder
특정 컨트롤러에서 바인딩 또는 검증 설정을 변경하고 싶을 때 사용
바인딩 설정
webDataBinder.setDisallowedFields();
포매터 설정
webDataBinder.addCustomFormatter();
Validator 설정
webDataBinder.addValidators();
또는 특정 이름의 모델 객체에만 바인딩 또는 검증 설정 적용
@InitBinder("event")
이벤트에 대한 데이터를 바인딩할때 id를 바인딩하지 않도록 설정
@InitBinder("event")
public void initEventBinder(WebDataBinder webDataBinder) {
webDataBinder.setDisallowedFields("id");
}
예외 처리 핸들러 : @ExceptionHandler
특정 예외가 발생한 요청을 처리하는 핸들러를 정의한다.
일반적인 핸들러 메서드와 비슷하게 작성할 수 있다.
@ExceptionHandler
public String eventErrorHandler(EventException exception, Model model) {
model.addAttribute("message", "event error");
return "error";
}
Rest API의 경우 보통 ResponseEntity를 사용하여 예외에 대한 정보를 응답 본문으로 전달한다.
@ExceptionHandler
public ResponseEntity errorHandler(EventException exception) {
return ResponseEntity.badRequest().body("can't create event.");
}
전역 컨트롤러 : @ControllerAdvice
@InitBinder, @ExceptionHandler, @ModelAttribute 를 모든 컨트롤러에서 사용하고 싶을 때 사용
@ControllerAdvice
public class GlobalController {
@ExceptionHandler
public String eventErrorHandler(EventException exception, Model model) {
model.addAttribute("message", "event error");
return "error";
}
@InitBinder
public void initEventBinder(WebDataBinder webDataBinder) {
webDataBinder.setDisallowedFields("id");
}
@ModelAttribute
public void categories(Model model) {
model.addAttribute("categories", List.of("study", "seminar", "hobby", "social"));
}
}
적용할 범위를 지정할 수 있다.
- 특정 애노테이션이 걸려있는 컨트롤러에만 적용
@ControllerAdvice(annotations = RestController.class)
- 특정 패키지 이하의 컨트롤러에만 적용
@ControllerAdvice("me.rockintuna.demowebmvc")
- 특정 클래스 타입에만 적용
@ControllerAdvice(assignableTypes = {EventController.class, EventApi.class})