[스프링 JPA] 섹션7,8

haeyoon·2024년 10월 23일
0

섹션7

비즈니스 로직(도메인 로직)이란?

  • 컴퓨터 프로그램에서 실세계의 규칙에 따라 데이터를 생성, 표시, 저장, 변경하는 부분을 일컫는다. 특히 데이터베이스, 표시장치 등 프로그램의 다른 부분과 대조되는 개념으로 쓰인다.
  • 현실 세상의 문제를 해결하는 코드
  • 유저가 원하는 행위를 컴퓨터에게 잘 전달하기 위해 짜여진 코드 로직

코드

  • Order 메서드
package jpabook.jpashop.domain;
...
public class Order {
	...
    //==생성 메서드==//
    public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems){//orderitem여러개
        Order order = new Order();
        order.setMember(member);
        order.setDelivery(delivery);
        for (OrderItem orderItem : orderItems){
            order.addOrderItem(orderItem);
        }
        
        order.setStatus(OrderStatus.ORDER);
        order.setOrderDate(LocalDateTime.now());
        return order;
    }
}
🤔

생성 메서드를 따로 만드는 이유?

  • 캡슐화:

    • 생성 메서드는 객체를 생성하는 과정에서 필요한 모든 의존성을 설정할 수 있습니다. 이를 통해 Order 객체가 항상 일관된 상태를 유지합니다.
  • 불변성 유지:
    - 직접적으로 생성자를 호출하여 객체를 생성할 수 없도록 하고, 대신 createOrder 메서드를 사용하게 함으로써 객체 생성의 통제를 강화합니다.

  • OrderItem... orderItems : 가변 인자

    가변 인자는 메서드가 호출될 때 여러 개의 인자를 배열처럼 받을 수 있게 해준다

    • ex) Order.createOrder(member, delivery, orderItem1, orderItem2, orderItem3);
  •     //== 비즈니스 로직 ==//
        //주문 취소
        public void cancel() {
            if (delivery.getStatus() == DeliveryStatus.COMP) {//배송이 이미 완료(COMP)된 상태
                throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
            }
    
            this.setStatus(OrderStatus.CANCEL); //취소
            for (OrderItem orderItem : orderItems) { //for문 돌면서 재고 수량 바꾸기
                orderItem.cancle();
            }
        }
    
        //==조회 로직==//
        //전체 주문 가격 조회
        public int getTotalPrice() { //내가 주문한 전체 가격
    
            int totalPrice = 0;
            for (OrderItem orderItem : orderItems){
                totalPrice +=orderItem.getTotalPrice();
            }
            return totalPrice;
        }
    }
    • OrderItem
      package jpabook.jpashop.domain;
      
      import jakarta.persistence.*;
      import jpabook.jpashop.domain.item.Item;
      import lombok.AccessLevel;
      import lombok.Getter;
      import lombok.NoArgsConstructor;
      import lombok.Setter;
      
      @Entity
      @Getter @Setter
      @NoArgsConstructor(access = AccessLevel.PROTECTED)
      public class OrderItem {
          @Id @GeneratedValue
          @Column(name = "order_item_id")
          private Long id;
      
          @ManyToOne(fetch = FetchType.LAZY)
          @JoinColumn(name = "item_id")
          private Item item;
      
          @ManyToOne(fetch = FetchType.LAZY)
          @JoinColumn(name = "order_id")
          private Order order;
      
          private int orderPrice; // 주문 가격
          private int count; // 주문 수량
      
          //==생성 메서드==//
          public static OrderItem createOrderItem(Item item, int orderPrice, int count){
              OrderItem orderItem = new OrderItem();
              orderItem.setItem(item);
              orderItem.setOrderPrice(orderPrice);
              orderItem.setCount(count);
      
              item.removeStock(count);
              return orderItem;
          }
      
          //==비즈니스 로직==//
          public void cancle() { //재고수량을 원복시킨다
              getItem().addStock(count);
          }
      
          //==조회 로직==//
          //주문 상품 전체 가격 조회
          public int getTo
      }
      
    • OrderService
      package jpabook.jpashop.service;
      
      import jpabook.jpashop.domain.Delivery;
      import jpabook.jpashop.domain.Member;
      import jpabook.jpashop.domain.Order;
      import jpabook.jpashop.domain.OrderItem;
      import jpabook.jpashop.domain.item.Item;
      import jpabook.jpashop.repository.ItemRepository;
      import jpabook.jpashop.repository.MemberRepository;
      import jpabook.jpashop.repository.OrderRepository;
      import lombok.RequiredArgsConstructor;
      import org.springframework.stereotype.Service;
      import org.springframework.transaction.annotation.Transactional;
      
      import java.util.List;
      
      @Service
      @Transactional(readOnly = true)
      @RequiredArgsConstructor
      public class OrderService {
      
          private final OrderRepository orderRepository;
          private final MemberRepository memberRepository;
          private final ItemRepository itemRepository;
      
          //주문
          @Transactional
          public Long order(Long memberId, Long itemId, int count){
      
              //엔티티 조회
              Member member = memberRepository.findOne(memberId);
              Item item = itemRepository.findeOne(itemId);
      
              //배송정보 생성
              Delivery delivery = new Delivery();
              delivery.setAddress(member.getAddress());
      
              //주문상품 생성
              OrderItem orderItem = OrderItem.createOrderItem(item, item.getPrice(), count);
      
              //주문 생성
              Order order = Order.createOrder(member, delivery, orderItem);
      
              //주문 저장
              orderRepository.save(order); 
              return order.getId();
          }
      
          //취소
          @Transactional
          public void cancleOrder(Long orderId){
              //주문 엔티티 조회
              Order order = orderRepository.findOne(orderId);
              //주문 취소
              order.cancel();
          }
      }
      
      • 주문 메서드 - 주문 저장 코드
        orderRepository.save(order);

        Order 엔티티를 저장할 때 Cascade 옵션 덕분에 관련된 OrderItemDelivery도 자동으로 저장되므로, 별도로 이들 각각을 저장할 필요가 없다!

        → 자동으로 orderitem이랑 delivery도 persist

        public class Order {
            ...
                @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
                private List<OrderItem> orderItems = new ArrayList<>();
            
                @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
                @JoinColumn(name = "delivery_id")
                private Delivery delivery;
             ...
             }
        
    🤔

    비즈니스 로직 처리 방식?

    주문 서비스의 주문과 주문 취소 메서드를 보면 비즈니스 로직 대부분이 엔티티에 있다. 서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할을 한다

    • OrderRepository 동적쿼리란?
      • 실행 시점에 쿼리의 일부가 변경(사용자 입력 또는 프로그램 로직에 따라, 쿼리의 조건이나 구조를 동적으로 결정해야 하는 경우)될 수 있는 쿼리

      • 동적 SQL요소를 사용해 동적 쿼리를 작성:

        <if><choose><where><when><otherwise><foreach><set><trim>

        package jpabook.jpashop.repository;
        ...
        
        @Repository
        @RequiredArgsConstructor
        public class OrderRepository {
            private final EntityManager em;
        
            public void save(Order order){
                em.persist(order);
            }
        
            public Order findOne(Long id){
                return em.find(Order.class,  id);
            }
        
            //jpql로 처리
            public List<Order> findAllByString(OrderSearch orderSearch) {
                //language=JPAQL
                String jpql = "select o From Order o join o.member m";
                boolean isFirstCondition = true;
                //주문 상태 검색
                if (orderSearch.getOrderStatus() != null) {
                    if (isFirstCondition) {
                        jpql+= " where";
                        isFirstCondition = false;
                    }
                    else {
                        jpql += " and";
                    }
                    jpql += " o.status = :status";
                }
                //회원 이름 검색
                if (StringUtils.hasText((orderSearch.getMemberName()))) {
                    if (isFirstCondition) {
                        jpql += " where";
                        isFirstCondition = false;
                    }
                    else {
                        jpql += " and";
                    }
                    jpql += " m.name like :name";
                }
                TypedQuery<Order> query = em.createQuery(jpql, Order.class)
                        .setMaxResults(1000); //최대 1000건
                if (orderSearch.getOrderStatus() != null) {
                    query = query.setParameter("status", orderSearch.getOrderStatus());
                }
                if (StringUtils.hasText(orderSearch.getMemberName())) {
                }
                query = query.setParameter("name", orderSearch.getMemberName());
                return query.getResultList();
            }
        
            //JPA Criteria로 처리
            public List<Order> findAllByCriteria(OrderSearch orderSearch) {
                CriteriaBuilder cb = em.getCriteriaBuilder();
                CriteriaQuery<Order> cq = cb.createQuery(Order.class);
                Root<Order> o = cq.from(Order.class);
                Join<Order, Member> m = o.join("member", JoinType.INNER); //회원과 조인
                List<Predicate> criteria = new ArrayList<>();
                //주문 상태 검색
                if (orderSearch.getOrderStatus() != null) {
                    Predicate status = cb.equal(o.get("status"),
                            orderSearch.getOrderStatus());
                    criteria.add(status);
                }
                //회원 이름 검색
                if (StringUtils.hasText(orderSearch.getMemberName())) {
                    Predicate name =
                            cb.like(m.<String>get("name"), "%" + orderSearch.getMemberName()
                                    + "%");
                    criteria.add(name);
                }
                cq.where(cb.and(criteria.toArray(new Predicate[criteria.size()])));
                TypedQuery<Order> query = em.createQuery(cq).setMaxResults(1000); //최대 1000건
        
                return query.getResultList();
            }
        }
        

    섹션8

    JPA에서의 Update에 관한 쿼리문은 변경감지(Dirty checking)병합(Merge)을 통해 이루어진다

    엔티티 생명 주기

    • 비영속 상태

      객체만 생성한 상태

      Member member = new Member();

    • 영속 상태

      영속성 컨텍스트에 의해 관리되어지는 상태

      • em.persist()를 통해 영속성 컨텍스트에 저장
      • em.find()를 통해 DB에서 엔티티를 조회

      트랜잭션 안에서 entity를 조회해야 영속성 상태로 조회된다

      em.persist(member)

    • 준영속 상태

      영속성 컨텍스트에 저장되었다가 분리된 상태

      영속 상태X

      • em.detach(member); //멤버만 준영속 상태로 전환
      • em.clear() : 영속성 컨텍스트를 완전히 초기화
      • em.close() : 영속성 컨텍스트를 종료

    준영속 엔티티 수정하는 방법

    1. 변경감지(Dirty Checking)

     @Transactional
     void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
    		Item findItem = em.find(Item.class, itemParam.getId()); //같은 엔티티를 조회한다.
        findItem.setPrice(itemParam.getPrice()); //데이터를 수정한다.
     }
    • entityManager로 entity를 직접 꺼내, 값을 수정하는 방식
    • 영속성 컨텍스트에서 엔티티를 다시 조회 → 데이터 수정

    2. 병합(Merge)

     @Transactional
     void update(Item itemParam) { //itemParam: 파리미터로 넘어온 준영속 상태의 엔티티
    		Item mergeItem = em.merge(itemParam); //병합
     }
    • 준영속 상태의 엔티티 → 영속 상태로 변경할 때 사용

    1) 준영속 엔티티(Member)의 식별자 값으로 1차 캐시 또는 DB에서 영속엔티티(mergeMember)을 조회

    2) 조회한 영속엔티티(mergeMember)의 값(회원1)을 준영속 엔티티(Member)의 값(회원명변경)으로 교체 = 병합

    3) 트랜잭션 commit 시점에 변경감지를 한 후 Flush 해서 DB에 UPDATE SQL이 실행

    🚨

    엔티티 변경할 때 병합을 사용하면 안되는 이유?

    변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만(부분 교체),

    병합을 사용하면 모든 속성이 변경된다(전체 교체).

    → 병합시 값이 없으면 null 로 업데이트 할 위험도 있다. (모든 필드를 교체해서)

    profile
    https://haeyoon12.tistory.com/

    0개의 댓글

    관련 채용 정보