웹 계층 개발
회원 등록, 조회
폼 객체를 사용해서 화면 계층과 서비스 계층을 명확하게 분리한다.
MemberForm
@Getter @Setter
public class MemberForm {
@NotEmpty(message = "회원 이름은 필수입니다.")
private String name;
private String city;
private String street;
private String zipcode;
}
MemberController
@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) { // 문제가 생기면 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:/";
}
@GetMapping("/members")
public String list(Model model) {
List<Member> members = memberService.findMembers();
model.addAttribute("members",members);
return "members/memberList";
}
}
Ctril + Alt + N : 인라인화 -> 코드 두개를 합쳐서 보여줌
API 만들 때는 엔티티를 외부로 반환하면 절대 안 됨!
Model addAttribute(String name, Object value)
- value 객체를 name 이름으로 추가한다.
- 뷰 코드에서는 name으로 지정한 이름을 통해서 value를 사용한다.
상품 등록, 조회, 수정
BookForm
@Getter @Setter
public class BookForm {
private Long id;
private String name;
private int price;
private int stockQuantity;
private String author;
private String isbn;
}
itemController
@Controller
@RequiredArgsConstructor
public class ItemController {
private final ItemService itemService;
@GetMapping("/items/new")
public String createForm(Model model) {
model.addAttribute("form", new BookForm());
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:/";
}
@GetMapping("/items")
public String list(Model model) {
List<Item> items = itemService.findItems();
model.addAttribute("items", items);
return "/items/itemList";
}
@GetMapping("/items/{itemId}/edit")
public String updateItemForm(@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";
}
@ModelAttribute
- 전달받은 파라미터를 Model에 담아서 전달할 때 필요한 어노테이션
- 스프링에서 Java beans 규칙(Getter, Setter, 생성자 포함)에 맞는 객체는 파라미터 전달이 자동으로 가능.
- 하지만 일반 변수의 경우, 자동 전달 x -> model 객체를 통해서 전달 필요.
- model.addAttributed("xxx", xxx); -> 코드 생략 가능
변경 감지와 병합
준영속 엔티티
- 영속성 컨텍스트가 더는 관리하지 않는 엔티티(변경 감지 x)
- (여기서는 itemService.saveItem(book) 에서 수정을 시도하는 Book 객체다. Book 객체는 이미 DB 에 한번 저장되어서 식별자가 존재한다. 이렇게 임의로 만들어낸 엔티티도 기존 식별자를 가지고 있으면 준영속 엔티티로 볼 수 있다.)
준영속 엔티티를 수정하는 2가지 방법
- 변경 감지 기능 사용
itemService
@Transactional
public void updateItem(Long itemId, Book param) { // param: 파리미터로 넘어온 준영속 상태의 엔티티
Item findItem = itemRepository.findOne(itemId); // 같은 엔티티를 조회한다.
findItem.setPrice(param.getPrice()); // 데이터를 수정한다.
findItem.setName(param.getName());
findItem.setStockQuantity(param.getStockQuantity());
// 이 코드들을 findItem.change(~~)로 하여 변경지점이 추적 가능하게 설계
}
영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터를 수정하는 방법
트랜잭션 안에서 엔티티를 다시 조회, 변경할 값 선택
-> 트랜잭션 커밋 시점에 변경 감지(Dirty Checking) 이 동작해서 데이터베이스에 UPDATE SQL 실행
- 병합( merge ) 사용
병합은 준영속 상태의 엔티티를 영속 상태로 변경할 때 사용하는 기능이다.
@Transactional
void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
Item mergeItem = em.merge(itemParam);
}
병합 동작 방식
- 1. merge() 를 실행한다.
- 2. 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다.
- 2-1. 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고, 1차 캐시에 저장한다.
- 3. 조회한 영속 엔티티( mergeMember )에 member 엔티티의 값을 채워 넣는다. (member 엔티티의 모든 값을 mergeMember에 밀어 넣는다. 이때 mergeMember의 “회원1”이라는 이름이 “회원명변경”으로 바뀐다.)
- 4. 영속 상태인 mergeMember를 반환한다.
주의!
- 변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만, 병합을 사용하면 모든 속성이 변경된다. 병합시 값이 없으면 null 로 업데이트 할 위험도 있다. (병합은 모든 필드를 교체한다.)
- merge 말고 변경 감지 기능을 쓰자 !
- 컨트롤러에서 어설프게 엔티티를 생성 x
- 트랜잭션이 있는 서비스 계층에 식별자( id )와 변경할 데이터를 명확하게 전달(파라미터 or dto)
- 트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회하고, 엔티티의 데이터를 직접 변경
- 트랜잭션 커밋 시점에 변경 감지가 실행됨
itemService
@Transactional
public void updateItem(Long id, String name, int price, int stockQuantity) {
Item item = itemRepository.findOne(id);
item.setName(name);
item.setPrice(price);
item.setStockQuantity(stockQuantity);
// Setter 없이 바로 추적 가능한 메서드 만들기 findItem.change(name, price, stockQuantity)
}
itemController
@PostMapping(value = "/items/{itemId}/edit")
public String updateItem(@PathVariable Long itemId, @ModelAttribute("form") BookForm form) {
itemService.updateItem(itemId, form.getName(), form.getPrice(),
form.getStockQuantity());
return "redirect:/items";
}
상품 주문
orderSearch
@Getter @Setter
public class OrderSearch {
private String memberName; // 회원 이름
private OrderStatus orderStatus; // 주문 상태 [ORDER, CANCEL]
}
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";
} // 핵심 비즈니스 로직이 있는 경우 트랜잭션 내에서 영속성 컨텍스트로 관리 가능하게 memberId, itemId, count만 넘김
@GetMapping("/orders")
public String orderList(@ModelAttribute("orderSearch")OrderSearch orderSearch, Model model) {
List<Order> orders = orderService.findOrders(orderSearch);
model.addAttribute("orders", orders);
return "order/orderList";
}
@PostMapping("/orders/{orderId}/cancel")
public String cancelOrder(@PathVariable("orderId") Long orderId) {
orderService.cancelOrder(orderId);
return "redirect:/orders";
}
참고 강의 :
실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의
실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., - 강
www.inflearn.com
'JPA > 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발' 카테고리의 다른 글
[스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발] 3~6. 도메인 개발 (0) | 2023.01.04 |
---|---|
[스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발] 2. 도메인 분석 설계 (0) | 2023.01.03 |
[스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발] 1. 프로젝트 환경설정 (0) | 2023.01.02 |