본문 바로가기

SPRING

김영한 - 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 7일차

참고자료

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