스프링

 

 

초급자를 위해 준비한
[웹 개발, 백엔드] 강의입니다.

실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다.

✍️
이런 걸
배워요!

스프링 부트와 JPA를 활용해서 실무에서 자바 웹 애플리케이션을 개발할 수 있습니다.

스프링 부트와 JPA를 활용하는 최적의 방법을 이해합니다.

도메인 모델을 이해하고 설계할 수 있습니다.

도메인 주도 설계를 이해합니다.

 

 

강의 :

https://www.inflearn.com/course/lecture?courseSlug=스프링부트-JPA-활용-1&unitId=24769&tab=curriculum

 

 

수업자료 : 

https://github.com/braverokmc79/jpa-basic-lecture-file/blob/main/실전!%20스프링%20부트와%20JPA%20활용1.pdf

 

강의 소스 코드 :

https://github.com/braverokmc79/jpashop-v20210728

 

 

소스코드

 

https://github.com/braverokmc79/spring-boot-and-jpa-jpabook-practice1

 

 

 

 

[6] 주문 도메인 개발

 

 

 

19.주문, 주문상품 엔티티 개발

 

강의 :

https://www.inflearn.com/course/lecture?courseSlug=스프링부트-JPA-활용-1&unitId=24297&tab=curriculum

 

 

Order   주문 엔티티 개발

package jpabook.jpashop.domain;

import jdk.jfr.Timestamp;
import lombok.Getter;
import lombok.Setter;
import org.aspectj.weaver.ast.Or;
import org.hibernate.annotations.Comment;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name="orders")
@Getter
@Setter
public class Order {

    @Id
    @GeneratedValue( strategy = GenerationType.IDENTITY)
    @Column(name="order_id")
    private Long id;

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

    //CascadeType.ALL 는 order
//    persist(orderItemA);
//    persist(orderItemA);
//    persist(orderItemB);
//    persist(orderItemB);
//    persist(order);

   // =>persist를 각각 해줘야 하는데 CascadeType.ALL  적용하면  persist(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;

    @Comment("주문시간")
    private LocalDateTime orderDate; //주문시간

    @Comment("주문상태")
    @Enumerated(EnumType.STRING)
    private OrderStatus status;//주문상태 [ORDER, CANCEL]



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

    //Order  + OrderItem 엔티티 영속성 객체 주입
    public void addOrderItem(OrderItem orderItem){
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }


    public void setDeliver(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.setDeliver(delivery);

       for(OrderItem orderItem : orderItems){
           //엔티티 영속성 객체 주입
           order.addOrderItem(orderItem);
       }
       order.setStatus(OrderStatus.ORDER);
       order.setOrderDate(LocalDateTime.now());

       return  order;
    }



    //===비즈니스 로직 ==//
    /** 주문 취소 */
    public void cancel(){
        //데이터만 바뀌면 JPA 가 더티 체킹으로 알아서 update 처리를 해준다.
        if(delivery.getStatus()==DeliveryStatus.COMP){
            throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
        }
        this.setStatus(OrderStatus.CANCEL);
        for(OrderItem orderItem :this.orderItems){
            orderItem.cancel();
        }
    }

    /** 전체 주문 가격 조회 */
    //==조회 로직 ==//
    public int getTotalPrice(){
        return orderItems.stream().mapToInt(OrderItem::getTotalPrice).sum();
    }

//    public int getTotalPrice(){
//        int totalPrice=0;
//        for(OrderItem orderItem :orderItems){
//            totalPrice += orderItem.getTotalPrice();
//        }
//        return  totalPrice;
//    }



}

 

 

기능 설명


생성 메서드( createOrder() ): 주문 엔티티를 생성할 때 사용한다. 주문 회원, 배송정보, 주문상품의
정보를 받아서 실제 주문 엔티티를 생성한다.


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


전체 주문 가격 조회: 주문 시 사용한 전체 주문 가격을 조회한다. 전체 주문 가격을 알려면 각각의 주문상품
가격을 알아야 한다. 로직을 보면 연관된 주문상품들의 가격을 조회해서 더한 값을 반환한다. (실무에서는 주로 주문에 전체 주문 가격 필드를 두고 역정규화 한다.)

 

 

 

 

OrderItem      주문상품 엔티티 코드

package jpabook.jpashop.domain;

import jpabook.jpashop.domain.item.Item;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Getter
@Setter
public class OrderItem {


    @Id
    @GeneratedValue( strategy = GenerationType.IDENTITY)
    @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;
    }


    //==비즈니스 로직 OrderItem 취소==//
    public void cancel() {
        getItem().addStock(this.count);
    }

    //==조회 로직==//\

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

 

 

 

 

기능 설명


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


주문 취소( cancel() ): getItem().addStock(count) 를 호출해서 취소한 주문 수량만큼 상품의 재고를 증가시킨다.


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

 

 

 

 

 

 

 

 

 

20.주문 리포지토리 개발

 

강의 :

https://www.inflearn.com/course/lecture?courseSlug=스프링부트-JPA-활용-1&unitId=24298&tab=curriculum

 

주문 리포지토리에는 주문 엔티티를 저장하고 검색하는 기능이 있다. 마지막의 findAll(OrderSearch

orderSearch) 메서드는 조금 뒤에 있는 주문 검색 기능에서 자세히 알아보자.

 

OrderRepository   주문 리포지토리 코드

package jpabook.jpashop.repository;

import jpabook.jpashop.domain.Order;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;

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


   // public  List<Order> findAll(OrderSearh orderSearch){}

}

 

 

 

 

 

 

 

 

 

 

 

21.주문 서비스 개발

 

강의 :

https://www.inflearn.com/course/lecture?courseSlug=스프링부트-JPA-활용-1&unitId=24299&tab=curriculum

 

 

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;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class OrderService {

    private final MemberRepository memberRepository;
    private final ItemRepository itemRepository;
    private final OrderRepository orderRepository;


    /** Order 객체 에서  orderItems  ,  delivery 가
     * cascade = CascadeType.ALL 적용 되어 있어서 현재 영속성 주입인  save 할 필요가 없는데,
     * CascadeType.ALL 의 사용은 현재 orderItems 이나 delivery 가 Order 에서만
     * 사용하고 있어서 가능하다. 만약에 다른 객체에서도 사용하고 있다면,
     * CascadeType.ALL 을 사용해서는 안된다.
     */

    /**주문 **/
    @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);

        // 다음과 같이 기본생성 자로 주문상품을 생성할 가능성이 있기 때문에
        //protected 처릴르 하여  막는다.
        //OrderItem orderItem1 =new OrderItem();
        //@NoArgsConstructor(access = AccessLevel.PROTECTED)

        //주문 생성
        //Order  @NoArgsConstructor(access = AccessLevel.PROTECTED)
        Order order = Order.createOrder(member, delivery, orderItem);



        //주문 저장
        //주문을 저장할때  cascade = CascadeType.ALL 옵션이 있기 때문에
        //delivery 와  orderItem 은 자동으로 함게 persist 되면서 DB 에 함께 저장 처리 된다.
        orderRepository.save(order);

        return  order.getId();
    }


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


    //검색
/*
    public List<Order> findOrders(OrderSearch orderSearch){
        return  orderRepository.findAll(orderSearch);
    }
*/

}

 

 

 

주문 서비스는 주문 엔티티와 주문 상품 엔티티의 비즈니스 로직을 활용해서 주문, 주문 취소, 주문 내역
검색 기능을 제공한다.

 


참고: 예제를 단순화하려고 한 번에 하나의 상품만 주문할 수 있다.


주문( order() ): 주문하는 회원 식별자, 상품 식별자, 주문 수량 정보를 받아서 실제 주문 엔티티를 생성한 후 저장한다.


주문 취소( cancelOrder() ): 주문 식별자를 받아서 주문 엔티티를 조회한 후 주문 엔티티에 주문 취소를 요청한다.


주문 검색( findOrders() ): OrderSearch 라는 검색 조건을 가진 객체로 주문 엔티티를 검색한다. 자세한 내용은 다음에 나오는 주문 검색 기능에서 알아보자.

 

 

 

 

도메인 모델 패턴

참고: 주문 서비스의 주문과 주문 취소 메서드를 보면 비즈니스 로직 대부분이 엔티티에 있다. 서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할을 한다. 이처럼 엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것을 도메인 모델 패턴(http://martinfowler.com/eaaCatalog/domainModel.html)이라 한다.

 

 

트랜잭션 스크립트 패턴

반대로 엔티티에는 비즈니스 로직이 거의 없고 서비스 계층에서 대부분의 비즈니스 로직을 처리하는 것을 트랜잭션 스크립트 패턴(http://martinfowler.com/eaaCatalog/transactionScript.html)이라 한다.

 

 

 

 

 

 

 

 

 

 

 

22.주문 기능 테스트

 

강의 :

https://www.inflearn.com/course/lecture?courseSlug=스프링부트-JPA-활용-1&unitId=24300&tab=curriculum

 

주문 기능 테스트

 


테스트 요구사항
상품 주문이 성공해야 한다.
상품을 주문할 때 재고 수량을 초과하면 안 된다.
주문 취소가 성공해야 한다.

 

 

상품 주문 테스트 코드
상품주문이 정상 동작하는지 확인하는 테스트다. Given 절에서 테스트를 위한 회원과 상품을 만들고
When 절에서 실제 상품을 주문하고 Then 절에서 주문 가격이 올바른지, 주문 후 재고 수량이 정확히
줄었는지 검증한다.

 


재고 수량 초과 테스트
재고 수량을 초과해서 상품을 주문해보자. 이때는 NotEnoughStockException 예외가 발생해야 한다.

 

 

재고 수량 초과 테스트 코드
코드를 보면 재고는 10권인데 orderCount = 11 로 재고보다 1권 더 많은 수량을 주문했다. 주문 초과로
다음 로직에서 예외가 발생한다.

 


주문 취소 테스트
주문 취소 테스트 코드를 작성하자. 주문을 취소하면 그만큼 재고가 증가해야 한다

 

 

주문 취소 테스트 코드
주문을 취소하려면 먼저 주문을 해야 한다. Given 절에서 주문하고 When 절에서 해당 주문을 취소했다.
Then 절에서 주문상태가 주문 취소 상태인지( CANCEL ), 취소한 만큼 재고가 증가했는지 검증한다.

 

 

 

OrderServiceTest

package jpabook.jpashop.service;

import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderStatus;
import jpabook.jpashop.domain.exception.NotEnoughStockException;
import jpabook.jpashop.domain.item.Book;
import jpabook.jpashop.domain.item.Item;
import jpabook.jpashop.repository.OrderRepository;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;


@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("회원1");
        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());
    }

    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(String name) {
        Member member =new Member();
        member.setName(name);
        member.setAddress(new Address("서울", "경기","123-123"));
        em.persist(member);
        return member;
    }


    @Test
    public void 주문취소() throws Exception{
        //given
        Member member = createMember("회원1");
        Item 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("회원1");
        Item item = createBook("시골 JPA", 10000, 10);
        int orderCount =11;

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

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





}

 

 

 

 

 

 

 

 

 

 

 

23.주문 검색 기능 개발

 

강의 :

https://www.inflearn.com/course/lecture?courseSlug=스프링부트-JPA-활용-1&unitId=24301&tab=curriculum

 

 

 

OrderSearch

package jpabook.jpashop.repository;

import jpabook.jpashop.domain.OrderStatus;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class OrderSearch {

    private String memberName; //회원 이름
    private OrderStatus orderStatus; //주문 상태[ORDER,CANCEL]
}

 

 

 

OrderRepository

package jpabook.jpashop.repository;

import jpabook.jpashop.domain.Order;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import java.util.List;

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


   public List<Order> findAll(OrderSearch orderSearch){

     return em.createQuery("select o  from o join o.member m  " +
                     " where  o.status = :status " +
                     " and m.name like :name ", Order.class)
                .setParameter("status", orderSearch.getOrderStatus())
                .setParameter("name", orderSearch.getMemberName())
                //.setFirstResult(10)
                .setMaxResults(1000) //최대 1000건
                .getResultList();
   }
}

 

 

JPA Criteria는 JPA 표준 스펙이지만 실무에서 사용하기에 너무 복잡하다. 결국 다른 대안이 필요하다.


많은 개발자가 비슷한 고민을 했지만, 가장 멋진 해결책은 Querydsl이 제시했다. Querydsl 소개장에서
간단히 언급하겠다. 지금은 이대로 진행하자.


> 참고: JPA Criteria에 대한 자세한 내용은 자바 ORM 표준 JPA 프로그래밍 책을 참고하자

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

옳은 행동을 하고 남보다 먼저 모범을 보이는 것이 교육이라는 것이다. -순자

댓글 ( 4)

댓글 남기기

작성