실전! 스프링 부트와 JPA 활용1

박민서·2023년 5월 17일
0

Jpa1

목록 보기
6/6

상품 도메인 개발

상품 리포지토리 개발

  • 비즈니스 로직은 data를 가지고 있는 Entity에 비즈니스 로직이 있어야 응집력이 좋아진다.

    그래서 Item Entity에 재고 수량에 증감에 대한 비즈니스 로직을 작성했다.

// 재고 수량 증가
    public void addStock(int quantity){
        this.stockQuantity += quantity;
    }
 // 재고 수량 감소
    public void removeStock(int quantity){
        int restStock = this.stockQuantity-quantity;
        if(restStock<0){
            throw new NotEnoughStockException("need more stock");
        }
        this.stockQuantity = restStock;
    }

주문 서비스 개발

@RequiredArgsConstructor
fina이 붙거나 @NotNull 이 붙은 필드의 생성자를 자동 생성해주는 룸복 어노테이션

    // 주문 저장
        // Order 엔티티가 Delivery 엔티티와 OrderItem 엔티티를 cascade하여 Order 엔티티가 persist될 때 Delivery, OrderItem 엔티티도 persist된다.
        // cascade의 범위에 대해서 사람들이 많이 고민 함. order가 delivery와 order item을 관리하니까 딱 이정도 범위에서만 사용
        // delivery와 orderitem은 order엔티티 빼고는 아무도 참조하지 않는다. 다른곳에서도 참조하면 cascade를 막 사용해서는 안된다.
        orderRepository.save(order);
   // 주문 상품 생성
        OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);

주문 상품을 생성하는 코드를 누군가는 아래의 방식으로 작성할 수 있다.

  OrderItem orderItem1 = new OrderItem();
        orderItem.setCount();

이러한 방식의 문제는 코드가 점점 커질수록 유지보수 하기 어려워진다는 점이다.

그래서 우리는 이러한 방식의 코드를 막아야한다.

  • 방법 1

    • 생성자를 만들 때 protected 방식으로 만들면된다.

    protected OrderItem(){}

  • 방법 2

    • OrderItem entity에 @NoArgsConstructor(access = AccessLevel.PROTECTED)을 달아주면 된다.
  • 도메인 모델 패턴

    • 비즈니스 로직 대부분이 엔티티에 존재하는 코드
    • 서비스에 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할
    • 엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것
    • 장점
      • 객체 지향에 기반한 재사용성, 확장성 그리고 유지 보수의 편리함 필요에 따라 약간의 수정이 필요하겠지만 언제든지 재사용할 수 있다.
    • 단점
      • 하나의 도메인 모델을 구축하는데 많은 노력이 필요하다. 객체를 판별하고 객체들 간의 관계를 정립해야 하고, 데이터베이스 사이의 매핑에 대해서 고민해야 한다.
  • 트랜잭션 스크립트 패턴

    • 엔티티에는 비즈니스 로직이 거의 없다.
    • 서비스 계층에서 대부분의 비즈니스 로직을 처리
    • 장점
      • 구현 방법의 단순함 때문에 구현이 매우 쉽다. 얼마나 모듈화를 잘하는지에 따라 높은 효율을 낼 수 있다.
    • 단점
      • 비즈니스 로직이 복잡해질수록 난잡한 코드를 만들게 된다. 도메인에 대한 분석/설계 개념이 약하기 때문에 코드의 중복 발생을 막기 어려워진다.

    주문 검색 기능 개발

  • 동적 쿼리를 처리하는 방식은 몇가지 방식이 있다.

    • JPQL로 처리
      JPQL 쿼리를 문자로 생성하기에는 너무 번거롭고, 실수로 인한 버그가 충분히 발생할 수 있다.
  • JPA Citerial로 처리
    JPA Citerial는 JPA 표준 스펙이지만 실무에서 사용하기에는 너무 복잡하다.

    웹 계층 개발

    HomeController

    @Slf4j 어노테이션

    Logger log = LoggerFactory.getLogger(getClass());

    홈 화면과 레이아웃

    resources/templates/home.html

     <head th:replace="fragments/header :: header">
    <div th:replace="fragments/bodyHeader :: bodyHeader" />
    <div th:replace="fragments/footer :: footer" />
  • 랜더링 될 때 각각 fragments의 header,bodyHeader,footer로 바꿔치기 된다.

    <a class="btn btn-lg btn-secondary" href="/members/new">회원 가입</a>
  • href : a 태그의 href 속성은 링크된 페이지의 URL을 명시합니다.

    회원 등록

    controller/MemberForm

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

    • null과 ""허용 X " "은 허용

resources/templates/members/createMemberForm

  th:object="${memberForm} // memberForm 객체를 계속 사용하겠다.
  <input type="text" th:field="*{name}"
  • *표시가 되어있으면 memberForm 오브젝트를 사용한다는 의미. Getter, Setter로 MemberForm 클래스를 접근한다.

    Member 클래스

       @Getter @Setter
    public class MemberForm {
    @NotEmpty(message = "회원 이름은 필수 입니다.")
    private String name;
    
    private String city;
    private String street;
    private String zipcode;

    }

                             

controller/MemberController

@Vaild 어노테이션

  • @NotNull, @Size, @Pattern 등의 검증 어노테이션을 사용하여 필드의 값이 null이 아니고, 특정 크기를 갖고, 특정 패턴을 따르는지 등을 검증할 수 있다.

  • public String create(@Valid MemberForm form)

    • MemberForm 클래스에 @NotEmpty를 보고 유효성 검사를 해준다.

    controller/MemberController.

  • BindingResult

    @Valid MemberForm form

    • 여기서 오류가 나면 컨트롤러에 코드가 안넘어가고 오류가 팅겨 나가는데 BindingResult가 있으면 오류가 담겨서 코드가 실행이 된다.
  • public String create(@Valid MemberForm form, BindingResult result)

회원 목록 조회

  • 폼 객체를 써야하는지 엔티티를 집적 사용해야 하는지 문제가 있다.

    • 요구사항이 단순할 때는 엔티티를 직접 사용해도 된다.
    • 하지만 실무에서는 요구사항이 단순하지 않다.
    • 화면 기능이 많아질수록 엔티티가 지저분해 지고 유지보수가 어려워진다.
    • 엔티티는 비즈니스 로직만 존재하고 VIEW 관련 로직은 없어야한다.
    • API를 만들 때는 엔티티를 절대 반환해서는 안된다.
      • API 스펙이 변해버린다.
    • 그래서 폼 객체나 DTO를 사용을 권장한다.

    상품 수정

  • @PathVariable
    @GetMapping("/items/{itemId}/edit") url에서 {itemId} 부분은 동적으로 변하는 아이템 Id를 나타낸다. 이 아이템 Id를 컨트롤러에서 메서드에서 추출하려면 @PathVariable 어노테이션을 사용한다.

    public String updateItemForm(@PathVariable("itemId") @PathVariable 어노테이션은 {itemId} 경로 변수의 값을 itemId 매개변수로 추출한다. 추출한 itemId를 이용해 해당 아이템 정보를 조회하고, 적정한 응답을 반환하는 로직을 구현할 수 있다.

  • @ModelAttribute

    • 사용자가 요청시 전달하는 값을 오브젝트 형태로 매핑해주는 어노테이션이다.

    변경 감지와 병합(merge)

  • 준영속 엔티티

    • 영속성 컨텍스트가 더는 관리하지 않는 엔티티

      ItemController

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

      
      

    book은 새로운 book이 아닌 Id가 세팅이 되어있다. 이 말은 뭔가 JPA의 한 번 들어갔다 나온 애라는 거다. 그래서 이 book은 준영속 상태의 객체가 된다.

    • JPA가 관리하지 않기 때문에 값을 변경해도 DB에 반영되지 않는다.
  • 준영속 엔티티를 수정하는 2가지 방법

    • 변경 감지 기능 사용

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

      트랜잭션이 커밋되면 flush가 영속성 컨텍스트에 병견된 값을 보고 DB에 UPDATE쿼리를 날려 값을 수정한다. 이게 변경 감지 기능이다.
      Setter를 쓰지 말고 의미있는 메서드를 하나 만들어서 사용, 파라미터가 너무 많으면 DTO를 만들어서 사용

    • 병합(merge)사용

      • 병합 동작 방식
        1. merge() 를 실행한다.
        2. 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회한다.
          2-1. 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고, 1차 캐시에 저장한다.
        3. 조회한 영속 엔티티( mergeMember )에 member 엔티티의 값을 채워 넣는다. (member 엔티티의 모든 값을 mergeMember에 밀어 넣는다. 이때 mergeMember의 “회원1”이라는 이 름이 “회원명변경”으로 바뀐다.)
        4. 영속 상태인 mergeMember를 반환한다.

      ItemRepository

        public void save(Item item){
            if(item.getId()== null){
                em.persist(item);
            } else{
               Item merge = em.merge(item);
            }
        }

      em.merge(item) merge를 실행할 때 불러온 item은 영속상태로 변하진 않고, Item merge가 영속성 컨택스트 에서 관리되는 객체이다. 더 수정할 일이 있으면 merge객체를 사용해야 한다.

    • 병합 동작 방식의 문제점

      • 변경 감지 기능은 원하는 속성만 선택해서 변경할 수 있지만, 병합을 사용하면 모든 속성이 파라미터로 넘어온 값으로 변해버린다.
      • 병합 시에 파라미터에 값이 없으면 null로 업데이트 할 위험이 있다.

    그니까 가급적이면 merge를 쓰지 말고 변경 감지를 써라.

    Reference
    https://www.inflearn.com/course/lecture?courseSlug=%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-%ED%99%9C%EC%9A%A9-1&unitId=24344&tab=curriculum

profile
ㅎㅇㅌ

0개의 댓글