Main (Home) 화면
- Controller (HomeController), home.html,
- 클라이언트의 요청을 1차적으로 받는 곳.
- 클라이언트가 요청을 하면, 해당 요청을 TomCat 내장 서버가 이를 받아오고 Spring Container 속 Controller들 중 이 요청에 Mapping 하는 것이 있는지 확인함.
- Main home 화면 매핑 →
String home()
@RequestMapping("/")
: main이 되는 화면의 Get 요청에 mappingreturn "home";
: 스프링 부트 thymeleaf viewName 맵핑 (resources:templates/ +{ViewName}+ .html.) 에 따라 resources의 templates의 home.html 을 HTTP 통신으로 Client에게 넘겨줌.
- home.html
- thymleaf 문법을 사용하며 header.html, bodyHeader.html, footer.html을 include하고 있음. (즉, include style. 이는 hierarchical style로 구성할 수도 있음)
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header">
<div th:replace="fragments/bodyHeader :: bodyHeader"/>
<div th:replace="fragments/footer :: footer"/>
Member 화면
회원 등록 화면
- 회원 등록 Form →
class MemberForm
- DTO : Data Transaction Object 로직을 가지지 않는 데이터 객체이고 getter/setter 메소드만 가진 클래스. 즉, data를 옮기는 역할만 하는 객체
- 화면에서 name, address(city, street, zipcode) 를 받아오는 역할
private String name;
←@NotEmpty(message = "이름은 필수로 입력하셔야 됩니다.")
⇒@NotEmpty
: validation 역할. 비어있으면 안되는 변수로 설정. 비어 있으면 해당 message를 날림.
- (Controller) 회원 등록 시 열어보는 용도 → Get →
String register(Model model)
@GetMapping("/members/new")
Model model
은 html로 넘어가 thymeleaf와 상호작용하는 놈. 즉, html에 데이터를 넘겨주는 역할. (model에 속성으로 데이터를 넣어주면 thymeleaf를 통해 html에서 받아온 데이터를 사용할 수 있음.)model.addAttribute("memberForm",new MemberForm());
: 빈 껍데기의 MemberForm 객체를 가지고 감- Validation 가능해짐.
- 입력된 정보를 mebmerform을 통해 PostMapping으로 보내줄 수 있음. (즉, 객체로 보내지는 것)
return "members/createMemberForm";
: resources의 templates의 members에서 createMemberForm.html을 HTTP 통신으로 Client에게 보냄.
- (Controller) 회원 정보 작성 후 제출, 등록하는 용도 → Post →
String join(@Valid MemberForm memberForm, BindingResult result)
memberForm
: Get에서 담겨 보냈던 등록 Form으로 Post하면 해당 Form 객체에 정보가 담긴 채 Postmapping으로 오게 됨. (@Valid
를 통해 내가 해당 form에서 설정한 필수 값인 name이 담겨 있는지 체크 가능. 즉, 없다면 Error(오류화면)를 발생 시킬 수 있게 됨.)BindingResult result
: 오류화면으로 넘어가지않고, 그 값이 없는 상태의 error form자체를 그냥 받아와줌. 즉, 오류가 BindingResult에 담겨짐. 즉, 어떤 것이 Error인지 아는 상태가 되며, 그 오류를 담은 상태로 하위 코드들이 실행 되는 것. (결과를 바인딩한다.)if (result.hasErrors()){ return "members/createMemberForm";}
: 그 result에 error가 담겨 있다면, MemberFrom, BindingResult를 담아서 다시 creatememberForm으로 넘김. 그럼 방금 담겨 있던 정보들과 name 자체에 error가 발생했다는 것을 아는 상태로 넘어가는 것. → 이를 thymeleaf를 통해 이벤트를 생성할 수 있음. (-> spring과 thymleaf가 연동(integration)되어 있기에 가능한 것)memberService.join(member);
: 에러가 발생하지 않았다면, Address를 저장하고 Member를 저장.return "redirect:/";
: 재로딩하지 않고 다시 home으로 돌아감- html에서의 thymeleaf는 생략.
회원 조회 화면
- (Controller) 회원 조회 →
String list(Model model)
@GetMapping("/members")
List<Member> members = memberService.findMembers();
: memberService (비지니스 로직) 을 통해 전체 회원 조회.model.addAttribute("members", members);
: html에 넘겨주는 model에 members list를 속성에 추가하여 넘겨줌.return "members/memberList";
: members의 memberList.html에 model을 넘겨주고 이를 바탕으로 해당 html을 Client에게 넘겨줌.
Item 화면
상품 등록 및 조회
- 회원 등록 및 조회와 똑같은 로직으로 진행.
- MemberForm → BookForm (수정을 위해 id를 추가한 form 생성)
상품 수정 [중요!]
- 수정 (Get) →
String updateItemForm(@PathVariable("itemId") Long itemId, Model model)
@GetMapping("/items/{itemId}/edit")
: get url 자체에 변수를 넣어준 것.- 수정버튼을 누르게 되면 해당 item의 id가 url에 실리고 그 id를 바탕으로 DB에서 em을 통해 해당 객체를 가져오게 됨 → 그 후, 객체의 정보를 form에 옮긴 후 이를 화면에 출력.
@PathVariable("itemId") Long itemId
: url에 담긴 변수를 itemId에 받아줌.Book item = (Book) itemService.findOne(itemId);
: 그 id를 바탕으로 DB에서 em을 통해 해당 객체를 가져오게 됨BookForm form = new BookForm();
from.set(item.get())...
: 객체의 정보를 form에 옮긴 후model.addAttribute("form", form);
: 이를 화면에 출력.
- 수정 (Post) →
String updateItem(@PathVariable("itemId") Long itemId, @ModelAttribute("form") BookForm form
@PostMapping("/items/{itemId}/edit")
: get url 자체에 변수를 넣어준 것.- 여기선 사실 PathVariable은 필요없음. → BookForm을 통해 id를 받아왔으므로.
- 받아온 form를 통해 새로운 Book Entity를 생성해 준 후 itemService를 통해 저장하면 됨.
Book book = new Book();
: 새로운 Book Entity를 생성 (수정하려고 하는 item의 id와 똑같은 id를 가지고 있는 book entity 생성)book.set(form.get())...
: 받아온 form의 정보를 새로운 Entity에 심어줌itemService.saveItem(book);
: 해당 Entity 저장.- 여기서 saveItem(저장 로직 method)를 보면 id가 있을 때와 없을 때를 구분지어 저장함.
- id가 없을 때 →
em.persist(item)
(즉, 새로 저장되는 Entity) - [중요!] id가 있을 때 **→
em.merge(item)
(즉, 수정된 Entity는 병합 진행! merge는 덮어씌워주는 개념. but 실무에서 잘 안씀. → 변경하지 않은 값들에 대해서도 덮어씌어지며 이상하게 변할 수 있기 때문)
변경감지 vs merge [중요!]
- 공통점 : 어쨌든 변경감지를 통해 Update를 진행
- 차이점 : 변경감지는 내가 원하는 값들만을 수정할 수 있지만, merge는 선택권없이 모든 값들을 수정(병합)함. → 변경감지를 쓰는 이유
- merge →
em.merge()
- 위에서 진행한 로직 그대로 사용할 경우의 상황
- 즉, 준영속 상태의 엔티티를 영속 상태로 변경할 때 사용하는 기능
- 준영속 엔티티 : 영속성 컨텍스트가 더는 관리하지 않는 엔티티.
- 위에서 진행한 로직을 보면, Book Entity를 새로 생성하고 Id 등 기존 정보들의 수정본을 심어줬음. 이러한 객체가 바로 준영속 Entity.
- em으로 가져온 것이 아니기에 영속 Entity가 아니지만, id는 부여되어 id를 통해 em이 DB에서 영속 Entity를 찾아올 수 있기에 준영속 엔티티라고 불림.
-
병합시 동작 방식을 간단히 정리
- 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회
- 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 교체 (병합)
- 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 UPDATE SQL이 실행
- 변경감지
- 영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터를 수정하는 방법 (즉, em을 통해 Entity를 가져와 영속성 컨텍스트에 올리고, 그 상태에서 set을 통해 데이터를 수정)
- 트랜잭션 안에서 엔티티를 다시 조회 후 변경 → 트랜잭션 커밋 시점에 변경 감지(Dirty Checking)이 동작해서 데이터베이스에 UPDATE SQL 실행 ⇒ 즉, 따로 persist를 하지 않아도 됨. (이미 영속성 컨텍스트 안에 있기에)
itemService.updateItem(itemId, form.getName(), form.getPrice(), form.getStockQuantity());
@Transactional // merge는 사실 이 변경감지 코드와 동일하게 동작함. 단지 merge하나만 적어주면 그 과정을 알아서 진행 (but, 모두 변경) public void updateItem(Long itemId, String name, int price, int quantity){ // merge가 아닌 변경감지로 저장하는 것. -> 영속성인 객체를 가져와서 context에 올리는 것 Item findItem = itemRepository.findOne(itemId); findItem.setName(name); // setter 없이 Entity안에서 직접 수정이 일어나도록 메서드를 짜는 게 더 좋은 코딩 findItem.setPrice(price); findItem.setStockQuantity(quantity); // 영속성 객체이기에 따로 em을 이용해 persist할 필요가 없음. -> 변경감지를 통해 자동으로 update } // 해당 Transactional이 끝나고 commit 시점에 자동으로 변경사항에 대한 query를 날려주는 것.
주문 (상품 주문)
- Order Entity는 앞선 도메인 개발에서 알 수 있듯이 member와 item등 여러 연관관계를 가지고 있기에 member,item service의 DI를 받아야 됨.
- 주문 등록
- (Controller) 주문 페이지 → Get →
String createForm(Model model)
@GetMapping("/order")
- 멤버 선택 및 상품 선택을 위해 모든 멤버와 상품을 불러들임. →
List<Member> members = memberService.findMembers();
,List<Item> items = itemService.findAll();
model.addAttribute("members", members);
,model.addAttribute("items", items);
return "order/orderForm";
-
(Controller) 주문 등록 → Post →
@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"; }
- 주문 등록 시 html을 통해 각 멤버와 상품의 id를 받아 와줌 (+ 주문 수량)
- 이를 바탕으로 Order Entity를 생성 (생성메서드 + 연관관계 메서드를 통해 Entity를 생성)
- 이때 중요한 것은 Entity 자체를 method에 보내주는 것이 아닌 식별자를 보내 줌으로 써 Service 내에서 Entity를 다룰 수 있도록 함. (즉, Transactional 안에서 Entity를 다루게 하는 것) → Entity는 Controller 보다는 Transactional이 있는 Service에서 다루는 것이 Best!!
- (Controller) 주문 페이지 → Get →
- 주문 조회 및 취소
- 주문 조회 → Get →
String orderList(OrderSearch orderSearch, Model model)
OrderSearch
는 MemberForm과 같이 web상에서 입력되는 값을 객체로 받아드려오는 것.- 그 OrderSearch의 값들을 통해 동적쿼리를 통해 해당 조건에 맞는 Order들을 불러옴 →
List<Order> orders = orderService.findOrders(orderSearch);
(findOrders는findAllByCriteria
를 통해 동적쿼리 실행.)
- 주문 취소 → Post →
String cancelOrder(Long orderId)
- 주문 List에서 취소 버튼을 누르면 작동
@PostMapping("/orders/{orderId}/cancel")
→ 얘 또한 식별자 값을 받아와 Service에서 취소 진행. (Transactional 안에서 값 변경 → 변경 감지 가능!) →orderService.cancelOrder(orderId);
- 주문 조회 → Get →