본문 바로가기
개발자 일지/Spring

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

by 네빌링 2022. 7. 3.
반응형

-스프링 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은 타임리프에서 자체적으로 제공해주는데 쿼리파라미터를 편리하게 조회하게 해준다.

 

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

 

 

 

 

 

 

반응형