//==연관 관계 메서드==//
// 연관 관계 편의를 위해 하나의 메소드로 묶어버림
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);
}
연관 관계를 묶어버리는 메소드들
Order는 Member, Delivery의 객체들을 가지고 있어야하니까
this.xx로 들어오는 객체들 지정
Member와는 1:N 관계, 하나의 멤버가 여러 개의 Order를 가질 수 있으니
member의 필드에 order 객체 추가
Delivery는 1:1 관계, 서로 지정해주면 끝
// == 생성 메서드 ==
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;
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;
*/
}
// == 생성 메소드 == //
public static OrderItem createOrder(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 cancel() { // 재고 수량을 되돌리기 위한 로직
getItem().addStock(count);
}
Order가 OrderItem을 관리하도록 설계했습니다.
OrderItem 데이터는 항상 Order에 의해 관리되도록 설계 상 제약
개념상 Order, OrderItem을 하나로 묶고(Aggregate)
Order를 통해서만 OrderItem에 접근하게 강제
이렇게 설계를 하면
이제 OrderItem의 생명주기는 모두 Order에 달림
Order 하나를 통해 OrderItem까지 관리 가능, 이런 lifecycle은 Cascade(상태 전파) 기능을 통해 관리
객체를 생성할 때는 3가지 방법중 하나를 사용
생성자
정적 팩토리 메서드
Builder 패턴
엔티티에 따라 이 방법중 상황에 따라서 하나를 선택하고, 파라미터에 객체 생성에 필요한 데이터를 다 넘기는 방법을 사
그러면 setter가 없는데 엔티티를 어떻게 수정할까요?
private OrderItem(Item item, int orderPrice, int count) {
this.item = item;
this.orderPrice = orderPrice;
this.count = count;
item.removeStock(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;
}
@Builder
private OrderItem(Item item, int orderPrice, int count) {
this.item = item;
this.orderPrice = orderPrice;
this.count = count;
item.removeStock(count);
}
public static OrderItem createOrderItem2(Item item, int orderPrice, int count) {
OrderItem orderItem = new OrderItem();
orderItem.mappingItem(item);
orderItem.mappingOrderPrice(orderPrice);
orderItem.mappingCount(count);
item.removeStock(count);
return orderItem;
}
public static OrderItem createOrderItem(Item item, int orderPrice, int count) {
OrderItem orderItem = OrderItem.builder()
.item(item)
.orderPrice(orderPrice)
.count(count)
.build();
item.removeStock(count);
return orderItem;
}
@Builder
private OrderItem(Item item, int orderPrice, int count) {
this.item = item;
this.orderPrice = orderPrice;
this.count = count;
}
굳이 왜 이렇게 나눌까?
@Builder
private OrderItem(Item item, int orderPrice, int count) {
this.item = item;
this.orderPrice = orderPrice;
this.count = count;
item.removeStock(count);
}
생성자에 바로 로직을 넣을 수도 있는데?
마지막에 고민하셨던 것 처럼 정적 팩토리 메서드 없이 생성자를 통해서 바로 처리하셔도 충분합니다.
//==비즈니스 로직==//
public void cancel() {
getItem().addStock(count);
}
public int getTotalPrice() {
return getOrderPrice() * getCount();
}
그런데! 사실은 객체 내부에서 필드에 직접 접근하는가, 아니면 getter를 통해서 접근하는가가 JPA 프록시를 많이 다루게 되면 중요해집니다. 일반적으로 이런 상황을 겪을일은 거의 없지만, 조회한 엔티티가 프록시 객체인 경우 필드에 직접 접근하면 원본 객체를 가져오지 못하고 프록시 객체의 필드에 직접 접근해버리게 됩니다. 이게 일반적인 상황에는 문제가 없는데, equals, hashcode를 JPA 프록시 객체로 구현할 때 문제가 될 수 있습니다.
프록시 객체의 equals를 호출했는데 거기서 필드에 직접 접근하면, 프록시 객체는 필드에 값이 없으므로 항상 null이 반환됩니다. 그래서 JPA 엔티티에서 equals, hashcode를 구현할 때는 getter를 내부에서 사용해야 합니다.
방금 말씀드린 내용은 JPA 고급이어서 조금 어려울 수 있는데, 더 자세한 내용은 JPA 책 15.3.3 프록시 동등성 비교에 잘 정리되어 있습니다
JPA는 동시성 문제를 해결하기 위해 낙관적 락과 비관적 락 2가지 방식을 제공합니다.
이 부분에 대한 자세한 내용은 자바 ORM표준 JPA 프로그래밍 책 16.1 트랜잭션과 락 부분을 참고해주세요
주문 수량만큼 재고가 감소되어야 한다 expected:<8> but was:<298>
Expected :8
Actual :298
<Click to see difference>
java.lang.AssertionError: 주문 수량만큼 재고가 감소되어야 한다 expected:<8> but was:<298>
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failNotEquals(Assert.java:834)
at org.junit.Assert.assertEquals(Assert.java:645)
at jpabook.jpashop.service.OrderServiceTest.상품주문(OrderServiceTest.java:62)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
동적 파라미터가 아닐 때
public List<Order> findAll(OrderSearch orderSearch) {
return em.createQuery("select o from Order o join o.member m"
+ " where o.status = :status "
+ " and m.username like :username", Order.class)
.setParameter("status", orderSearch.getOrderStatus())
.setParameter("username", orderSearch.getMemberName())
.setMaxResults(1000) // 최대 1000 건 조회
.getResultList();
}
코드를 입력하세요