Spring MVC : 간단한 웹 페이지 구현

    인프런 김영한님의 강의를 듣고 정리한 내용입니다. 

     

     

    전체 서비스 제공


    전체 서비스 제공 흐름은 위와 같이 구성이 되었다.

    • 검정색 : 컨트롤러
    • 하얀색 : 뷰템플릿 

    각 박스는 색깔별로 위와 같은 형태로 구성되어있다. 모든 페이지를 MVC 패턴으로 구현한다. 컨트롤러가 필요없어도 컨트롤러로 먼저 들어온 후, 뷰 타입으로 반환하는 형태다. 

     

    웰컴 페이지 생성


    프로젝트를 생성한 후, 먼저 필요한 설정을 해주었다. Annotation Processor 및 Gradle 설정을 완료했다. 이후, 서버가 정상적으로 동작하는지 띄워보았다. 화이트 페이지가 나오며 정상동작하는 것이 확인되었다. 이후에는 편의를 위해 WelCome Page를 만들었다.

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    <ul>
        <li>상품 관리
            <ul>
                <li><a href="/basic/items">상품 관리 - 기본</a></li>
            </ul>
        </li>
    </ul>
    </body>
    </html>

    WelCome Page는 Static의 index.html을 만들어두면, 자동으로 localhost:8080으로 접속했을 때 만들어진다. index.html의 코드는 위의 코드를 사용했다. 위의 페이지를 만든 후 서버 재가동을 해서 웰컴 페이지로 정상접속되는지를 확인했다. 

     

    필요 클래스 생성 및 테스트 (상품 클래스, 상품 저장소)


    상품 클래스 생성

    상품 클래스는 총 4개의 속성을 가진다. 

    • ID
    • 상품명
    • 상품 가격
    • 상품 갯수

    위의 속성을 만족하는 상품 클래스를 생성한다.

    @Getter @Setter
    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;
        }
    
    }
    • Price와 Quantity는 Integer로 설정했다. Int로 설정하면 null이 들어갈 수 있으나, Integer로 설정하면 null이 들어갈 수 없기 때문이다.
    • @Getter, @Setter만 사용했다. @Data를 사용하면 여러가지 편의 메서드들이 생성되지만, 이는 예상치 못하게 동작할 수 있다. 따라서 필요한 @Getter @Setter만 사용했다. 

     

    상품 저장 클래스 생성

    @Repository
    public class ItemRepository {
        private static final Map<Long, Item> store = new HashMap<>();
        private static long sequence = 0L;
    
        public Item save(Item item){
            item.setId(++sequence);
            store.put(item.getId(), item);
            return item;
        }
    
        public Item findById(Long itemId){
            return store.get(itemId);
        }
    
        public List<Item> findAll(){
            return new ArrayList<>(store.values());
        }
    }
    

     

    상품 저장 클래스는 위의 코드로 구현했다.

    • 이 때 HashMap과 Long을 쓴 것은 멀티 쓰레드를 고려하지 않은 것이다. 멀티 쓰레드 환경에서는 HashMap과 Long에 동시에 접근 시 꼬이는 문제가 있다. 따라서 실제로는 ConcurrentHashMap, AtomicLong을 써야한다. 
    • @Repository 어노테이션을 추가한다. @Component가 되어 @ComponentScan의 대상이 된다. 이후 컨트롤러에 DI에 사용된다. 해당 저장소는 싱글톤으로 관리되어야 하기 때문이다. 

     

    1. 통합 컨트롤러 뼈대 구성


    @RequestMapping("/basic/items")
    @Controller
    @RequiredArgsConstructor
    public class BasicItemController {
    
    
        //@RequiredArgrsConstructor를 ItemRepository 의존주입.
        private final ItemRepository itemRepository;
    
     @PostConstruct
        public void init(){
            Item itemA = new Item("itemA", 10000, 10);
            Item itemB = new Item("itemB", 20000, 20);
    
            itemRepository.save(itemA);
            itemRepository.save(itemB);
        }

    통합 컨트롤러의 뼈대를 먼저 구성한다. 

    • URL Pattern은 "/basic/items"로 이루어져 있다.
    • @Controller 어노테이션을 달아주어 Dispatcher Servlet으로 설정해준다.
    • @RequiredArgsConstructor를 달아주어 private final로 선언된 itemRepository에 DI를 실행해준다.
    • @PostConstructor 메서드를 만들어, 생성자 실행 후 실행되는 것을 만들어준다. 

     

     

    2. 상품목록 컨트롤러 구현 


    @GetMapping
    public String items(Model model){
        List<Item> items = itemRepository.findAll();
        model.addAttribute("items", items);
    
        return "basic/items";
    }
    
    • 상품목록 컨트롤러의 URL은 /basic/items다. 클래스 레벨의 URL과 동일하기 때문에 추가 URL은 필요없다.
    • itemRepository에서 저장된 모든 Item을 가지고 온다. 그리고 그 items를 Model에 담아서 전달해준다.
    • String을 Return하고 @ReqeustBody, @ResponseBody가 아니기 때문에 return 값은 뷰템플릿의 논리이름이 된다.

    이후 해야할 일은 뷰템플릿을 구현하고, Model에 담긴 값을 동적으로 표현해주는 것이다. 

     

    3. 상품목록 뷰템플릿 구현


    <!DOCTYPE HTML>
    <html>
    <head>
        <meta charset="utf-8">
        <link 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'" 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>
                    <td><a href="item.html">1</a></td>
                    <td><a href="item.html">테스트 상품1</a></td>
                    <td>10000</td>
                    <td>10</td>
                </tr>
                <tr>
                    <td><a href="item.html">2</a></td>
                    <td><a href="item.html">테스트 상품2</a></td>
                    <td>20000</td>
                    <td>20</td>
                </tr>
                </tbody>
            </table>
        </div>
    </div> <!-- /container -->
    </body>
    </html>

    먼저 상품 목록 HTML의 정적 페이지가 구현된 코드를 가지고 왔다. 여기서 Thymeleaf 문법을 활용해서 우리가 필요한 것들을 동적으로 사용할 수 있도록 해줘야한다.

    가장 먼저 위의 HTML을 resources/templates/basic 폴더에 옮겨준다. 

    <html xmlns:th="http://www.thymeleaf.org">

    그리고 먼저 templates의 items.html 파일에 위의 코드를 명시해준다. 이 코드를 명시하면, 여기서부터는 Thymeleaf를 사용하겠다는 의미다. 

    <link th:href="@{/css/bootstrap.min.css}"
            href="../css/bootstrap.min.css" rel="stylesheet">

    위의 코드를 활용해서 CSS를 절대경로로 읽도록 한다. 서버를 새로 돌린 후, 정상적으로 돌아가는지를 확인한다. 

    정상 실행 시 위와 같은 화면이 나온다. 상품명이 @PostConstructor로 들어가는 것이 아닌 것을 알았다. 정적으로 되어있는 것이 확인되었으니 이 부분을 동적으로 고쳐주어야 한다. Items 컨트롤러에서 Items를 Model에 담아서 View에 넘겨주었다. 따라서 View에서 Items를 가지고 와서 반복문으로 출력을 해주면 된다. 

    <tr th:each="item : ${items}">
        <td><a href="item.html" th:text="${item.id}">상품 ID</a></td>
        <td><a href="item.html" th:text="${item.itemName}">상품명</a></td>
        <td th:text="${item.price}">상품가격</td>
        <td th:text="${item.quantity}">상품갯수</td>
    </tr>

    위의 코드를 작성해준다.

    • th:each는 타임리프에서 제공하는 loop문이다.
    • ${...}을 사용하면 뷰 템플릿으로 넘겨진 모델에서 이름을 Key로 하는 Value를 가지고 온다.
    • 자바의 Advanced For문과 마찬가지로 for(item : items) 형태로 사용할 수 있다.
    • th:text는 텍스트를 출력하는 명령어다.

     

    위 코드 적용 후, 스프링을 다시 시작해보면 위와 같은 화면이 나오게 된다. 그리고 ID, 상품명에 태그가 걸려 있는 것을 고쳐주어야 한다. 현재 상태에서는 item.html로 이동하도록 a href 태그가 걸려있는데, 이것을 동적으로 변경해주어야한다.

    <td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}"  th:text="${item.id}">상품 ID</a></td>
    <td><a href="item.html" th:href="@{|/basic/items/${item.id}|}" th:text="${item.itemName}">상품명</a></td>

    경로는 위의 두 가지로 바꿀 수 있다. 둘다 결과는 똑같은데 표현방식이 다른 것이다. @PostConstructor를 사용해서 넣어둔 기초 데이터가 나오도록 바뀌었고, HTML 태그가 수정되었다. 이제 상품등록 버튼을 눌렀을 때, 상품등록 컨트롤러로 이동하도록 연결을 해주어야 한다.

    th:onclick="|location.href='@{/basic/items/add}'|"
    onclick="location.href='addForm.html'" type="button">상품

    th 명령어를 사용해서 onclick도 Thymeleaf 용으로 구현한다. 이 버튼을 클릭하게 되면 /basic/add URL로 태그되도록 구현했다. 

    실행 결과를 확인하면, 에러 페이지가 뜨지만 URL은 정상적으로 내부 연결이 된 것을 확인했다. 다음은 회원등록 컨트롤러를 구현해야할 차례다. 

     

    4. 상품상세 컨트롤러 구현 


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

    상품상세 컨트롤러는 위와 같이 구현했다.

    • Get으로 넘어오는 데이터는 itemId 밖에 없다. 따라서 @ModelAttribute로 Item을 불러와도 Item 객체가 없다. 그렇기 때문에 itemId로 itemRepository에 있는 item을 찾은 후, Model에 넘겨줘야한다.
    • @PathVariable 어노테이션을 활용했다. {...}에 @Pathvariable이 붙은 변수를 넣어주는 형식으로 사용이 된다. @ModelAttribute, @ReqeustParam과 유사하게 요청 파라미터에서 동일한 이름을 가진 변수를 찾아서 넣어준다.

     

    5. 상품상세 뷰 템플릿 구현


    <!DOCTYPE HTML>
    <html>
    <head>
        <meta charset="utf-8">
        <link 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" readonly>
        </div>
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" name="itemName" class="form-control"
                   value="상품A" readonly>
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" id="price" name="price" class="form-control"
                   value="10000" readonly>
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" id="quantity" name="quantity" class="form-control"
                   value="10" 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'" type="button">상품 수정</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        onclick="location.href='items.html'" type="button">목록으로</button>
            </div>
        </div>
    </div> <!-- /container -->
    </body>
    </html>

    기본적인 HTML틀은 위와 같다. 해야할 일을 살펴보자.

    • 상품 ID, 상품명, 가격, 수량의 동적 표현 필요
    • 상품수정 클릭 시 경로 수정 필요
    • 목록 클릭시 경로 수정 필요
    <div>
        <label for="itemId">상품 ID</label>
        <input type="text" id="itemId" name="itemId" class="form-control"
               th:value="${item.id}"
               value="1" readonly>
    </div>
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" name="itemName" class="form-control"
               th:value="${item.itemName}"
               value="상품A" readonly>
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" name="price" class="form-control"
               th:value="${item.price}"
               value="10000" readonly>
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" name="quantity" class="form-control"
               th:value="${item.quantity}"
               value="10" readonly>
    </div>

    위의 박스에 필요한 값이 동적으로 표현될 수 있도록 th:value문을 사용해서 동적으로 값이 표현될 수 있도록 했다. item.id, item.itemName, item.price, item.quantity를 동적으로 사용해서 정상적으로 출력될 수 있도록 구현했다.

    <button class="w-100 btn btn-primary btn-lg"
            th:onclick="|location.href='@{/basic/items/edit}'|"
            onclick="location.href='editForm.html'" type="button">상품 수정</button>

    상품 수정을 눌렀을 때, edit 페이지로 넘어갈 수 있도록 th:onclick을 구성했다. 이 때, |...|를 사용해서 경로가 틀어지지 않도록 했고, @{...}으로 th문의 경로를 표현해주었다.

    <div class="col">
        <button class="w-100 btn btn-secondary btn-lg"
                th:onclick="|location.href='@{/basic/items}'|"
                onclick="location.href='items.html'" type="button">목록으로</button>
    </div>

    목록으로 돌아가기 위해 경로 수정을 해주었다. 위와 동일하게 '/basic/items'가 불려질 수 있도록 GET Controller를 호출했고, 이후에는 해당 Controller에서 상품 상세 페이지 뷰 템플릿으로 이동하게 해두었다.

     

     

     

    6. 상품등록 폼 컨트롤러 구현 


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

    상품등록 폼 컨트롤러는 위와 같이 구현했다.

    • 상품등록 폼 컨트롤러는 HTML FORM을 요청해서 받는 용도다. 따라서 Get으로 요청한다.
    • 뷰 템플릿은 basic/addForm으로 연결한다.

     

    5. 상품등록폭 뷰 템플릿 동적구현

    <!DOCTYPE HTML>
    <html>
    <head>
        <meta charset="utf-8">
        <link 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" method="post">
            <div>
                <label for="itemName">상품명</label>
                <input type="text" id="itemName" name="itemName" class="formcontrol" 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="formcontrol" 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>

    HTML은 위와 같다. 먼저 어떤 상태인지 확인하기 위해서 해당 HTML을 절대경로로 실행해보면 다음과 같은 것을 확인할 수 있다. 

    해야할 것을 살펴보자.

    1. 상품등록 경로
    2. 취소 경로
    <div class="col">
        <button class="w-100 btn btn-secondary btn-lg"
                th:onclick="|location.href='@{/basic/items}'|"
                onclick="location.href='items.html'" type="button">취소</button>
    </div>

    먼저 위의 th 코드를 이용해서 취소 경로를 설정했다. 취소 경로를 할 경우 상품목록으로 돌아갈 수 있도록 경로를 설정해주었다. 서버를 띄운 후 정상적으로 동작하는지 확인했다. 

    <form th:action="@{/basic/items/add}"
            action="item.html" method="post">
            
    <form th:action
    		action="item.html" method="post">

    이후에는 th로 POST경로를 수정했다. 위 코드, 아래 코드 모두 가능하다. th:action이 빌 경우 현재 경로 그대로를 인식하고 POST를 한다고 한다. 따라서 편한 것으로 설정하면 addForm에서 구현해야 하는 것은 다 구현했다.

    addForm.html에서는 HTML FORM 형식으로 데이터를 입력받는다. 그리고 그 데이터를 쿼리 파라미터 형식으로 서버에 POST하게 된다. 따라서 Post Mapping할 수 있는 컨트롤러의 작성이 필요하다.

     

    7. 상품저장 컨트롤러 구현


        @PostMapping("/add")
        public String addV1(@RequestParam String itemName,
                            @RequestParam int price,
                            @RequestParam Integer quantity,
                            Model model) {
    
            Item item = new Item(itemName, price, quantity);
            Item savedItem = itemRepository.save(item);
            model.addAttribute("item", item);
            return "basic/item";
        }

    상품저장 컨트롤러 프로토타입이다.

    • HTML FORM은 요청 파라미터 형식(key=value)으로 전달되기 때문에 필요한 데이터 값을 @RequestParam으로 가지고 온다. 
    • 뷰 템플릿으로 랜더링 할 계획이기 때문에 Model에 값을 넣어준다.

    위와 같이 할 경우, 번거롭게 여러 매개변수를 가져와 하나씩 넣어야 한다. 따라서 좀 더 간단하게 구현해본다.

    public String addV2(@ModelAttribute("item") Item item, Model model) {
    
        Item savedItem = itemRepository.save(item);
        model.addAttribute("item", item);
        return "basic/item";
    }

    두번째 타입은 @ModelAttribute를 활용한다.

    • @ModelAttribute의 "name"값에 우리가 만든 객체를 넣어준다. 
      • 이 경우, 해당 객체의 필드와 매칭되는 요청 파라메터를 받아서 자동으로 바인딩하고, 그 객체를 만들어서 반환해준다.
      • 따라서, Item을 넣어주기만 하면 된다. 
    public String addV3(@ModelAttribute Item item, Model model) {
    
        Item savedItem = itemRepository.save(item);
        return "basic/item";
    }

    세번째 타입은 Model.addAttribute()를 줄인 것이다.

    • @ModelAttirbute에 name 값을 넣지 않으면, 클래스의 가장 앞부분을 소문자로 바꾼 것을 이름으로 해서 바인딩해준다.
    • @ModelAttribute를 하게 되면, 자동으로 Model에 해당 변수를 addAttribute해준다.
    public String addV4(Item item) {
        Item savedItem = itemRepository.save(item);
        return "basic/item";
    }

    네번째 타입은 @ModelAttribute 마저 없앴다.

    • 요청 파라미터의 경우 @ModelAttribute, @RequestParam을 생략할 수 있다.
      • 기본 타입 : 자동으로 @RequestParam과 매칭
      • 임의 생성 객체 : 자동으로 @ModelAttibute와 매칭

    위의 과정을 통해서 상품저장 컨트롤러를 완성했다. 주로 세번째 타입을 추천하는데, 네번째 타입은 명시성이 너무 부족하기 때문이라고 한다. 

     

     

    8. 상품수정 조회 컨트롤러 구현


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

    위 코드로 구현했다. 

    • 특정 상품을 수정하기 때문에 특정 상품 상세 페이지를 타고 들어온다. 따라서 정보를 다시 한번 표현해줄 필요가 있기 때문에 @PathVariable로 경로를 표현했다.
    • Get으로 넘어올 때 ItemId만 넘어오기 때문에 Item 객체에 대한 전체 정보는 제대로 알지 못한다. 따라서, itemRepositroy에서 필요한 값을 찾아온 후 Model로 넘겨줘야한다. 

     

    9. 상품수정 조회 뷰템플릿 구현


    <!DOCTYPE HTML>
    <html>
    <head>
        <meta charset="utf-8">
        <link 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" method="post">
            <div>
                <label for="id">상품 ID</label>
                <input type="text" id="id" name="id" class="form-control" value="1"
                       readonly>
            </div>
            <div>
                <label for="itemName">상품명</label>
                <input type="text" id="itemName" name="itemName" class="formcontrol" value="상품A">
            </div>
            <div>
                <label for="price">가격</label>
                <input type="text" id="price" name="price" class="form-control"
                       value="10000">
            </div>
            <div>
                <label for="quantity">수량</label>
                <input type="text" id="quantity" name="quantity" class="formcontrol" value="10">
            </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'" type="button">취소</button>
                </div>
            </div>
        </form>
    </div> <!-- /container -->
    </body>
    </html>
    

     

    먼저 HTML 디자인 파일은 위와 같다. 해야할 일을 정리하면 다음과 같다.

    • 동적으로 상품명, 가격, 수량 정보 표현하기
    • POST 메서드 보내는 경로 수정하기
    • 취소 시 상품상세 목록으로 돌아가도록 설정
    <div>
        <label for="id">상품 ID</label>
        <input type="text" id="id" name="id" class="form-control"
               th:value="${item.id}"
               value="1"
               readonly>
    </div>
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" name="itemName" class="formcontrol"
               th:value="${item.itemName}"
               value="상품A">
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" name="price" class="form-control"
               th:value="${item.price}"
               value="10000">
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" name="quantity" class="formcontrol"
               th:value="${item.quantity}"
               value="10">
    </div>

    동적으로 값이 나오게 하는 것은 th:value="${...}" 형태로 동적으로 나올 수 있게 구현해두었다.

    <form th:action
            action="item.html" method="post">

    POST 경로는 th:action으로 설정해두었다. th:action으로 설정해두면, 뒤에 아무 경로가 없기 때문에 현재 URL 그대로 POST를 다시 보낸다는 의미다. 이를 풀어서 쓰면, th:action="/basic/items/edit/${item.id}"가 될 것이다.

    <button class="w-100 btn btn-secondary btn-lg"
            th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|"
            onclick="location.href='item.html'" type="button">취소</button>

    상품상세 목록으로 돌아가야 하기 때문에 itemId를 동적으로 표현해서 /basic/items/{itemId} 컨트롤러를 호출할 수 있도록 설정했다. 

     

    10. 상품수정 컨트롤러 구현


    @PostMapping("/edit/{itemId}")
    public String editForm(@PathVariable Long itemId, @ModelAttribute Item item) {
        itemRepository.update(itemId, item);
        return "redirect:/basic/items";
    }

    상품수정 컨트롤러를 위와 같이 구현했다. 

    • 동일한 URL로 들어오지만 POST 메서드로 실제 수정이 이루어지도록 POST Mapping을 했다.
    • HTML FORM POST를 하면, Item정보가 넘어온다. 따라서, @ModelAttribute의 Item item으로 값을 받을 수 있따. 이를 바탕으로 update 함수를 구현해서 값을 업데이트했다.
    • return 값은 스프링의 redirect: 문법을 활용해서 상품목록 페이지로 리다이렉팅 했다.

     

    11. 상품 등록 컨트롤러의 개선(PRG 패턴 적용)


    현재 상품 등록 컨트롤러의 문제점이 있다. 바로 상품 등록을 한 상태에서 새로고침을 하면 마지막 메서드인 POST /add + 쿼리파라미터가 계속 실행된다는 점이다. 즉. 새로고침을 하면 위와 같이 동일한 상품이 계속 등록되는 경우가 있다. 이 경우를 해결해주기 위해 PRG 처리를 해주고자 한다.

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

    먼저 위와 같이 직접적으로 URL에 getId()로 넘기는 방법이 있다. 이렇게 URL을 더하기로 표현할 경우, 인코딩이 되지 않는다는 점과 공백이 있을 경우 제대로 된 리다이렉팅이 안될 수 있다는 것이다. 또한, 이외의 다른 값들을 View 템플릿에 넣어줄 수 없다는 것이 문제가 된다. 

    @PostMapping("/add")
    public String addV7(@ModelAttribute Item item, RedirectAttributes redirectAttributes) {
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", item.getId());
        redirectAttributes.addAttribute("status", true);
    
        return "redirect:/basic/items/{itemId}";
    }

    좀 더 좋은 방법은 위처럼 RedirectAttributes라는 매개변수를 받은 후, 이 매개변수에 필요한 값들을 넣어주는 것이다. 이 경우 리디렉팅에 {...} 형식으로 값을 넣을 수 있게 되면서 인코딩이 된 URL로 리다이렉팅을 유도할 수 있다. 또한, redirectAttribute는 model처럼 값이 뷰 템플릿으로 넘어간다. 뷰 템플릿에서는 param.변수명으로 접근이 가능하다.

    또한, 실제로 붙지 못한 나머지 값들은 모두 쿼리 파라메터 형식으로 뒷쪽에 붙는다. 이를 통해 클라이언트에 대략적인 성공유무를 알려줄 수도 있고, 뷰템플릿에서도 이 값을 인지하고 조건문을 활용해 여러가지들을 표현할 수 있게 된다.

     

    12. 상품상세 뷰 템플릿 업그레이드(상품 저장 표시하기)


    <h2 th:if="${param.status}" th:text="'저장 완료'"></h2>
    

    위 문구를 상품상세 뷰 템플릿에 추가를 한다. 이 경우, RedirectAttribute를 통해서 값이 넘어오는 값들을 param.변수 형태로 가져올 수 있다. 여기서 우리가 넘겨준 param.status라는 값이 true로 확인이 되면, "저장 완료"라는 것을 표현할 수 있도록 구현했다. 

     

    13. 상품삭제 뷰 템플릿 추가 + 상품삭제 컨트롤러 구현 


    먼저 뷰 템플릿에 상품삭제 박스를 추가하기로 했다. 기존의 상품 상세 뷰템플릿에 상품삭제 박스를 추가해서 컨트롤러로 접근할 수 있도록 구현하기로 했다.

    <div class="col">
        <button class="w-100 btn btn-secondary btn-lg"
                th:onclick="|location.href='@{/basic/items/delete/{itemId}(itemId=${item.id})}'|"
                onclick="location.href='items.html'" type="button">상품삭제</button>
    </div>

    박스는 바로 앞에 있는 '목록으로' 코드를 그대로 가져와서, onclick 경로를 /delete/{itemId} 형식으로 갈 수 있도록 변경했다. 

    @GetMapping("/delete/{itemId}")
    public String delete(@PathVariable Long itemId, RedirectAttributes redirectAttributes){
        itemRepository.deleteById(itemId);
    
        redirectAttributes.addAttribute("status", true);
    
        return "redirect:/basic/items";
    }

    삭제 컨트롤러는 GetMapping으로 구현했다. 현재 삭제하는데에 있어서는 GetMapping으로 넘어오는 itemId 파라미터면 충분하기 때문이다.

    삭제는 HashMap의 remove 기능으로 구현했다.

    삭제 이후 RedirectAttributes에 true를 반환해주었고, 상품 목록 페이지로 리디렉션 하도록 설정했다.

     

    <h2 th:if="param.status" th:text="'상품 삭제 완료'"></h2>

    상품 목록 페이지에서는 상품이 정상적으로 삭제되었을 때 "상품 삭제 완료'라는 것이 표현될 수 있도록 param.status로 구현했다. 

    위의 값들을 추가하고 실행해보면 다음과 같은 결과가 나오는 것을 확인할 수 있다.

     

     

     

     

    댓글

    Designed by JB FACTORY