레포지토리 패턴 적용, 주문 도메인 개발

dropKick·2020년 8월 12일
0

엔티티

Order

   //==연관 관계 메서드==//
    // 연관 관계 편의를 위해 하나의 메소드로 묶어버림
    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();
        }
  • 주문을 취소하는 메소드에서 특이한 점
    모든 객체에 cancel() 이라는 상태 메소드를 호출
    1:N 관계이기 때문에 Order에서는 상태 호출만 넘김
// == 조회 로직 == //
    // 전체 주문 가격 조회
    public int getTotalPrice() {
        // 스트림
        return orderItems.stream()
                .mapToInt(OrderItem::getTotalPrice)
                .sum();

        /* 사용하지 않았을 때
        int totalPrice = 0;
        for (OrderItem orderItem : orderItems) {
            totalPrice += orderItem.getTotalPrice();
        }
        return totalPrice;
         */
    }
  • 스트림의 사용
    스트림을 사용하지 않으면 별도의 변수를 생성하고 각 값을 꺼내온 뒤 더해야 함
    하지만 스트림 사용 시 mapToInt를 통해 getTotalPrice() 결과값 int들을 전부 모아 Stream으로 반환하고 이를 더하면 끝

OrderItem

   // == 생성 메소드 == //
    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;
    }
  • 실제 주문 시 할인 등에 의해 가격이 변동될 수 있기에 별도의 orderPrice를 둠
    주문이 만들어진다는 건 물건이 감소하다는 것, 수량 감소 로직
public void cancel() { // 재고 수량을 되돌리기 위한 로직
        getItem().addStock(count);
    }
  • 주문이 취소되면 수량이 복원되야 함
    주문에 따라 여러 물건이 있을 수 있으니 각각 cancel

너무 좋은 질문들

왜 Repository를 사용하지 않았나요?

  • 왜 엔티티마다 레포지토리를 만들지 않았을까?
  • 왜 OrderItem은 Order 엔티티에 의해 관리될까?

Order가 OrderItem을 관리하도록 설계했습니다.
OrderItem 데이터는 항상 Order에 의해 관리되도록 설계 상 제약
개념상 Order, OrderItem을 하나로 묶고(Aggregate)
Order를 통해서만 OrderItem에 접근하게 강제

이렇게 설계를 하면

  • 외부에서는 Order, OrderItem 중에 Order만 알면 되기 때문에 도메인을 좀 더 덜 복잡하게 설계
  • 이렇게 그룹을 대표하는 엔티티를 도메인 주도 설계(DDD)에서는 aggregate root(에그리게잇 루트) 엔티티라고 함

이제 OrderItem의 생명주기는 모두 Order에 달림
Order 하나를 통해 OrderItem까지 관리 가능, 이런 lifecycle은 Cascade(상태 전파) 기능을 통해 관리

생성 메소드와 정적 팩토리, 빌더 패턴

엔티티에는 setter가 존재하면 안된다고 함, 실무에서는 setter를 어떻게 대체해서 사용하는지?

객체를 생성할 때는 3가지 방법중 하나를 사용

  • 생성자

  • 정적 팩토리 메서드

  • Builder 패턴

엔티티에 따라 이 방법중 상황에 따라서 하나를 선택하고, 파라미터에 객체 생성에 필요한 데이터를 다 넘기는 방법을 사

  • 정적 팩토리 메서드나, Builder 패턴을 사용할 때는 생성자를 private 처리
  • 객체 생성이 간단할 때는 단순히 생성자를 사용
  • 만약 객체 생성이 복잡하고, 의미를 가지는 것이 좋다면 나머지 방법 중 하나를 선택

그러면 setter가 없는데 엔티티를 어떻게 수정할까요?

  • setter를 만들기 보다는 의미있는 변경 메서드 이름을 사용
    예를들어서 고객의 등급이 오른다면 member.levelUp() 같은 의미가 부여된 내부 필드값 변경 메소드를 만들어 사용

생성자, 정적 팩토리, 빌더, 변경 메소드

별도의 글 작성, 객체를 생성하는 패턴들

생성자
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);
}

생성자에 바로 로직을 넣을 수도 있는데?

마지막에 고민하셨던 것 처럼 정적 팩토리 메서드 없이 생성자를 통해서 바로 처리하셔도 충분합니다.

  • 핵심은 정적 팩토리, 단순 생성자, 빌더, 어떤것을 사용하든 상관없음
  • 중요한 것은 이렇게 생성자에 파라미터를 넘기는 기법을 사용해서, 변경이 필요없는 필드에 추가적인 setter를 외부에 노출하는 것을 줄이는 것이 핵심
  • 생성 이후 변경이 필요 없는데 setter가 노출 되어 있다면 혼란, 없다면 변경하지 말아야 한다는 것을 알 수 있음
  • 외부에 어떤 것을 공개할지 객체 생성을 목적으로 하는 것이기 때문에 외부에 한가지 방식만 제공
  • new 생성자 안에 비즈니스 로직을 직접 집어넣는 것은 관례 상 좋지않음
    new 생성자는 객체를 생성하는데 필요한 역할만 담당해야 함
    정적 팩토리 메소드나 외부 service 클래스에 비즈니스 로직 호출을 넘기는 방식으로 처리
  • setter를 어느정도는 허용가능
    연관관계를 양방향으로 적용하시려면 필연적으로 들어가야 할 때도 있음
    하지만 관리가 잘 되어야 함

필드에 직접 접근 vs. getter 사용

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

public int getTotalPrice() {
    return getOrderPrice() * getCount();
}
  • 객체 외부에서는 당연히 필드에 직접 접근하면 안되겠지만, 객체 내부에서는 필드에 직접 접근해도 아무 문제가 없습니다.
  • 번거롭게 getXxx를 호출하는 것 보다는 필드를 직접 호출하는 것이 코드도 더 깔끔하고요.

그런데! 사실은 객체 내부에서 필드에 직접 접근하는가, 아니면 getter를 통해서 접근하는가가 JPA 프록시를 많이 다루게 되면 중요해집니다. 일반적으로 이런 상황을 겪을일은 거의 없지만, 조회한 엔티티가 프록시 객체인 경우 필드에 직접 접근하면 원본 객체를 가져오지 못하고 프록시 객체의 필드에 직접 접근해버리게 됩니다. 이게 일반적인 상황에는 문제가 없는데, equals, hashcode를 JPA 프록시 객체로 구현할 때 문제가 될 수 있습니다.

프록시 객체의 equals를 호출했는데 거기서 필드에 직접 접근하면, 프록시 객체는 필드에 값이 없으므로 항상 null이 반환됩니다. 그래서 JPA 엔티티에서 equals, hashcode를 구현할 때는 getter를 내부에서 사용해야 합니다.

방금 말씀드린 내용은 JPA 고급이어서 조금 어려울 수 있는데, 더 자세한 내용은 JPA 책 15.3.3 프록시 동등성 비교에 잘 정리되어 있습니다

멀티 스레드 문제

  • 여러 스레드에서 addStock 또는 removeStock method 실행시 stock 수가 변경할때 동시성 문제가 발생

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();
    }
코드를 입력하세요

0개의 댓글