[Spring Boot2][2] 7. 웹 계층 개발(2)

🏷 상품 수정

상품 등록, 조회와 달리 수정은 까다로운 부분이 많으니 집중해서 듣고 확실히 이해하도록 하자😉


✔️ 상품 수정 컨트롤러

    // itemId는 변경될 수 있으므로 @PathVariable 사용
    @GetMapping("items/{itemId}/edit")
    public String updateItemForm(@PathVariable("itemId") Long itemId, Model model) {
        // 캐스팅을 하는 것이 좋은 방법은 아니나, 여기서는 예제 단순화를 위해 사용
        Book item = (Book) itemService.findOne(itemId);

        // 폼을 업데이트 할 건데, 엔티티가 아닌 BookForm 을 보냄
        BookForm form = new BookForm();
        form.setId(item.getId());
        form.setName(item.getName());
        form.setPrice(item.getPrice());
        form.setStockQuantity(item.getStockQuantity());
        form.setAuthor(item.getAuthor());
        form.setIsbn(item.getIsbn());

        model.addAttribute("form", form);
        return "items/updateItemForm";
    }

    @PostMapping("items/{itemId}/edit")
    // @PathVariable Long itemId : 폼에서 그대로 넘어오기 때문에 생략해도 됨
    // @ModelAttribute("form") : 폼에서 오브젝트 이름으로 설정한 부분이 그대로 넘어옴
    public String updateItem(@PathVariable Long itemId, @ModelAttribute("form") BookForm form) {

        itemService.updateItem(itemId, form.getName(), form.getPrice(), form.getStockQuantity());

        return "redirect:/items";
    }
  • 수정 버튼을 선택하면 /items/{itemId}/edit URL을 GET 방식으로 요청
  • 그 결과로 updateItemForm() 메서드를 실행하는데 이 메서드는 itemService.findOne(itemId) 를 호출해서 수정할 상품을 조회
  • 조회 결과를 모델 객체에 담아서 뷰(items/updateItemForm)에 전달

✔️ 상품 수정 폼

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>

<div class="container">
    <div th:replace="fragments/bodyHeader :: bodyHeader"/>

    <form th:object="${form}" method="post">
        <!-- id -->
        <input type="hidden" th:field="*{id}" />

        <div class="form-group">
            <label th:for="name">상품명</label>
            <input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요" />
        </div>
        <div class="form-group">
            <label th:for="price">가격</label>
            <input type="number" th:field="*{price}" class="form-control" placeholder="가격을 입력하세요" />
        </div>
        <div class="form-group">
            <label th:for="stockQuantity">수량</label>
            <input type="number" th:field="*{stockQuantity}" class="form-control" placeholder="수량을 입력하세요" />
        </div>
        <div class="form-group">
            <label th:for="author">저자</label>
            <input type="text" th:field="*{author}" class="form-control"  placeholder="저자를 입력하세요" />
        </div>
        <div class="form-group">
            <label th:for="isbn">ISBN</label>
            <input type="text" th:field="*{isbn}" class="form-control"  placeholder="ISBN을 입력하세요" />
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>

    <div th:replace="fragments/footer :: footer" />

</div> <!-- /container -->

</body>
</html>


🏷 변경 감지와 병합(merge)

💡 즈엉~말 중요한 내용이니 꼭❗️ 완벽하게 이해하고 넘어갑시다^-^


준영속 엔티티란 ❓

  • 영속성 컨텍스트가 더는 관리하지 않는 엔티티!
  • 여기서는 itemService.saveItem(book) 에서 수정을 시도하는 Book 객체다.
  • Book 객체는 이미 DB 에 한번 저장되어서 식별자가 존재한다!
  • 이렇게 임의로 만들어낸 엔티티도 기존 식별자를 가지고 있으면 준영속 엔티티로 볼 수 있다.

준영속 엔티티를 수정하는 2가지 방법❗️

  • 변경 감지 기능 사용
  • 병합(merge) 사용

1️⃣ 변경 감지 기능 사용

@Transactional
// itemParam : 파리미터로 넘어온 준영속 상태의 엔티티
void update(Item itemParam) { 
	Item findItem = em.find(Item.class, itemParam.getId()); // 같은 엔티티를 조회한다.
	findItem.setPrice(itemParam.getPrice()); // 데이터를 수정한다. 
}

✔️ 영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터를 수정하는 방법

  • 트랜잭션 안에서 엔티티를 다시 조회, 변경할 값 선택
  • 트랜잭션 커밋 시점에 변경 감지(Dirty Checking) 이 동작해서 데이터베이스에 UPDATE SQL 실행!

2️⃣ 병합(merge) 사용

병합은 준영속 상태의 엔티티를 영속 상태로 변경할 때 사용하는 기능이다❗️

@Transactional
// itemParam : 파리미터로 넘어온 준영속 상태의 엔티티 
void update(Item itemParam) { 
	Item mergeItem = em.merge(item);
}


1️⃣ merge()를 실행
2️⃣ 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회함
➡️ 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고, 1차 캐시에 저장
3️⃣ 조회한 영속 엔티티(mergeMember)에 member 엔티티의 값을 채워 넣는다.
➡️ member 엔티티의 모든 값을 mergeMember에 밀어 넣는데, 이 때 mergeMember의 “회원1”이라는 이름이 “회원명변경”으로 바뀜
4️⃣ 영속 상태인 mergeMember를 반환한다.


✔️ 병합 시 동작 방식 정리
1️⃣ 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회
2️⃣ 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 교체 (=병합, 쉽게 말하면 바꿔치기!!!)
3️⃣ 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 UPDATE SQL이 실행


🤚🏻 주의

  • 변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만, 병합을 사용하면 모든 속성이 변경된다(=모든 필드를 교체해버림!)
  • 병합시 값이 없으면 null로 모든 필드를 업데이트 할 위험이 생긴다!

📌 상품 리포지토리의 저장 메서드 분석 - ItemRepository

@Repository
@RequiredArgsConstructor
public class ItemRepository {

    private final EntityManager em;

    public void save(Item item) {
        // 처음 저장할 때는 아이템의 id가 존재하지 않으므로 이 부분을 고려해야 함
        if (item.getId() == null) {
            // 아이템 그냥 저장
            em.persist(item);
        } else {
            // 아이템 업데이트(?) - 나중에 자세히 배움
            em.merge(item);
        }
    }
  • save() 메서드는 식별자 값이 없으면(=null) 새로운 엔티티로 판단해서 영속화(persist)하고 식별자가 있 으면 병합(merge)
  • 지금처럼 준영속 상태인 상품 엔티티를 수정할 때는 id 값이 있으므로 병합 수행!

💡 가장 좋은 해결 방법 - 엔티티를 변경할 때는 항상 변경 감지를 사용하자!

  • 컨트롤러에서 어설프게 엔티티를 생성하지 않기!
  • 트랜잭션이 있는 서비스 계층에 식별자와 변경할 데이터를 명확하게 전달하기!
  • 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회하고, 엔티티의 데이터를 직접 변경하기
    ➡️ 이렇게 하면 트랜잭션 커밋 시점에 변경 감지가 실행됨👍🏻


🏷 상품 주문

✔️ 상품 주문 컨트롤러

@Controller
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;
    private final MemberService memberService;
    private final ItemService itemService;

    @GetMapping("/order")
    public String createForm(Model model) {
        // 내가 가진 모든 멤버들과 아이템들을 다 가져옴
        List<Member> members = memberService.findMembers();
        List<Item> items = itemService.findItems();
        // 가져온 후 모델에 담아서 폼으로 넘김
        model.addAttribute("members", members);
        model.addAttribute("items", items);

        return "order/orderForm";
    }

    @PostMapping("/order")
    public String order(@RequestParam("memberId") Long memberId,
                        @RequestParam("itemId") Long itemId,
                        @RequestParam("count") int count) {
        // 파라미터로 주문 생성
        orderService.order(memberId, itemId, count);
        // 주문 내역 목록으로 리다이렉트
        return "redirect:/orders";
    }
}
  • 메인 화면에서 상품 주문을 선택하면 /order 를 GET방식으로 호출
  • OrderControllercreateForm() 메서드
  • 주문 화면에는 주문할 고객정보와 상품 정보가 필요하므로 model 객체에 담아서 뷰에 넘겨줌

✔️ 상품 주문 폼

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>

<div class="container">
    <div th:replace="fragments/bodyHeader :: bodyHeader"/>

    <form role="form" action="/order" method="post">

        <div class="form-group">
            <label for="member">주문회원</label>
            <select name="memberId" id="member" class="form-control">
                <option value="">회원선택</option>
                <option th:each="member : ${members}"
                        th:value="${member.id}"
                        th:text="${member.name}" />
            </select>
        </div>

        <div class="form-group">
            <label for="item">상품명</label>
            <select name="itemId" id="item" class="form-control">
                <option value="">상품선택</option>
                <option th:each="item : ${items}"
                        th:value="${item.id}"
                        th:text="${item.name}" />
            </select>
        </div>

        <div class="form-group">
            <label for="count">주문수량</label>
            <input type="number" name="count" class="form-control" id="count" placeholder="주문 수량을 입력하세요">
        </div>

        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
    <br/>
    <div th:replace="fragments/footer :: footer" />

</div> <!-- /container -->

</body>
</html>

⬆️ 실행 결과 👍🏻



🏷 주문 목록 검색, 취소

✔️ 주문 목록 검색 컨트롤러

    @GetMapping("/orders")
    public String orderList(@ModelAttribute("orderSearch") OrderSearch orderSearch, Model model) {
        List<Order> orders = orderService.findOrders(orderSearch);
        model.addAttribute("orders", orders);

        return "order/orderList";
    }

✔️ 주문 목록 검색 화면

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header"/>
<body>

<div class="container">

    <div th:replace="fragments/bodyHeader :: bodyHeader"/>

    <div>
        <div>
            <form th:object="${orderSearch}" class="form-inline">
                <div class="form-group mb-2">
                    <input type="text" th:field="*{memberName}" class="form-control" placeholder="회원명"/>
                </div>
                <div class="form-group mx-sm-1 mb-2">
                    <select th:field="*{orderStatus}" class="form-control">
                        <option value="">주문상태</option>
                        <option th:each="status : ${T(jpabook.jpashop.domain.OrderStatus).values()}"
                                th:value="${status}"
                                th:text="${status}">option
                        </option>
                    </select>
                </div>
                <button type="submit" class="btn btn-primary mb-2">검색</button>
            </form>
        </div>

        <table class="table table-striped">
            <thead>
            <tr>
                <th>#</th>
                <th>회원명</th>
                <th>대표상품 이름</th>
                <th>대표상품 주문가격</th>
                <th>대표상품 주문수량</th>
                <th>상태</th>
                <th>일시</th>
                <th></th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="item : ${orders}">
                <td th:text="${item.id}"></td>
                <td th:text="${item.member.name}"></td>
                <td th:text="${item.orderItems[0].item.name}"></td>
                <td th:text="${item.orderItems[0].orderPrice}"></td>
                <td th:text="${item.orderItems[0].count}"></td>
                <td th:text="${item.status}"></td>
                <td th:text="${item.orderDate}"></td>
                <td>
                    <a th:if="${item.status.name() == 'ORDER'}" href="#" th:href="'javascript:cancel('+${item.id}+')'"
                       class="btn btn-danger">CANCEL</a>
                </td>
            </tr>

            </tbody>
        </table>
    </div>

    <div th:replace="fragments/footer :: footer"/>

</div> <!-- /container -->

</body>
<script>
    function cancel(id) {
        var form = document.createElement("form");
        form.setAttribute("method", "post");
        form.setAttribute("action", "/orders/" + id + "/cancel");
        document.body.appendChild(form);
        form.submit();
    }
</script>
</html>

✔️ 주문 취소 컨트롤러

    @PostMapping("/orders/{orderId}/cancel")
    public String cancelOrder(@PathVariable("orderId") Long orderId) {
        orderService.cancelOrder(orderId);
        return "redirect:/orders";
    }

다음에 할 일.....🤔

이제까지 전체적으로 한 사이클을 돌아봤다!!!!

다음에 할 일은 API를 만들어 보는 것이다😮

과거에는 이렇게 화면 만드는 정도로 끝나는 정도가 많았지만,
요즘 개발은 주로 싱글 페이지 애플리케이션도 많고, 거의 API로 통신해야 하는 상황도 상당히 많다!

그래서 우리는❗️
➡️ JPA를 가지고 어떻게 API를 잘 만들어 내는지,
➡️ 그리고 나아가서 성능 최적화까지 다뤄볼 예정이다!


API도 할 수 이따! 이따!

좋은 웹페이지 즐겨찾기