• [인프런 김영한 로드맵4]스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(7)

    2022. 7. 3.

    by. 웰시코더

    반응형

    -스프링 MVC를 활용한 간단한 웹페이지를 만들어본다.
    -요구사항 분석 후 알맞은 도메인, HTML, 서비스 로직 등을 개발한다.

    -모든 소스는 깃허브에서 관리한다.(https://github.com/coderahn/Spring-Lecture4_Item-Service)


    7.스프링 MVC - 웹 페이지 만들기


    1.프로젝트 생성

    우선 프로젝트를 다음과 같이 생성한다.(https://start.spring.io/)


    2.요구사항 분석

    핵심 요구사항은 상품을 관리할 수 있는 서비스다.

    상품도메인 모델은 다음과 같다.

    • 상품ID, 상품명, 가격, 수량


    관련된 기능은 다음과 같다.

    • 상품목록, 상품상세, 상품등록, 상품수정


    서비스 흐름은 다음과 같다.

    • 클라이언트(사용자)는 상품 목록을 조회한다.
      • 상품 등록 클릭시, 상품 등록 폼으로 이동하여 상품 내용 입력 후 '상품 저장'을 클릭한다. 그러면 상품 상세로 돌아간다.
      • 상품 제목 클릭시, 상품 상세로 이동한다.
      • 상품 상세에서 '상품 수정' 클릭시, 상품 수정 폼으로 이동한다. 수정 내용 입력 후 '상품 수정'을 클릭하면 상품상세로 리다이렉트된다.


    그리고 다음과 같은 흐름을 가정하여 개발을 진행한다고 해보자.

    • 요구사항이 정리된 후 디자이너는 요구사항에 맞게 화면 디자인을 한다.
    • 웹퍼블리셔는 디자이너에게 받은 디자인을 활용하여 HTML, CSS 만든 후 개발자에게 제공한다.
    • 개발자는 웹퍼블리셔를 통해 HTML화면이 나오기 전, 시스템 설계와 핵심 비지니스 모델을 개발한다. HTML이 나오면 HTML을 뷰템플릿 변환 후 동적으로 화면을 그린다.


    3.상품 도메인 개발

    우선 상품 도메인을 다음과 같이 개발한다. VO 역할을 한다.

    [java/hello/itemservice/domain/item/Item.java]

    //@Data는 위험. @Getter, @Setter만 쓰는게 좋음. 핵심도메인 모델에서는 가능하면 안 사용
    //data 왔다갔다용 dto에서는 어느정도 써도 됨
    @Data
    public class Item {
    
        private Long id;
        private String itemName;
        private Integer price; //price가 없을 수도 있으니 Integer
        private Integer quantity; //수량이 없는 경우 있을 수도 있으니 Integer
    
        public Item() {
        }
    
        public Item(String itemName, Integer price, Integer quantity) {
            this.itemName = itemName;
            this.price = price;
            this.quantity = quantity;
        }
    }


    다음으로 상품 저장소 역할의 서비스를 개발한다. 데이터베이스 역할은 HashMap을 사용한다. 서버가 올라간 후 한 번 생성되어 사용하기 위해 static변수로 선언한다.


    [src/main/java/hello/itemservice/domain/item/ItemRepository.java]

    @Repository
    public class ItemRepository {
        //스프링 컨테이너 안에서 쓰면 싱글톤이라 static을 안 써도 되긴 함.
        //따로 ItemRepository를 new로 사용하면 static 필수
        //멀티스레드 환경에서 여러개가 동시에 store 접근시 HashMap이 아닌 ConcurrentHashMap 사용
    
        //HashMap은 멀티쓰레드 환경에서 사용하면 안됨.
        private static final Map<Long, Item> store = new HashMap<>();
    
        //멀티스레드 환경에서 long이 아닌 AtomicLong 사용
        private static long sequance = 0L;
    
        //아이템 저장
        public Item save(Item item) {
            item.setId(++sequance);
            store.put(item.getId(), item);
            return item;
        }
    
        public Item findById(Long id) {
            return store.get(id);
        }
    
        public List<Item> findAll() {
            //store.values()를 그대로 반환해도되지만, ArrayList를 감싸서 add작업을 해도 store에 영향 없도록 하기 위해 감쌈
            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();
        }
    }


    위의 코드에서 다음 코드가 DB역할을 한다.

        private static final Map<Long, Item> store = new HashMap<>();


    이 코드가 static으로 되어 있는데, 메모리에 한 번 올라간 후 서버가 내려갈 때까지 유지되야 되기 때문이다. 근데 실제로 스프링컨테이너는 ItemRepository를 싱글톤 기반 스프링 빈으로 등록하는데, 싱글톤이기 때문에 실제로 static을 붙이지 않아도 되긴 한다.

    다음과 같이 서비스들에 대한 테스트코드를 작성할 수 있다.

    [src/test/java/hello/itemservice/domain/item/ItemRepositoryTest.java]

    class ItemRepositoryTest {
        ItemRepository itemRepository = new ItemRepository();
    
        @AfterEach
        void afterEach() {
            itemRepository.clearStore();
        }
    
        @Test
        void save() {
            //given
            Item item = new Item("itemA", 10000, 10);
    
            //when
            Item savedItem = itemRepository.save(item);
    
            //then
            Item findItem = itemRepository.findById(item.getId());
            assertThat(findItem).isEqualTo(savedItem);
        }
    
        @Test
        void findAll() {
            //given
            Item item1 = new Item("item1", 10000, 10);
            Item item2 = new Item("item2", 20000, 20);
    
            itemRepository.save(item1);
            itemRepository.save(item2);
    
            //when
            List<Item> result = itemRepository.findAll();
    
            //then
            assertThat(result.size()).isEqualTo(2);
            assertThat(result).contains(item1, item2);
        }
    
        @Test
        void updateItem() {
            //given
            Item item = new Item("item1", 10000, 10);
            Item savedItem = itemRepository.save(item);
            Long itemId = savedItem.getId();
    
            //when
            Item updateParam = new Item("item2", 20000, 30);
            itemRepository.update(itemId, updateParam);
            Item findItem = itemRepository.findById(itemId);
    
            //then
            assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
            assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
            assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
        }
    }


    4.상품서비스 HTML

    이제 위에서 서비스 로직을 개발하였으니, HTML 화면을 개발한다.

    여기서는 부트스트랩을 사용한다.
    부트스트랩은 공식 사이트에서 다운로드 받은 후, bootstrap.min.css를 복사하여 resources/static/css에 추가한다.

    HTML과 CSS파일은 다음과 같이 위치한다.

    • resources/static/css/bootstrap.min.css //부트스트랩 css파일
    • resources/static/html/items.html //상품 목록
    • resources/static/html/item.html //상품 상세
    • resources/static/html/addForm.html //상품 등록
    • resources/static//html/editForm.html //상품 수정


    resources/static에 넣어두었기 때문에 스프링부트는 정적리소스를 제공하게 된다. 정적리소스기 때문에 다음과 같이 접근가능하다.

    http://localhost:8080/html/items.html

    5.상품목록 - 타임리프

    이제 컨트롤러와 뷰템플릿을 개발한다.

    [BasicItemController.java]

    @Controller
    @RequestMapping("/basic/items")
    @RequiredArgsConstructor
    public class BasicItemController {
        private final ItemRepository itemRepository;
    
        //생성자가 1개만 있으면 Autowired 생략 가능
        //@RequiredArgsConstructor를 사용하면 final 필드가 있는 itemRepository를 자동 생성
    //    public BasicItemController(ItemRepository itemRepository) {
    //        this.itemRepository = itemRepository;
    //    }
    
        @GetMapping
        public String items(Model model) {
            List<Item> items = itemRepository.findAll();
            model.addAttribute("items", items);
            return "basic/items";
        }
    
        //테스트용 데이터 추가
        @PostConstruct
        public void init() {
            itemRepository.save(new Item("itemA", 10000, 10));
            itemRepository.save(new Item("itemB", 20000, 20));
    
        }
    }


    @RequiredArgsConstructor를 붙이면 final 멤버변수의 생성자를 자동으로 만들어준다.

    @GetMapping된 items(Model model)은 호출된 경우 매핑되어 실행되는 컨트롤러 메소드다. findAll()을 통해 저장된 item들을 모두가져와서 model에 담은 후 basic/items.html로 랜더링한다.

    @PostConstruct는 해당 빈 의존관계가 모두 주입되고 초기화 용도로 호출된다. 테스트데이터를 셋팅하기 위해 사용한다.

    다음으로 /resources/static/items.html을 /resources/templates/basic/items.html로 복사한 후 다음과 같이 수정한다.

    [/resources/templates/basic/items.html]

    <!DOCTYPE HTML>
    <html xmlns:th="http://www.thymeleaf.org"> <!-- 타임리프 선언 -->
    <head>
      <meta charset="utf-8">
      <link th:href="@{/css/bootstrap.min.css}"
              href="../css/bootstrap.min.css" rel="stylesheet">
    </head>
    <body>
    <div class="container" style="max-width: 600px">
      <div class="py-5 text-center">
        <h2>상품 목록</h2>
      </div>
    
      <div class="row">
        <div class="col">
          <button class="btn btn-primary float-end"
                  onclick="location.href='addForm.html'"
                  th:onclick="|location.href='@{/basic/items/add}'|"
                  type="button">상품 등록</button>
        </div>
      </div>
    
      <hr class="my-4">
      <div>
        <table class="table">
          <thead>
          <tr>
            <th>ID</th>
            <th>상품명</th>
            <th>가격</th>
            <th>수량</th>
          </tr>
          </thead>
          <tbody>
          <tr th:each="item : ${items}">
            <td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원아이디</a></td>
            <td><a href="item.html" th:href="@{|/basic/items/${item.id}|}" th:text="${item.itemName}">상품명</a></td>
            <td th:text="${item.price}">10000</td>
            <td th:text="${item.quantity}">10</td>
          </tr>
          </tbody>
        </table>
      </div>
    </div> <!-- /container -->
    </body>
    </html>


    th는 타임리프 속성을 사용할 때 사용하는 키워드다. 타임리프의 뷰템플릿을 거치면 완래값을 th:xxx로 변경한다. th:xxx가 붙은 부분은 서버사이드에서 랜더링되어 기존 것을 대체한다. html을 파일로 열었을 때 th:xxx가 있어도 웹브라우저는 th:속성을 몰라서 무시한다. 따라서 html을 파일보기로 유지하면서 템플릿 기능도 할 수 있다.

    th:href는 속성을 변경한다. href="value1"을 th:href="value2"로 변경한다.

    @{...}는 타임리프의 URL링크 표현식이다. @{...}를 사용하면 서블릿컨텍스트를 자동으로 포함한다.

    참고로 서블릿 컨텍스트는 서블릿 컨테이너(Tomcat 등) 실행시 각 컨텍스트(웹어플리케이션)마다 한 개의 서블릿컨텍스트 객체를 생성하는데, 웹어플리케이션이 실행되면서 애플리케이션 전체의 자원이나 정보를 미리 바인딩해서 서블릿들이 공유하여 사용하는 객체다.(https://java117.tistory.com/18)

    <button>상품등록</button>을 클릭하면 th:onclick 속성이 실행 된다.

    th:onclick="|location.href='@{/basic/items/add}'|" 부분에 리터럴 대체 문법이 사용되었다. |...| 이렇게 사용한다.

    • 리터럴 대체 문법을 사용 안 할 경우 문자와 표현식을 더하기(+)로 사용해야 한다.
      • ex) <span th:text="'Welcome, ' + ${user.name} + '!'">
    • 리터럴 대체 문법 사용시 더하기 없이 사용 가능하다.
      • ex)<span th:text="|Welcome, ${user.name}!|">


    th:each는 반복출력으로 jsp의 c:foreach같은 역할이다.

    th:text는 내용 값을 th:text로 변경한다. <td th:text="${item.price}">10000</td>인 경우 10000이 ${item.price}로 대체된다.

    위의 HTML에 다음과 같은 코드가있다.

    <td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원아이디</a></td>


    여기서 th:href의 링크표현식에 (itemId=${item.id})라고 되어 있는 부분을 볼 수 있다. {itemId}라는 url 경로 부분에 ${item.id}를 넣어서 동적으로 URL을 만들 수 있다. 또한 (itemId=${item.id}, query'test')처럼 쿼리파라미터 추가도 가능하다. 위의 경우 url은 http://localhost:8080/basic/items/1?query=test같은 방식으로 URL이 생성된다.

    6.상품 상세

    상품 상세의 컨트롤러 매핑은 위의 BasicItemController에 다음과 같이 추가한다.

    [BasicItemController.java]

    //...
    
    @GetMapping("/{itemId}")
    public String item(@PathVariable long itemId, Model model) {
        Item item = itemRepository.findById(itemId);
        model.addAttribute("item", item);
    
        return "basic/item";
    }
    
    //...



    @PathVariable을 이용한다. 넘어온 상품ID로 findById(itemId)를 통해 상품 조회한다. 뷰는 다음과 같이 만든다.

    [/resources/templates/basic/item.html]

    <!DOCTYPE HTML>
    <html xmlns:th="http://www.thymeleaf.org">
    <head>
      <meta charset="utf-8">
      <link th:href="@{/css/bootstrap.min.css}"
            href="../css/bootstrap.min.css" rel="stylesheet">
      <style>
        .container {
          max-width: 560px;
        }
      </style>
    </head>
    <body>
    <div class="container">
      <div class="py-5 text-center">
        <h2>상품 상세</h2>
      </div>
      <div>
        <label for="itemId">상품 ID</label>
        <input type="text" id="itemId" name="itemId" class="form-control" value="1" th:value="${item.id}" readonly>
      </div>
      <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" name="itemName" class="form-control" value="상품A" th:value="${item.itemName}" readonly>
      </div>
      <div>
        <label for="price">가격</label>
        <input type="text" id="price" name="price" class="form-control" value="10000" th:value="${item.price}" readonly>
      </div>
      <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" name="quantity" class="form-control" value="10" th:value="${item.quantity}" readonly>
      </div>
    
      <hr class="my-4">
      <div class="row">
        <div class="col">
          <button class="w-100 btn btn-primary btn-lg"
                  onclick="location.href='editForm.html'"
                  th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"
                  type="button">상품 수정</button>
        </div>
        <div class="col">
          <button class="w-100 btn btn-secondary btn-lg"
                  onclick="location.href='items.html'"
                  th:onclick="|location.href='@{/basic/items}'|"
                  type="button">목록으로</button>
        </div>
      </div>
    </div> <!-- /container -->
    </body>
    </html>


    th:value는 모델에 있는 정보를 프로퍼티 접근법으로 출력한다. th:value="${item.id}"의 경우 item.id가 출력된다.

    7.상품등록 폼

    상품등록폼으로 이동하기 위한 컨트롤러 메소드를 만든다.

    [BasicItemController.java]

    //...
    
    @GetMapping("/add")
    public String addForm() {
        return "basic/addForm";
    }
    
    //...


    단순히 addForm.html로 이동하는 역할만 한다.

    등록 폼은 다음과 같이 만든다.

    [/resources/templates/basic/addForm.html]

    <!DOCTYPE HTML>
    <html>
    <head>
      <meta charset="utf-8">
      <link th:href="@{/css/bootstrap.min.css}"
            href="../css/bootstrap.min.css" rel="stylesheet">
      <style>
        .container {
          max-width: 560px;
        }
      </style>
    </head>
    <body>
    <div class="container">
      <div class="py-5 text-center">
        <h2>상품 등록 폼</h2>
      </div>
      <h4 class="mb-3">상품 입력</h4>
      <form action="item.html" th:action method="post"> <!-- 같은 URL은 action을 비워도됨("/basic/items/add")-->
        <div>
          <label for="itemName">상품명</label>
          <input type="text" id="itemName" name="itemName" class="form-control" placeholder="이름을 입력하세요">
        </div>
        <div>
          <label for="price">가격</label>
          <input type="text" id="price" name="price" class="form-control"
                 placeholder="가격을 입력하세요">
        </div>
        <div>
          <label for="quantity">수량</label>
          <input type="text" id="quantity" name="quantity" class="form-control" placeholder="수량을 입력하세요">
        </div>
        <hr class="my-4">
        <div class="row">
          <div class="col">
            <button class="w-100 btn btn-primary btn-lg" type="submit">상품
              등록</button>
          </div>
          <div class="col">
            <button class="w-100 btn btn-secondary btn-lg"
                    onclick="location.href='items.html'" type="button">취소</button>
          </div>
        </div>
      </form>
    </div> <!-- /container -->
    </body>
    </html>


    th:action을 사용하여 현재 URL에 데이터를 전송한다.

    8.상품 등록 처리 - @ModelAttribute

    상품등록 폼에서 전달된 데이터를 서버에서 처리해보자.

    • POST방식의 HTML Form 방식으로 전달
    • content-type은 application/x-www-form-urlencoded
    • 메세지 바디에 쿼리파라미터 형식 전달
      • itemName=itemA&price=20000&quantity=20


    요청 파라미터들을 처리하는 방식은 앞서서 학습했듯이 여러가지 방법으로 처리 가능하다. 우선 @RequestParam으로 처리해본다.

    [BasicItemController.java]

    //...
    
    //@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";
    }
        
    //...


    요청 파라미터들을 각각 @RequestParam으로 받아서 Item 객체 setter를 통해 셋팅 후 save(item)으로 저장한다.

    이 방법은 변수를 각각 받아 불편하다. @ModelAttribute를 통해 한 번에 처리해보자.

    [BasicItemController.java]

    //...
    
    //ModelAttribute(네임)의 '네임'은 model.addAttribute 역할을 해준다. view에서 보여줄 attributeName이라고 볼 수 있다.
    @PostMapping("/add")
    public String addItemV2(@ModelAttribute("item") Item item, Model model) {
        itemRepository.save(item);
        //model.addAttribute("item", item); //ModelAttribute("item")으로 인해 생략 가능
    
        return "basic/item";
    }
    
    //...


    @ModelAttribute가 Item객체 생성 후 setter를 통해 입력까지 해준다.

    여기서 또 중요한 기능이 있다. @ModelAttribute가 model.setAttribute(name, value) 역할도 해준다. model에 데이터를 담을 때 name이 필요하듯이, @ModelAttribute(name)의 name값이 model의 name역할을 해준다.

    예를 들어 @ModelAttribute("name") Item item은 model.setAttribute("name", item)과 같다.

    여기서 더 나아가면 ModelAttribute의 name을 생략할 수 있다.

    [BasicItemController.java]

    //...
    
    //ModelAttribute(네임)의 '네임'생략시 클래스네임 앞을 소문자로 바꿔서 model.addAttribute(네임,value)가 된다.
    //ex)ModelAttribute Item item은 model.addAttribute("item", value)와 같다.
    //@PostMapping("/add")
    public String addItemV3(@ModelAttribute Item item) {
        itemRepository.save(item);
        
        return "basic/item";
    }
    
    //...


    더 더 나아가면, ModelAttribute까지 생략된다. 대상 객체는 모델에 자동 등록된다.

    [BasicItemController.java]

     //ModelAttribute 생략 가능(String등 단순타입인 경우 생략하면 @RequestParam)
    @PostMapping("/add")
    public String addItemV4(Item item) {
        itemRepository.save(item);
    
        return "basic/item";
    }


    9.상품 수정

    상품 상세에서 상품 수정 버튼을 클릭하면 수정 화면으로 이동한다. 이 때 해당 item의 id를 컨트롤러에서 pathVariable로 사용하여 해당 데이터를 조회 후 model에 담아줘야 한다.

     

    컨트롤러 매핑은 다음과 같이 만든다.

     

    [BasicItemController.java]

     //상품수정폼
    @GetMapping("/{itemId}/edit")
    public String editForm(@PathVariable Long itemId, Model model) {
        Item item = itemRepository.findById(itemId);
        model.addAttribute("item", item);
        return "basic/editForm";
    }

     

    수정화면은 다음과 같다.(등록 폼과 비슷하다)

     

    [/resources/templates/basic/editForm.html]

    <!DOCTYPE HTML>
    <html xmlns:th="http://www.thymeleaf.org">
    <head>
      <meta charset="utf-8">
      <link th:href="@{/css/bootstrap.min.css}"
            href="../css/bootstrap.min.css" rel="stylesheet">
      <style>
        .container {
          max-width: 560px;
        }
      </style>
    </head>
    <body>
    <div class="container">
      <div class="py-5 text-center">
        <h2>상품 수정 폼</h2>
      </div>
      <form action="item.html" th:action method="post"> <!-- HTML Form 전송은 PUT, PATCH 지원 안 함-->
        <div>
          <label for="id">상품 ID</label>
          <input type="text" id="id" name="id" class="form-control" value="1" th:value="${item.id}" readonly>
        </div>
        <div>
          <label for="itemName">상품명</label>
          <input type="text" id="itemName" name="itemName" class="form-control" value="상품A" th:value="${item.itemName}">
        </div>
        <div>
          <label for="price">가격</label>
          <input type="text" id="price" name="price" class="form-control" value="10000" th:value="${item.price}">
        </div>
        <div>
          <label for="quantity">수량</label>
          <input type="text" id="quantity" name="quantity" class="form-control" value="10" th:value="${item.quantity}">
        </div>
        <hr class="my-4">
        <div class="row">
          <div class="col">
            <button class="w-100 btn btn-primary btn-lg" type="submit">저장
            </button>
          </div>
          <div class="col">
            <button class="w-100 btn btn-secondary btn-lg"
                    onclick="location.href='item.html'"
                    th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|"
                    type="button">취소</button>
          </div>
        </div>
      </form>
    </div> <!-- /container -->
    </body>
    </html>

     

    이제 상품 수정 화면에서 '저장'을 클릭하면 수정 로직이 작동한다. 컨트롤러 매핑은 아래와 같다.

     

    [BasicItemController.java]

    //상품수정
    @PostMapping("/{itemId}/edit")
    public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
        itemRepository.update(itemId, item);
    
        //뷰템플릿 호출 대신 상품상세로 리다이렉트(302)
        //@PathVariable값 사용 가능
        return "redirect:/basic/items/{itemId}";
    }

     

    업데이트 후 수정된 내용을 상품 상세에서 보여주기 위해 redirect한다.

     

    10.PRG Post/Redirect/Get

     

    생각해보면 상품 등록에서 상품 저장(/add) 후 redirect 처리를 안 했다. 이러면 새로고침할 때 POST방식의 /add가 계속 실행되기 때문에 동일한 데이터가 중복등록된다. 저장 후 상품 수정과 같이 redirect를 해주면 마지막 요청이 GET이 되기 때문에 새로고침을 해도 상품 상세가 호출되어 이런 문제를 방지할 수 있다.

     

    저장(POST) -> 리다이렉트(Redirect) -> 상세(Get)의 앞 글자를 따서 PRG 패턴이라고 한다.

     

    [BasicItemController.java]

     //PRG 패턴 적용(저장로직 중복 방지)
    @PostMapping("/add")
    public String addItemV5(Item item) {
        itemRepository.save(item);
        return "redirect:/basic/items/" + item.getId();
    }

     

    11.RedirectAttributes

     

    상품 저장 후 상세화면으로 이동하면 저장이 잘 된 것인지 판단이 안 선다. 화면에 저장여부를 텍스트로 표기해주려고 한다. 이 때 RedirectAttributes를 사용하면 리다이렉트 후 model 처리를 해준다.

     

    //리다이렉트 후 model 처리할 수 있는 방버
    @PostMapping("/add")
    public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
        Item savedItem = itemRepository.save(item);
    
        //{itemId}에 itemId가 들어간다. status는 쿼리파라미터 형태로 들어간다.
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
    
        return "redirect:/basic/items/{itemId}";
    }

     

    위에서 "status"는 뷰템플릿에서 텍스트 출력을 위한 플래그 역할을 한다. 실행 시 다음과 같은 리다이렉트 결과가 나온다.

     

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

     

     

     

     

    위의 리다이렉트 결과에서 보듯이 RedirectAttributes는 URL 인코딩과 pathVariable, 쿼리 파라미터까지 처리해준다.

     

    [/resources/templates/basic/item.html]

    <div class="container">
      <div class="py-5 text-center">
        <h2>상품 상세</h2>
      </div>
    
      <!-- param은 쿼리파라미터 쓸 수 있는 객체 -->
      <h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>
      
      <!-- 생략... -->
    </div>

     

    th:if 타임리프 문법을 사용하여 분기처리를 해준다. 여기서 param은 타임리프에서 자체적으로 제공해주는데 쿼리파라미터를 편리하게 조회하게 해준다.

     

    결과화면은 아래와 같다. (상품 저장 후) 

     

     

     

     

     

     

    반응형

    댓글