jpa-spring boot 실습 - 4

김강현·2023년 4월 9일
0

SPRING-JPA-실습

목록 보기
4/5

Order

  • order 같은 복잡한 class 같은 경우는 별도의 생성 메소드가 있으면 좋음
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order {
    @Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<OrderItem>();

    // 주로 접근하는 것에 FK (Foreign 키를 두는 편) <- 연관관계 주인
    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;

    private LocalDateTime orderDate;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;

    // == 연관관계 편의 메서드 ==  //
    public void setMember(Member member){
        this.member = member;
        member.getOrders().add(this);
    }

    public void addOrderItem(OrderItem orderItem){
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }

    public void setDelivery(Delivery delivery){
        this.delivery = delivery;
        delivery.setOrder(this);
    }
}
   //== 생성 메서드 ==//
   public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems){ 
       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;
   }

여러개 받아올 수 있을때, ... 을 붙혀주면 가능함!!

  • 생성 메소드로만 인스턴스 생성이 가능하도록 하고 싶으면!!
  1. protected
protected Order(){}

이렇게 생성자를 protected 로 지정해두면, 생성 메소드안에서만 가능하게 됨!

  1. @NoArgsConstructor(access = AccessLevel.PROTECTED)
    lombok 의 어노테이션을 활용하면, 외부에서 인스턴스 생성 불가능!
    //== 비즈니스 로직 ==//

    /**
     * 주문 취소
     */
    public void cancel() {
        if(delivery.getStatus() == DeliveryStatus.COMP){
            throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
        }
        this.setStatus(OrderStatus.CANCEL);
        for (OrderItem orderItem : orderItems) {
            orderItem.cancel();
        }
    }
    //== 조회 로직 ==//

    /**
     * 전체 주문 가격 조회
     */
    public int getTotalPrice(){
        return orderItems.stream().mapToInt(OrderItem::getTotalPrice).sum();
//        똑같이 동작
//        int totalPrice = 0;
//        for (OrderItem orderItem : orderItems) {
//            totalPrice += orderItem.getTotalPrice();
//        }
//        return totalPrice;
    }

OrderService

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
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.findOne(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();
    }

    // 취소

    // 검색
}

OrderService 의 경우 MemberRepository, ItemRepository 가 추가로 필요함. final field 로 지정하여, @RequiredArgsConstructor 에 의해 생성될때 주입 받음

위 기능에서 orderRepository.save(order) 만으로 되는 이유!!

Order.java -> CascadeType.ALL 으로 지정해두었기 때문에 Order 만 영속성 컨텍스트에 persist 하더라도,
OrderItem, Delivery는 자동으로 따라 올라감!!

Order 가 두 객체 상대로 확실하게 private owner 포지션이기 때문에 가능!!

도메인 모델 패턴

비즈니스 로직 대부분이 엔티티에 있음!
서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할을 한다.

  • 이처럼 엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것을 도메인 모델 패턴 이라고 함
  • 반대로 엔티티에는 비즈니스 로직이 거의 없고, 서비스 계층에서 대부분의 비즈니스 로직을 처리하는 것을
    트랜잭션 스크립트 패턴 이라고 함

==> 한 프로젝트 안에서도, 문맥에 따라 두개가 공존하기도 한다!!

Test Code

참고! 정말로 좋은 Test Code는 운영 코드와 완전히 분리되어 비즈니스 로직들만 테스트 할 수 있는 것!!
지금의 경우, spring 이나 jpa 가 함께 종속되어 있으므로, 완벽하게 좋은 Test Code 라고 할 수는 없다.

    ...
    @Test
    public void 상품주문() throws Exception {
        // given
        Member member = getMember();
        Book book = getBook();

        // when
        int orderCount = 2;
        Long orderId = orderService.order(member.getId(), book.getId(), orderCount);

        // then
        Order getOrder = orderRepository.findOne(orderId);

        assertEquals("상품 주문시 상태는 ORDER", OrderStatus.ORDER, getOrder.getStatus());
        assertEquals("주문한 상품 종류 수가 정확해야 한다.", 1, getOrder.getOrderItems().size());
        assertEquals("주문한 가격은 가격 * 수량 이다.", 10000 * orderCount, getOrder.getTotalPrice());
        assertEquals("주문 수량만큼 재고가 줄어야 한다.", 8, book.getStockQuantity());
    }
    ...
    private Book getBook() {
        Book book = new Book();
        book.setName("시골 JPA");
        book.setPrice(10000);
        book.setStockQuantity(10);
        em.persist(book);
        return book;
    }

    private Member getMember() {
        Member member = new Member();
        member.setName("member1");
        member.setAddress(new Address("서울", "강가", "123-123"));
        em.persist(member);
        return member;
    }
    ...

ctrl + alt + p 하면, 함수 내 값을 parameter 로 받도록 자동 세팅!!

재고 초과 에러

    @Test(expected = NotEnoughStockException.class)
    public void 상품주문_재고수량초과() throws Exception {
        // given
        Member member = getMember("member1");
        Book book = getBook("시골 JPA", 10000, 10);

        int orderCount = 11;

        // when
        orderService.order(member.getId(), book.getId(), orderCount);
        
        // then
        fail("여기가 실행이 되면 안된다!");
    }

expected 에 Exception 정의해주면, 메소드 종료

실제 재고 수량 초과에 대한 알고리즘은 Item.java 파일에 있음

    public void removeStock(int quantity){
        int restStock = this.stockQuantity - quantity;
        if(restStock < 0){
            throw new NotEnoughStockException("need more stock");
        }
        this.stockQuantity = restStock;
    }

이 메소드에 대한, 단위 테스트를 만드는 것이 좋음!!

  • Entity 에 비즈니스 로직이 들어가 있으므로, Entity 단위로 테스트를 만드는 것도 괜찮다!!

주문 검색 기능

  • 동적 쿼리가 필요한 상황
    OrderSearch.java 를 만들어서, search 관련 파라미터들을 전달하는 용도로 사용.
@Getter @Setter
public class OrderSearch {
    private String memberName; // 회원 이름
    private OrderStatus orderStatus; // 주문 상태 [ ORDER, CANCEL ]
}

< OrderRepository.java >

    public List<Order> findAll(OrderSearch orderSearch) {
        List<Order> resultList = em.createQuery("select o from Order o join o.member m"
                + " where o.status = :status "
                + " and m.name like :name", Order.class)
                .setParameter("status", orderSearch.getOrderStatus())
                .setParameter("name", orderSearch.getMemberName())
                .setMaxResults(1000)
                .getResultList();
        return resultList;
    }

이처럼 orderSearch 를 받아서, jpql 생성가능!!

여기서, memberName 이 null 이거나 orderStatus 가 null 인 경우도 처리해 주어야지!!!
1. (권장 x) if 분 분기시켜서, jpql 구문 수정. query 수정
2. (권장 x) JPA Criteria 활용 => jpql / 쿼리 자동 생성 => 직관적이지 못함. 유지보수 힘듬
3. Query DSL [ 추후 제대로 ㄱㄱ ]

동적 쿼리, 복잡한 jpql 문 등등 자바 문법으로 가시화 하기 위해, Query DSL 을 쓰는 것을 강추

profile
this too shall pass

0개의 댓글