주문 도메인 개발

woom·2023년 4월 30일

Spring Boot

목록 보기
5/6
post-thumbnail

김영한 강사님 [실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발] 강의 참조


🌼 구현 기능

  • 상품 주문
  • 주문 내역 조회
  • 주문 취소

🌼 주문, 주문상품 엔티티 개발

  • 엔티티 작성시 생성메소드, 비즈니스 로직, 조회 로직을 구분하여 작성할 것을 권장

📕 주문 엔티티

  • 생성 메서드( createOrder() ): 주문 회원, 배송정보, 주문상품의 정보를 받아서 실제 주문 엔티티를 생성

  • 주문 취소( cancel() ): 주문 상태를 취소로 변경하고 주문상품에 주문 취소를 알린다. 만약 이미 배송을 완료한 상품이면 주문을 취소하지 못하도록 예외를 발생시킨다.

  • 전체 주문 가격 조회: 주문 시 사용한 전체 주문 가격을 조회. 연관된 주문상품들의 가격을 조회해서 더한 값을 반환

    // == 생성 메서드 (주문생성) == //
    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
        order.setOrderDate(LocalDateTime.now());//주문시간 : 현재시간
        return order;
    }

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

    // == 조회 로직 == //
    //전체 주문 가격 조회
    public int getTotalPrice(){
        int totalPrice = 0;
        for (OrderItem orderItem: orderItems) {
            totalPrice += orderItem.getTotalPrice();
            //주문가격과 주문수량이 필요하므로 orderitem메소드 호출
        }
        return totalPrice;
    }
}

📙 주문상품 엔티티

  • 생성 메서드( createOrderItem() ): 주문 상품, 가격, 수량 정보를 사용해서 주문상품 엔티티를 생성. 주문한 수량만큼 상품의 재고를 줄인다.

  • 주문 취소( cancel() ): 취소한 주문 수량만큼 상품의 재고를 증가시킨다.

  • 주문 가격 조회( getTotalPrice() ): 주문 가격에 수량을 곱한 값을 반환


    // == 생성 메서드 (주문아이템 생성) == //
    public static OrderItem createOrderItem(Item item, int orderPrice, int count){
    //쿠폰받거나 할인받을 수 있기 때문에 orderprice 따로 작성
        OrderItem orderItem = new OrderItem();
        orderItem.setItem(item);
        orderItem.setOrderPrice(orderPrice);
        orderItem.setCount(count);
        
        item.removeStock(count);
        return orderItem;
    }
    
    // == 비즈니스 로직 == //
    //주문취소시 재고수량 원복
    public void cancel(){
        getItem().addStock(count);
    }

    // == 조회 로직 == //
    //주문상품 전체가격 조회
    public int getTotalPrice() {
        return getOrderPrice() * getCount();
    }
}

📒 메소드 작성시 개념

  • public static Order createOrder(OrderItem... orderItems){}
    • 위 코드에서 ...은 가변 인자(variable arguments)를 의미 (메소드에서 인자의 개수가 정해져 있지 않은 경우에 사용)
    • OrderItem... orderItems는 OrderItem 객체가 0개 이상인 배열을 가변 인자로 받는 것을 나타낸다. 이렇게 선언된 가변 인자는 메소드에서 배열 형태로 처리.
    • 예를 들어, createOrder 메소드를 호출할 때 OrderItem 객체를 3개 전달하고 싶다면
	OrderItem orderItem1 = new OrderItem();
	OrderItem orderItem2 = new OrderItem();
	OrderItem orderItem3 = new OrderItem();
	Order.createOrder(orderItem1, orderItem2, orderItem3);
  • 이렇게 하면 createOrder 메소드에서는 OrderItem 객체를 담은 배열이 아니라, 개별적인 인자로 받아서 배열로 묶어서 처리(OrderItem... orderItems는 가변 인자를 배열 형태로 처리하는 것을 의미)

🌼 주문 리포지포리 개발

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

}

🌼 주문 서비스 개발

  • 주문( order() ): 주문하는 회원 식별자, 상품 식별자, 주문 수량 정보를 받아서 실제 주문 엔티티를 생성한 후 저장
  • 주문 취소( cancelOrder() ): 주문 식별자를 받아서 주문 엔티티를 조회한 후 주문 엔티티에 주문 취소 요청
  • 주문 검색( findOrders() ): OrderSearch 라는 검색 조건을 가진 객체로 주문 엔티티를 검색
	//주문
    @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();
    }

    //주문 취소
    @Transactional
    public void cancelOrder(Long orderId){
        //주문 엔티티 조회
        Order order = orderRepository.findOne(orderId);
        //주문 취소
        order.cancel();
    }

📗 서비스 관련 개념

  • 엔티티 클래스에서 연관관계 매핑 시 cascade = CascadeType.ALL : 상위 엔티티에서 하위 엔터티로 모든 작업을 전파
    • order 클래스만 persist 해주면 cascade = CascadeType.ALL로 매핑된 orderitem과 delivery도 자동 persist됨
    • cascade 설정 범위는 참조하는 대상이 private owner일 경우에 사용 (orderitem과 delivery는 order만 참조함)
    • 참조하는 대상이 많을 경우에는 repository를 따로 생성해서 작성하는 것을 권장
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;
  • new연산자를 통해서 객체를 직접 생성하는 것 권장 하지 않음.
    • 생성 메소드를 통하지 않고 객체를 생성할 경우 유지보수의 어려움이 있으므로 생성하지 못하게 설정
  1. protected OrderItem() {} : JPA는 protected까지 허용
  2. @NoArgsConstructor(access = AccessLevel.PROTECTED)
    • 1번과 2번 같은 의미

📌 JPA 장점

  • JPA 활용 시 엔티티 안에있는 데이터만 변경하면 jpa가 알아서 바뀐 변경 포인트들을 변경내역감지(dirty checking)를 통해 변경된 내역 찾아서 db에 업데이트 쿼리가 자동으로 전달

    • 기존의 mybatis나 jdbc를 이용하여 db에 접속 시 전달 값이 변경되면 변경 sql문을 모두 작성해야함

💡 도메인 모델 패턴 vs 트랜잭션 스크립트 패턴

  • 도메인 모델 패턴

    • 비즈니스 로직 대부분이 엔티티에 있다.
    • 서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할을 한다.
    • 즉, 엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것.
  • 트랜잭션 스크립트 패턴

    • 엔티티에는 비즈니스 로직이 거의 없고 서비스 계층에서 대부분의 비즈니스 로직을 처리

🌼 주문 기능 테스트

  • 테스트 요구사항

    • 상품 주문이 성공해야 한다.
    • 상품을 주문할 때 재고 수량을 초과하면 안 된다.
    • 주문 취소가 성공해야 한다.
  • 예상값과 실제값 불일치시 test fail

package jpabook.jpashop.service;

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class OrderServiceTest {

    @Autowired EntityManager em;
    @Autowired OrderService orderService;
    @Autowired OrderRepository orderRepository;

    @Test
    public void 상품주문() throws Exception {
        //given
        Member member = createMember();
        Book book = createBook("JPA", 10000, 10);
        int orderCount = 2;

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

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

        Assert.assertEquals("상품 주문시 상태는 ORDER", OrderStatus.ORDER, getOrder.getStatus());
        Assert.assertEquals("주문한 상품 종류 수가 정확해야 한다.", 1, getOrder.getOrderItems().size());
        Assert.assertEquals("주문 가격은 가격 * 수량이다.", 10000 * orderCount, getOrder.getTotalPrice() );
        Assert.assertEquals("주문 수량만큼 재고가 줄어야 한다.", 8, book.getStockQuantity());
    }

    @Test
    public void 주문취소() throws Exception {
        //given
        Member member = createMember();
        Book item = createBook("JPA", 10000, 10);
        int orderCount = 2;
        Long orderId = orderService.order(member.getId(), item.getId(), orderCount);

        //when
        orderService.cancelOrder(orderId);

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

        Assert.assertEquals("주문 취소시 상태는 CANCEL 이다.", OrderStatus.CANCEL, getOrder.getStatus());
        Assert.assertEquals("주문이 취소된 상품은 그만큼 재고가 증가해야 한다.", 10, item.getStockQuantity());
    }

    @Test(expected = NotEnoughStockException.class)
    public void 상품주문_재고수량초과() throws Exception {
        //given
        Member member = createMember();
        Item item = createBook("JPA", 10000, 10);
        int orderCount = 11; //재고수량 초과로 예외 발생

       //when
        orderService.order(member.getId(), item.getId(),orderCount);

        //then
        Assert.fail("재고 수량 부족 예외가 발생해야 한다.");
    }

    private Book createBook(String name, int price, int stockQuantity) {
        Book book = new Book();
        book.setName(name);
        book.setPrice(price);
        book.setStockQuantity(stockQuantity);
        em.persist(book);
        return book;
    }

    private Member createMember() {
        Member member = new Member();
        member.setName("홍길동");
        member.setAddress(new Address("서울", "강남구", "12345"));
        em.persist(member);
        return member;
    }
}

📘 JPQL 조인

  1. 내부/외부조인 (둘 이상의 테이블에 존재하는 공통 속성의 값이 같은 것을 결과로 추출)
    • SELECT m FROM Member m [INNER/OUTER] JOIN m.team t WHERE t.name = :teamName
    • 회원(별칭:m)을 검색하고 회원이 가지고 있는 연관 필드로 팀(별칭:t)과 조인한다.(조건 : team이름이 parameter값)
  1. 세타 조인 : 동등조인이면서 동시에 sql에서 join구문 없이 사용하는 것 (서로 연관관계가 없을 때 사용)
    • 동등 조인(equi join) : 양쪽 테이블에서 조인 조건이 일치하는 행
    • select count(m) from Member m, Team t where m.username = t.name
  1. ON 절 (조인 대상 필터링)
    • SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
    • 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
  2. ON 절 (연관관계 없는 엔티티 외부 조인)
    • SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
    • 회원의 이름과 팀의 이름이 같은 대상 외부 조인

🌼 주문 검색 기능 개발

   //검색결과 조회
    public List<Order> findAll(OrderSearch orderSearch){
        return 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)//최대 1000건까지 조회
                .getResultList();
    }

    //JPA criteria를 이용한 동적 쿼리(권장하지 않음 Querydsl을 권장)
    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();
    }

📌 QueryDSL

  • 정적 타입을 이용해서 SQL 등의 쿼리를 생성해주는 프레임워크

  • 장점

    • 문자가 아닌 코드로 쿼리를 작성함으로써, 컴파일 시점에 문법 오류를 쉽게 확인할 수 있다.
    • 자동 완성 등 IDE의 도움을 받을 수 있다.
    • 동적인 쿼리 작성이 편리하다.
    • 쿼리 작성 시 제약 조건 등을 메서드 추출을 통해 재사용할 수 있다.
public List<Order> findAll(OrderSearch ordersearch){

	QOrder order = QOrder.order;
    QMember member = QMember.member;
    
    return query
    	.select(order)
        .from(order)
        .join(order.member, member)
        .where(statusEq(orderSearch.getOrderStatus()),
        	nameLike(orderSearch.getMembername()))
        .limit(1000)
        .fetch();
}

profile
Study Log 📂

0개의 댓글