@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;
}
여러개 받아올 수 있을때, ...
을 붙혀주면 가능함!!
- 생성 메소드로만 인스턴스 생성이 가능하도록 하고 싶으면!!
protected
protected Order(){}
이렇게 생성자를 protected 로 지정해두면, 생성 메소드안에서만 가능하게 됨!
@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;
}
@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는 운영 코드와 완전히 분리되어 비즈니스 로직들만 테스트 할 수 있는 것!!
지금의 경우, 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
을 쓰는 것을 강추