IT recording...

[스프링 JPA1] 3. 컨트롤러 본문

Spring

[스프링 JPA1] 3. 컨트롤러

I-one 2022. 2. 15. 00:53

[원문링크]

https://adorable-aspen-d23.notion.site/JPA1-3-854d982b16f548719baa55ca118d50bf

 

[스프링 JPA1] 3. 컨트롤러

1. MemberController

adorable-aspen-d23.notion.site

김영한님의 [실전!스프링부트와JPA활용1 - 웹 어플리케이션개발] 강의를 듣고 작성한 글입니다.

 

1. 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 memberForm, BindingResult result){
        //@Valid -> validation을 사용하는구나, MemberForm내부에 필수로 사용하게 적어놓은 것을 필수로 받음
        //BindingResult -> 에러가 있어도 그 내용을 여기에 담아서 실행이 됨
        if(result.hasErrors()){
            return "members/createMemberForm";
        }

        Address address = new Address(memberForm.getCity(), memberForm.getStreet(),memberForm.getZipcode());

        Member member = new Member();
        member.setUsername(memberForm.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";
    }
}

1-1. MemberForm

@Getter @Setter
public class MemberForm {
    @NotEmpty(message = "회원 이름은 필수 입니다.")
    private String name; //이름은 필수적으로 입력받도록 함

    private String city;
    private String street;
    private String zipcode;
}
  • @NotEmpty
    • 필수적으로 받아야 하는 정보를 명시한다.
    • @NotEmpty를 설정한 필드가 있을 때 @Valid를 걸어주면 입력되지 않았을 시 에러를 뿜는다.
    • 그 에러 메시지를 BindingResult에 받을 수 있음
@PostMapping("/members/new")
public String create(**@Valid MemberForm memberForm**, BindingResult result){
    if(result.hasErrors()){
        return "members/createMemberForm";
    }
    ...
    return "redirect:/";
}

1-2. 폼 객체 사용 이유

  • Member를 사용하지 않고 MemberForm을 사용하는 이유가 무엇일까?
  • 요구사항이 복잡해지게 되면 Entity에 화면을 처리하기 위한 기능이 증가하며 종속적으로 변한다.
  • 유지보수가 어려워진다는 말이다.
  • 실무에서 엔티티는 핵심 비즈니스 로직만 가지고 있고, 화면을 위한 로직은 없어야 한다. → 화면이나 API에 맞는 폼 객체 / DTO를 사용하자.

2. 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 crate(BookForm bookForm){
        Book book = new Book();
        book.setName(bookForm.getName()); //실제 설계를 할 때는 setter를 제거하고 create메소드를 사용하자
        book.setPrice(bookForm.getPrice());
        book.setStockQuantity(bookForm.getStockQuantity());
        book.setAuthor(bookForm.getAuthor());
        book.setIsbn(bookForm.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.setAuthor(item.getAuthor());
        form.setStockQuantity(item.getStockQuantity());
        form.setIsbn(item.getIsbn());

        model.addAttribute("form",form);
        return "items/updateItemForm";
    }
		//위의 방식보다는 아래의 방식으로해야 영속성 컨텍스트의 지원을 받을 수 있다.
    @PostMapping("items/{itemId}/edit")
    public String updateItem(@ModelAttribute("form") BookForm form){
        **itemService.updateItem(form.getId(), form.getName(),form.getPrice(),form.getStockQuantity());**
        return "redirect:/items";
    }
}
  • Controller에서는 가급적 엔티티의 id, 변경할 data만을 넘겨주는 역할만 하도록 하자
  • 대신 service에서 일 다 해줌
    • 한 트랜잭션 내에서 데이터 변경이 일어나니 원자성, 일관성, 고립성, 지속성을 보장한다.
//merge는 null도 업데이트 해주니, 아래와 같은 변경 감지를 이용하자
@Transactional
public void updateItem(Long itemId, String name, int price, int stockQuantity){
    Item findItem = itemRepository.findOne(itemId);
    //얘는 영속 상태니까 데이터만 바꿔주면 JPA가 알아서 flush 날려준다.(바뀐애들 업데이트쿼리 디비에 날림)
    //원래는 set말고 change메소드 같은 것을 만들어서 사용해야 함(역추적 가능)
    findItem.setPrice(price);
    findItem.setName(name);
    findItem.setStockQuantity(stockQuantity);
}

2-1. 변경 감지와 병합(merge)

  • 준영속 엔티티
    • 영속성 컨텍스트가 더는 관리하지 않는 엔티티
    • 기존 식별자를 가지고 있는 엔티티
  • 변경 감지 기능
    • 영속성 컨텍스트에서 엔티티를 다시 조회한 후 데이터를 수정한다.
    • 트랜잭션 내부에서 수행해야 한다. (service 계층)
      • 트랜잭션 커밋 시점에 변경 감지(Dirty Checking)이 동작해서 데이터베이스에 UPDATE SQL 날림
    • 서비스 계층에 식별자(id)와 변경할 데이터(parameter/dto)를 전달한다.
@Transactional
void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
	 Item findItem = em.find(Item.class, itemParam.getId()); //같은 엔티티를 조회한다.
	 findItem.setPrice(itemParam.getPrice()); //데이터를 수정한다.
}
  • 병합 사용
    • 준영속 상태의 엔티티를 영속 상태로 변경할 때 사용
    • 가급적 사용X (모든 필드를 변경하기 때문에 null 문제 발생 가능)
@Transactional
void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
	 Item mergeItem = em.merge(item);
}

2-2. BookForm

@Getter @Setter
public class BookForm {
	 private Long id;
	 private String name;
	 private int price;
	 private int stockQuantity;
	 private String author;
	 private String isbn;
}

3. OrderController

@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";
    }

    @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 cancel(@PathVariable("orderId") Long orderId){
        orderService.cancelOrder(orderId);
        return "redirect:/orders";
    }
}
Comments