• spring
  • spring-mvc

@ModelAttribute와 PRG 패턴으로 폼 요청 처리하기

@RequestParam에서 @ModelAttribute로 폼 처리 코드를 줄이고, PRG와 RedirectAttributes로 중복 등록을 막는다.

시리즈 · Spring5 / 14
  1. SOLID 원칙과 OCP·DIP를 지키는 방법
  2. 의존성 주입(DI)과 스프링 컨테이너
  3. 쓰레드 풀과 싱글톤 서블릿의 동시성 문제
  4. FrontController 패턴과 핸들러 어댑터로 스프링 MVC 구조 이해하기
  5. @ModelAttribute와 PRG 패턴으로 폼 요청 처리하기
  6. System.out 대신 SLF4J로 로그 남기기
  7. 세션 직접 만들어 보기와 HttpSession
  8. 스프링 인터셉터와 예외 발생 시 오류 페이지 흐름
  9. 서블릿 Part와 MultipartFile로 파일 업로드 처리하기
  10. JDBC로 CRUD 구현하기와 커넥션 풀, DataSource
  11. JDBC 트랜잭션에서 스프링 트랜잭션 동기화와 트랜잭션 AOP까지
  12. JPA 변경 감지(dirty checking)와 병합(merge)
  13. 서블릿 컨테이너 초기화와 스프링 컨테이너 등록 과정
  14. 싱글톤 빈의 동시성 문제와 ThreadLocal

상품 등록 폼을 처리하는 컨트롤러를 단계적으로 개선해 본다.

@RequestParam

@PostMapping("/add")
public String saveItemV1(@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/addForm";
}

파라미터를 하나씩 @RequestParam으로 받아 객체에 직접 set 하고, model.addAttribute로 모델에 등록하는 과정까지 모두 작성해야 한다.

@ModelAttribute

@PostMapping("/add")
public String saveItemV2(@ModelAttribute("item") Item item) {
    itemRepository.save(item);
    return "basic/addForm";
}

@ModelAttribute는 두 가지 일을 해 준다.

  • 요청 파라미터를 Item 객체의 프로퍼티에 바인딩한다.
  • 그 객체를 지정한 이름("item")으로 모델에 등록한다.

이름은 생략할 수 있다. 생략하면 클래스명의 첫 글자를 소문자로 바꾼 이름(Item → item)으로 모델에 등록된다.

@PostMapping("/add")
public String saveItemV3(@ModelAttribute Item item) {
    itemRepository.save(item);
    return "basic/addForm";
}

@ModelAttribute 자체도 생략할 수 있다.

@PostMapping("/add")
public String saveItemV4(Item item) {
    itemRepository.save(item);
    return "basic/addForm";
}

PRG (Post/Redirect/Get)

POST로 상품을 등록한 뒤 뷰를 바로 반환하면, 브라우저에서 새로고침할 때 마지막 요청인 POST가 그대로 다시 전송되어 상품이 중복 등록된다.

상품 저장 후 새로고침하면 POST /add 요청이 다시 전송된다

POST 처리 후 GET 요청으로 리다이렉트하면 새로고침을 해도 GET만 다시 요청되므로 문제가 해결된다.

@PostMapping("/add")
public String addItemV5(Item item) {
    itemRepository.save(item);
    return "redirect:/basic/items/" + item.getId();
}

다만 이렇게 URL에 값을 직접 더하면 URL 인코딩이 되지 않는다. RedirectAttributes를 사용하는 편이 좋다.

@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
    Item savedItem = itemRepository.save(item);
    redirectAttributes.addAttribute("itemId", savedItem.getId());
    redirectAttributes.addAttribute("status", true);

    return "redirect:/basic/items/{itemId}";
}

URL 템플릿({itemId})에 사용된 속성은 경로에 치환되고, 사용되지 않은 나머지 속성은 쿼리 파라미터로 붙는다.

http://localhost:8080/basic/items/3?status=true

Thymeleaf에서는 ${param.status}로 이 값에 접근할 수 있다.

참고

  • 김영한, 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 (인프런)