참고자료
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
HTTP 메시지 컨버터
뷰 템플릿으로 HTML을 생성해서 응답하는 것이 아니라, HTTP API처럼 JSON 데이터를 HTTP 메시지 바디에서
직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하며 편리하다.
@ResponseBody를 사용
HTTP의 BODY에 문자 내용을 직접 반환
viewResolver 대신에 HttpMessageConverter가 동작
기본 문자처리: StringHttpMessageConverter
기본 객체처리: MappingJackson2HttpMessageConverter
canRead() , canWriter() : 메시지 컨버터가 해당 클래스, 미디어 타입을 지원하는지 체크
read() , writer() : 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능
스프링 부트 기본 메시지 컨버터
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter
스프링 부트는 다양한 메시지 컨버터를 제공하는데, 대상 클래스 타입과 미디어 타입 둘을 체크해서 사용여부를 결정한다. 만약 만족하지 않으면 다음 메시지 컨버터로 우선순위가 넘어간다.
ByteArrayHttpMessageConverter : byte[] 데이터를 처리한다.
클래스타입: byte[] , 미디어타입: */*
StringHttpMessageConverter : String 문자로 데이터를 처리한다.
클래스 타입: String , 미디어 타입 */*
MappingJackson2HttpMessageConverter : application/json
클래스타입: 객체 또는 HasMap, 미디어타입 application/json 관련
HTTP 요청 데이터 읽기
HTTP 요청이 오고, 컨트롤러에서 @ReqeustBody, HttpEntity 파라미터를 사용한다.
메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead()를 호출한다.
대상 클래스를 지원하는지? , HTTP 요청의 Content-Type 미디어 타입을 지원하는지 ?
canRead() 조건을 만족하면 read() 를 호출해서 객체 생성하고, 반환한다.
HTTP 응답 데이터 생성
컨트롤러에서 @ResponseBody, HttpEntity로 값이 반환된다.
메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWriter() 호출한다.
대상 클래스 타입을 지원하는지 ? , HTTP 요청의 Accept 미디어 타입을 지원하는지 ?
canWriter() 조건을 만족하면 write()를 호출해서 HTTP 응답 메시지 바디에 데이터를 생성한다.
애노테이션 기반의 컨트롤러는 다양한 파라미털르 사용할 수 있었다.
HttpServletRequest , Model 은 물론이고 , @RequestParam , @ModelAttribute 등등
이렇게 파라미터를 유연하게 처리할 수 있는 이유가 바로 ArgumentResolver 덕분이다.
애노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdaptor는 바로 이 ArgumentResolver를 호출해서
컨트롤러가 필요로 하는 다양한 파라미터의 값을 생성한다.
ReturnValueHandler
이것은 응답 값을 변환하고 처리한다.
컨트롤러에서 String으로 뷰 이름을 반환해도, 동작하는 이유가 바로 ReturnValueHandler 덕분이다.
요청의 경우 @RequestBody를 처리하는 ArgumentResolver가 있고, HttpEntity를 처리하는
ArgumentResolver가 있다. 이 ArugumentResolver들이 HTTP 메시지 컨버터를 사용해서 필요한
객체를 생성하는 것이다.
응답의 경우 @ResponseBody와 HttpEntity를 처리하는 ReturnValueHandler가 있다. 그리고 여기에서
HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다.
스프링 MVC - 웹 페이지 만들기
상품을 관리할 수 있는 간단한 서비스르 만들어 보았다.
package hello.itemservice.domain.item;
import lombok.Data;
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
package hello.itemservice.domain.item;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Repository
public class ItemRepository {
private static final Map<Long, Item> store = new HashMap<>(); //static
private static long sequence = 0L; //static
public Item save(Item item){
item.setId(++sequence);
store.put(item.getId(), item);
return item;
}
public Item findById(Long id) {
return store.get(id);
}
public List<Item> findAll() {
return new ArrayList<>(store.values());
}
public void update(Long itemId, Item updateParam) {
Item findItem = findById(itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
public void clearStore() {
store.clear();
}
}
package hello.itemservice.web.basic;
import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.annotation.PostConstruct;
import java.util.List;
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {
private final ItemRepository itemRepository;
@GetMapping
public String items(Model model) {
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "basic/items";
}
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId , Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/item";
}
@GetMapping("/add")
public String addForm() {
return "basic/addForm";
}
// @PostMapping("/add")
public String addItemV1(@RequestParam String itemName,
@RequestParam int price,
@RequestParam Integer quantity,
Model model
){
Item item = new Item();
item.setItemName(itemName);
item.setPrice(price);
item.setQuantity(quantity);
itemRepository.save(item);
model.addAttribute("item", item);
return "basic/item";
}
// @PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item,
Model model){
itemRepository.save(item);
return "basic/item";
}
// @PostMapping("/add")
public String addItemV3(@ModelAttribute Item item, Model model){
itemRepository.save(item);
return "basic/item";
}
// @PostMapping("/add")
public String addItemV4(Item item, Model model){
itemRepository.save(item);
return "basic/item";
}
// @PostMapping("/add")
public String addItemV5(Item item, Model model){
itemRepository.save(item);
return "redirect:/basic/items/"+item.getId();
}
@PostMapping("/add")
public String addItemV6(Item item, Model model, RedirectAttributes redirectAttributes){
Item saveItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId",saveItem.getId());
redirectAttributes.addAttribute("status",true);
return "redirect:/basic/items/{itemId}";
}
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId , Model model){
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/editForm";
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId , @ModelAttribute Item item){
itemRepository.update(itemId, item);
return "redirect:/basic/items/{itemId}";
}
/**
* 테스트용 데이터 추가
*/
@PostConstruct
public void init() {
itemRepository.save(new Item("itemA", 10000, 10));
itemRepository.save(new Item("itemB", 20000, 20));
}
}
@ModelAttribute
@ModelAttribute는 모델(Model)에 @ModelAttribute로 지정한 객체를 자동으로 넣어준다.
model.addAttribute("item", item)이 주석처리가 되어 있어도 잘 동작하는 것을 확인할 수 있다.
모델에 데이터를 담을 때는 이름이 필요하다. @ModelAttribute에 지정한 name 속성을 사용한다. 예시)addItemV2 메소드
@RequiredArgsConstructor
final 이 붙은 멤버변수만 사용해서 생성자를 자동으로 만들어준다.
@PostConstruct
해당 빈의 의존관계가 모두 주입되고 나면 초기화 용도로 호출된다.
간단한 테스트용 데이터를 넣기 위해 사용했다.
리다이렉트
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId , @ModelAttribute Item item){
itemRepository.update(itemId, item);
return "redirect:/basic/items/{itemId}";
}
스프링은 redirec:/... 으로 편리하게 리다이렉트를 지원한다.
컨트롤러에 매핑된 @PathVariable의 값은 redirect에도 사용 할 수 있다.
PRG Post/Redirect/GET
지금 만들어진 서비스에는 심각한 문제가 있었다.
상품 등록을 완료하고 웹 브라우저의 새로고침 버트늘 클릭하면 상품이 계속해서 중복 등록되는 것을 확인 할 수 있다.
이러한 문제는 상품 등록폼에서 데이터를 입력하고 저장을 선택하면 POST /add + 상품 데이터를 서버로 전송한다.
이 상태에서 새로 고침을 또 선택하면 마지막에 전송한 POST /add + 상품 데이터를 서버로 다시 전송하게 된다.
이 문제를 해결 하기 위해서는
웹 브라우저의 새로고침은 마지막에 서버에 전송한 데이터를 다시 전송하기 떄문에
상품 상세 화면으로 리다이렉트를 호출해주면 된다.
@PostMapping("/add")
public String addItemV5(Item item, Model model){
itemRepository.save(item);
return "redirect:/basic/items/"+item.getId();
}
하지만 redirect에서 +item+getId() 처럼 URL에 변수를 더해서 사용하는 것은 URL 인코딩이 안되기 때문에 위험하다.
RedirectAttributes
@PostMapping("/add")
public String addItemV6(Item item, Model model, RedirectAttributes redirectAttributes){
Item saveItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId",saveItem.getId());
redirectAttributes.addAttribute("status",true);
return "redirect:/basic/items/{itemId}";
}
RedirectAttributes 를 사용하면 URL 인코딩도 해주고 , pathVariable , 쿼리 파라미터까지 처리해준다.
출처
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1
'SPRING' 카테고리의 다른 글
김영한 - 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술 2일차 (0) | 2022.01.11 |
---|---|
김영한 - 스프링 MVC 2편 - 백엔드 웹 개발 핵심 기술 1일차 (0) | 2022.01.10 |
김영한 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 6일차 (0) | 2022.01.06 |
김영한 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 5일차 (0) | 2022.01.04 |
김영한 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 4일차 (0) | 2022.01.03 |