SpringJPA 웹 계층

GGOMG·2022년 9월 29일
0

1. 홈 화면과 레이아웃

레이아웃

  1. Include
  2. Hierarchical (계층형)
    코드의 중복을 없앨 수 있음

2. 회원 등록

MemberForm.class

@Getter
@Setter
public class MemberForm {

    @NotEmpty(message = "회원 이름은 필수입니다.")
    private String name;

    private String city;
    private String street;
    private String zipcode;
}
MemberController.class

@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:/";
    }
}
  • @Valid
    validation 기능
  • BindingResult
    오류가 발생하면 오류를 담아서 실행해줌
  • @PostMapping create()에 Member 엔티티가 아닌 MemberForm을 넣는 이유
    컨트롤-화면 validation과 도메인 validation이 다를 수 있기 때문
    코드도 지저분해짐

3. 회원 목록 조회

1. 폼 객체 vs 엔티티 직접 사용

요구사항이 정말 단순하다면 엔티티를 직접 사용해도 된다
그러나 실무에서 요구사항이 단순하지 않다
엔티티를 직접 폼에 써버리면, 화면을 구현하기 위한 요소들이 엔티티에 추가가된다
결과적으로 화면 종속적으로 변한다. -> 유지보수가 어려워진다

엔티티는 핵심 비즈니스 로직만 의존관계를 가지고, 최대한 순수하게 유지하자

2. API를 만들 때는 이유를 불문하고 절대 엔티티를 웹으로 반환하지 마라

@GetMapping("/members")
public String list(Model model) {
    List<Member> members = memberService.findMembers();
    model.addAttribute("members", members);
    return "members/memberList";
}

위의 create에서는 폼 객체를 썼는데
조회할 때는 Member 객체를 쓴다?
예제가 단순하고, 템플릿 엔진이기 때문에 이렇게 구성했을 뿐 실무에서는 DTO를 사용하자

엔티티 로직의 변화로 API 스펙이 변한다

4. 상품 등록

    // 여기선 validation 생략
    @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";
    }

예제와 달리 setter를 제거하고 order의 사례처럼 static 메소드를 만드는것이 더 나은 설계다

5. 상품 수정

    @PostMapping("items/{itemId}/edit")
    public String updateItem(@PathVariable String itemId, @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";
    }

보안상 취약하다. URL의 아이템 코드만 바꾸어서 권한없는 사용자가 무결성을 해친다

  • 권한이 있는 사용자인지 확인하는 로직을 추가한다
  • 업데이트할 객체를 세션에 담아서 사용한다

★ 준영속 엔티티 수정

준영속 엔티티란?

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

    @PostMapping("items/{itemId}/edit")
    public String updateItem(@PathVariable String itemId, @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";
    }

예제의 book은 준영속 엔티티이다
새로운 객체지만, 이미 존재하는 ID 값으로 세팅한다 = JPA가 식별자가 존재한다
이를 준영속상태의 객체라고 한다

준영속 엔티티의 문제

JPA가 관리를 하지 않는다 따라서 상태가 변경되어도 JPA가 알아서 업데이트하지 않는다
준영속 엔티티를 수정하는 2가지 방법을 알아보자

1. 변경 감지

@Transactional
public void updateItem(Long itemId, Book param) {
    Item findItem = itemRepository.findOne(itemId);
    findItem.setPrice(param.getPrice());
    findItem.setName(param.getName());
    findItem.setStockQuantity(param.getStockQuantity());
    // itemRepository.save(findItem);
}

마지막에

itemRepository.save(findItem);

코드를 넣어야할 것 같지만 없어도 된다

  1. findItem은 영속성 엔티티이다
  2. 값을 세팅한다
  3. spring의 @Transcational을 통해 트랜잭션이 커밋된다
  4. 커밋되면 JPA는 flush를 날린다.
    flush : 영속성 엔티티 변경사항을 찾음
  5. 바뀐 값에 대해서 업데이트를 쿼리함

2. 병합

준영속 상태의 엔티티를 영속 상태로 변경
em.merge(item);

itemService.saveItem(book);

->

itemRepository.save(item);

->

em.merge(item);

파라미터로 넘어온 준영속 엔티티의 식별자 값으로 엔티티를 조회한다
엔티티가 없으면 DB에서 엔티티를 조회한다
조회한 엔티티 값을 파라미터로 넘어온 준영속 엔티티 값들로 바꿔넣는다
return 조회한 엔티티(바꿔치기 당한 엔티티)

tmp엔티티를 merge한다
tmp 식별자를 1차 캐시에서 조회 (보통 없다)
DB에서 식별자를 가진 member엔티티를 꺼낸다
tmp의 값을 방금 꺼낸 member에 넣는다 member = tmp
return member

  • 주의점 1
    파라미터로 넣은 tmp는 여전히 영속성 컨텍스트가 아니다.
    return 받은 값은 영속성 컨텍스트이다.
    계속해서 로직을 작성하고 싶다면 return받은 값을 사용하자
  • 주의점 2
    변경감지는 원하는 속성을 변경할 수 있다.
    그러나 병합은 모든 값이 변경된다. 파라미터에 값이 없으면 null 값이 들어간다.

3. 결론

  • 변경감지를 사용하자
  • set메서드 말고 엔티티에 의미있는 메서드를 만들어 사용하자
  • 컨트롤러에서 어설프게 엔티티를 생성하지 말자

1. bad

@Transactional
public void updateItem(Long itemId, Book param) {
    Item findItem = itemRepository.findOne(itemId);
    findItem.setPrice(param.getPrice());
    findItem.setName(param.getName());
    findItem.setStockQuantity(param.getStockQuantity());
}
@PostMapping("items/{itemId}/edit")
public String updateItem(@PathVariable String itemId, @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";
}

2. good

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

3. best

별도의 DTO 클래스를 만드는 것이 더 나은 설계

6. 상품 주문

  • 컨트롤러에서는 식별자만 넘기고, 서비스 계층에서 핵심 비즈니스 로직을 구현하는게 좋다
    예제의 경우 파라미터 여러개를 넘김
@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";
}

출처
김영한 실전! 스프링 부트와 JPA 활용1 (인프런)

0개의 댓글