홈 화면 및 레이아웃
홈 컨트롤러 등록
@Controller
@Slf4j
public class HomeController {
@RequestMapping("/")
public String home() {
log.info("home controller");
return "home";
}
}
가입하기
화면과 서비스 레이어를 명확하게 구분하기 위해 폼 객체를 사용합니다.
회원 등록 양식 객체
@Getter @Setter
public class MemberForm {
@NotEmpty(message = "회원 이름은 필수입니다.
")
private String name;
private String city;
private String street;
private String zipcode;
}
회원 등록 관리자
@Controller
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
@GetMapping("/members/new")
public String createForm(Model model) {
model.addAttribute("memberForm", new MemberForm());
return "members/createMemberForm";
}
@PostMapping("/members/new")
public String create(@Valid MemberForm form, BindingResult result) {
if (result.hasErrors()) {
return "members/createMemberForm";
}
Address address = new Address(form.getCity(), form.getStreet(), form.getZipcode());
Member member = new Member();
member.setName(form.getName());
member.setAddress(address);
memberService.join(member);
return "redirect:/";
}
}
회원 목록 쿼리
구성원 목록 컨트롤러 추가
@Controller
@RequiredArgsConstructor
public class MemberController {
//...
@GetMapping("/members")
public String list(Model model) {
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
}
- 검색된 상품을 Spring MVC에서 제공하는 Model 객체에 저장하여 View에 전달합니다.
- 실행할 보기의 이름을 반환합니다.
참고: 양식 개체 및 엔터티를 직접 사용
참고: 요구 사항이 매우 간단한 경우 등록 및 편집 화면에서 양식 개체( MemberForm ) 없이 엔터티( Member )를 직접 사용할 수 있습니다.
그러나 화면 요구 사항이 더 복잡해짐에 따라 엔터티는 화면을 처리할 수 있는 기능이 점점 더 많아집니다.
이렇게 하면 엔티티가 화면에 점점 더 의존하게 되고 화면 기능이 지저분해지는 엔티티는 결국 유지 관리할 수 없게 됩니다.
실제로 엔터티에는 핵심 비즈니스 논리만 있어야 하며 화면 논리는 없어야 합니다.
화면이나 API에 맞는 양식 개체 또는 DTO를 사용하십시오. 따라서 이를 사용하여 화면 또는 API 요구 사항을 처리하고 엔터티를 가능한 한 순수하게 유지하십시오.
제품 등록
제품 등록 컨트롤러
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping("/items/new")
public String createForm(Model model) {
model.addAttribute("form", new Book());
return "items/createItemForm";
}
@PostMapping("/items/new")
public String create(BookForm form) {
Book book = new Book();
book.setName(form.getName());
book.setPrice(form.getPrice());
book.setStockQuantity(form.getStockQuantity());
book.setAuthor(form.getAuthor());
book.setIsbn(form.getIsbn());
itemService.saveItem(book);
return "redirect:/items";
}
}
- 제품 등록 양식에 데이터를 입력하고 제출 버튼을 클릭하면 POST 방식으로 /items/new가 요청됩니다.
- 상품 저장 후 상품 목록 화면으로 이동 ( redirect:/items )
상품 목록
@Controller
@RequiredArgsConstructor
public class ItemController {
//...
@GetMapping("/items")
public String list(Model model) {
List<Item> items = itemService.findItems();
model.addAttribute("items", items);
return "items/itemList";
}
}
- 아이템을 꺼내서 상품 정보를 출력, 아이템은 모델에 저장된 상품 목록입니다.
제품 수정
제품 수정 관련 컨트롤러 코드
@Controller
@RequiredArgsConstructor
public class ItemController {
// ...
@GetMapping("/items/{itemId}/edit")
public String uodateItemForm(@PathVariable("itemId") Long itemId, Model model) {
Book item = (Book) itemService.findOne(itemId);
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")
public String updateItem(@ModelAttribute("form") BookForm form) {
Book book = new Book();
book.setId(form.getId());
book.setName(form.getName());
book.setPrice(form.getPrice());
book.setStockQuantity(form.getStockQuantity());
book.setAuthor(form.getAuthor());
book.setIsbn(form.getIsbn());
itemService.saveItem(book);
return "redirect:/items";
}
}
- “편집” 버튼을 선택하면 GET 방식으로 /items/{itemId}/edit URL을 요청합니다.
- 따라서 updateItemForm() 메서드가 실행되어 itemService.findOne(itemId)를 호출하여 수정할 제품을 검색합니다.
- 검색 결과를 모델 객체에 넣고 뷰에 전달합니다( items/updateItemForm ).
제품 수정 실행
상품 편집 양식 HTML에는 상품 ID(숨김), 상품명, 가격 및 수량 정보가 포함되어 있습니다.
- 상품 수정 양식의 정보를 수정하려면 제출 버튼을 클릭하십시오.
- POST 메서드에서 /items/{itemId}/edit URL을 요청하고 updateItem() 메서드를 실행합니다.
- 이때 컨트롤러에 매개변수로 전달된 아이템 엔터티 인스턴스는 현재 반영구적인 상태입니다.
따라서 지속성 컨텍스트를 지원할 수 없으며 데이터가 수정되더라도 변경 감지가 작동하지 않습니다.
변경 감지 및 병합
참고: 이것은 정말 중요하므로 반드시 읽어야 합니다!
완전히 이해해야 합니다.
반영구적 존재?
지속성 컨텍스트에서 더 이상 관리하지 않는 엔티티입니다.
(여기서 itemService.saveItem(book)은 Book 객체를 수정하려고 시도한다.
Book 객체는 DB에 한 번 저장되어 식별자를 가진다.
이렇게 임의로 생성된 개체도 반영구적 개체로 볼 수 있다.
기존 식별자.)
반영구 항목을 수정하는 두 가지 방법
- 변경 감지 사용
- 병합 사용
변경된 기능 감지 활성화
@Transactional
void update(Item itemParam) { // itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
Item findItem = em.find(Item.class, itemParam.getId()); // 같은 엔티티를 조회한다.
findItem.setPrice(itemParam.getPrice()); // 데이터를 수정한다.
}
지속성 컨텍스트에서 엔터티를 다시 쿼리한 후 데이터를 수정하는 방법
트랜잭션 내에서 엔터티를 다시 검색하여 변경할 값 선택 → 트랜잭션 커밋 시 변경 감지(dirty check)가 실행되고 데이터베이스에서 UPDATE SQL 실행
병합 사용
머지(Merge)는 반영구적 상태의 엔터티를 영속적 상태로 바꾸는 기능입니다.
@Transactional
void update(Item itemParam) { // itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
Item mergeItem = em.merge(item);
}
병합: 기존 엔터티
합병 작동 방식
- 병합() 실행
- 인수로 전달된 반영구 엔터티의 식별자 값을 사용하여 기본 캐시에서 엔터티를 검색합니다.
- 검색된 영구 엔터티(mergeMember)에 구성원 엔터티의 값을 입력합니다.
- 영구 상태인 mergeMember를 반환합니다.
합체시 조작방법 간단 요약
- 반영구적 엔터티의 식별자 값을 이용하여 영속적 엔터티를 검색합니다.
- 모든 영구 엔터티 값은 반영구적 엔터티 값으로 대체(병합)됩니다.
- 트랜잭션이 커밋되면 변경 감지가 활성화되고 데이터베이스에서 UPDATE SQL이 실행됩니다.
지침:
변경 감지를 사용하면 원하는 속성만 선택하여 변경할 수 있지만 병합을 사용하면 모든 속성이 변경됩니다.
병합할 때 값이 없으면 null로 업데이트될 위험도 있습니다(병합하면 모든 필드가 대체됨).
인용하다:
실제로 업데이트 기능은 일반적으로 매우 제한적입니다.
그러나 데이터가 없는 경우 병합은 모든 필드를 변경하고 null로 업데이트합니다.
병합을 사용할 때 이 문제를 해결하려면 항상 모든 데이터를 양식 변경 화면에 유지하십시오. 사실 병합을 사용하는 것은 일반적으로 변경 가능한 데이터만 노출하기 때문에 다소 번거롭습니다.
최상의 솔루션
엔티티를 변경할 때는 항상 변경 감지를 사용하십시오.
- 컨트롤러에서 엔터티 생성을 방해하지 마십시오.
- 식별자(id)와 변경할 데이터를 트랜잭션이 위치한 서비스 레이어(파라미터 또는 DTO)에 명시적으로 전달합니다.
- 트랜잭션 서비스 계층에서 지속 상태의 엔터티를 쿼리하고 엔터티의 데이터를 직접 변경합니다.
- 변경 감지는 트랜잭션 커밋에서 실행됩니다.
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
// ...
@PostMapping("items/{itemId}/edit")
public String updateItem(@PathVariable("itemId") Long itemId, @ModelAttribute("form") BookForm form) {
itemService.updateItem(form.getId(), form.getName(), form.getPrice(), form.getStockQuantity());
return "redirect:/items";
}
}
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ItemService {
private final ItemRepository itemRepository;
// ...
@Transactional
public void updateItem(Long itemId, String name, int price, int stockQuantity) {
Item findItem = itemRepository.findOne(itemId);
findItem.setName(name);
findItem.setPrice(price);
findItem.setStockQuantity(stockQuantity);
}
// ...
}
제품 주문
제품 주문 컨트롤러
@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 메소드로 호출됩니다.
- OrderController의 createForm() 메서드
- 주문 화면은 주문을 하기 위해 고객 정보와 상품 정보가 필요하므로 모델 객체에 넣어 뷰에 전달합니다.
주문 실행
- 주문할 회원, 상품, 수량을 선택하고 Submit 버튼을 클릭하면 POST 방식으로 /order URL 호출
- 컨트롤러의 order() 메서드 실행
- 이 메서드는 주문 서비스에서 주문을 요청하기 위해 고객 식별자(memberId), 상품 식별자(itemId) 및 수량(count) 정보를 수신합니다.
- 주문이 완료되면 해당 제품의 주문 내역이 포함된 /orders URL로 리디렉션됩니다.
주문 목록 조회, 취소
주문 목록 검색 컨트롤러
@Controller
@RequiredArgsConstructor
public class OrderController {
//...
@GetMapping("/orders")
public String orderList(@ModelAttribute("orderSearch") OrderSearch orderSearch, Model model) {
List<Order> orders = orderService.findOrders(orderSearch);
model.addAttribute("orders", orders);
return "order/orderList";
}
}
주문 취소
@Controller
@RequiredArgsConstructor
public class OrderController {
//...
@PostMapping("/orders/{orderId}/cancel")
public String cancelOrder(@PathVariable("orderId") Long orderId) {
orderService.cancelOrder(orderId);
return "redirect:/orders";
}
}