본문 바로가기
개발서/ToyProject-Smart

[Toy - Smart] Order 리팩토링

by 사서T 2023. 2. 23.

Order 살펴보기

Order는 회원의 주문을 의미하며 주문 제품 리스트와 주문 시간, 주문한 회원의 정보를 저장한다. 특징은 다음과 같다.

 

- Entity 객체이며 Member, OrderItem과 연관관계를 가진다.

- 매개변수 없는 생성자를 가진다.

- createOrder, setMembe, addOrerItem, toInfoDto 메서드를 가진다.

 

@Entity
@Getter
@NoArgsConstructor
@Table(name = "orders")
@EntityListeners(AuditingEntityListener.class)
public class Order {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long orderId;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();
    @CreatedDate
    private LocalDateTime orderDateTime;

    public static Order createOrder(Member member, OrderItem... orderItems) {
        Order order = new Order();

        order.setMember(member);
        for (OrderItem orderItem : orderItems) {
            order.addOrderItem(orderItem);
        }
        return order;
    }

    public void setMember(Member member) {
        this.member = member;
        member.getOrders().add(this);
    }

    public void addOrderItem(OrderItem orderItem) {
        this.orderItems.add(orderItem);
        orderItem.setOrder(this);
    }

    public OrderInfoDto toInfoDto() {
        List<OrderItemInfoDto> orderItemInfoDtoList = new ArrayList<>();

        for (OrderItem orderItem : orderItems)
            orderItemInfoDtoList.add(orderItem.toInfoDto());
        return new OrderInfoDto(this.orderId, orderItemInfoDtoList);
    }
}

Order 분석하기

1. Anntation

 

- @Entity

기능 : 데이터 모델링의 객체로 DB 테이블 생성시 해당 클래스를 참조한다.

분석 : 해당 클래스는 Entity 객체로 사용하기 위해 구현되었기 때문에 적합하다.

 

- @Getter

기능 : 클래스에 선언된 필드에 대해 get 메서드를 자동 생성한다.

분석 : Entity 클래스의 경우 필드에 대한 접근 제한을 위해 private로 선언되었다. 따라서 필드 값에 접근하기 위한 별도의 방법이 필요하며 @Getter를 사용하는 방식을 채택하였다.

 

- @NoArgsConstructor

기능 : 매개변수가 없는 생성자를 자동 생성한다.

분석 : 클래스에서 따로 생성자를 정의하지 않은 경우 디폴트 생성자가 만들어진다. 따라서 굳이 선언할 필요가 없다. 하지만 JPA에서 필요로 하는 기본 생성자를 위해 선언한 경우 access 옵션을 PROTECTED로 설정하여 해당 Entity의 무분별한 생성을 방지할 수 있다.

 

- @Table(name = "orders")

기능 : Entity를 이용해 생성하는 테이블의 설정을 변경할 수 있다.

분석 : Entity를 이용해 테이블을 생성하는 경우 디폴트 테이블명은 클래스명이기 때문에 order로 생성하게 되는데 이 경우 mysql의 명령어인 order by와 구분할 수 없어 SQLGrammarException이 발생하게 된다. 따라서 @Table을 이용해 생성 테이블의 이름을 변경하는 것은 적합하다.

 

- @EntityListeners(AuditingEntityListener.class)

기능 : Entity에 이벤트가 발생한 경우 관련 DB 테이블의 데이터를 조작하는 역할을 한다.

분석 : Order가 생성되거나 데이터가 수정되는 경우 개발자가 직접 DB에 반영하는 것은 실수가 생길 수 있다. 해당 Annotation을 이용해 변경 사항을 자동적으로 업데이트함으로써 보다 안정성을 높일 수 있다.

 

- @Id

기능 : 해당 필드를 테이블의 기본키로 사용한다.

분석 : 기본키는 테이블 내에서 데이터를 구분할 수 있는 유일한 값으로 필수적으로 정의되어야 한다.

 

- @GeneratedValue(strategy = GenerationType.IDENTITY)

기능 : 기본키 생성 방식을 결정하는 Annotation으로 현재 IDENTITY 방식을 사용하고 있다.

분석 : IDENTITY 방식은 Entity를 DB에 저장 후 기본키를 생성하는 방식으로 트랜잭션의 쓰기 지연 기능을 사용할 수 없다. 즉, 영속성 컨텍스트로 등록하기 위해선 insert가 실행되어야 한다. SEQUENCE 방식은 시퀀스를 미리 메모리에 할당하고 DB에 접근없이 할당된 시퀀스를 기본키로 할당하는 방식이다. 이 경우 쓰기 지연 기능을 사용할 수 있지만 메모리를 차지한다는 단점이 존재한다. 각 방식마다 장단점이 존재하며 현재 서비스 상태에선 크게 고려할 점은 아니라고 판단하여 AUTO로 전략을 수정해두는 것이 적합하다고 판단한다.

 

- @ManyToOne(fetch = FetchType.LAZY) - member

기능 : 연관관계를 설정하여 repository를 통할 필요없이 해당 객체에 접근하는 것으로 데이터를 조회할 수 있다. EAGER로 설정한 경우 접근시가 아니라 order 조회시 join을 이용해 조회한다.

분석 :

  • EAGER의 경우 join을 이용해 관련 데이터를 한 번에 조회하기 때문에 효율성 측면에서는 LAZY보다 높지만 작성되는 쿼리문의 구조를 통제할 수 없고 N + 1 문제가 발생할 수 있다.
  • LAZY의 경우 효율성은 떨어지지만 쿼리문의 구조를 통제할 수 있다. 이 경우 repository에 fetch join을 이용해 효율성을 높일 수 있다.
  • 연관관계를 사용하지 않고 repository의 메서드를 이용해 직접 조회한 데이터를 넣는 방식이 더 효율적인 경우도 존재한다. 해당 방법을 이용한 구현도 고려해보자.

- @JoinColumn(name = "member_id")

기능 : join시 사용할 외래키 컬럼명을 지정하고 필드로 추가한다.

분석 : 연관관계 설정시 선언하는 Annotation으로 이후 구현 방식이 변경됨에 따라 제거될 가능성이 있다.

 

- @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) - orderItem

기능 :

  • 연관관계를 설정하여 repository를 통할 필요없이 해당 객체에 접근하는 것으로 데이터를 조회할 수 있다. EAGER로 설정한 경우 접근시가 아니라 order 조회시 join을 이용해 조회한다.
  • mappedBy 옵션을 이용해 외래키를 저장할 테이블을 명확히 한다. (mappedBy로 설정한 테이블이 아닌 테이블이 외래키를 갖는다.)
  • cascade 옵션은 Order에 대한 영속성 처리시 연관관계에 있는 대상에 대해서도 이를 반영한다.

분석 

  • EAGER의 경우 join을 이용해 관련 데이터를 한 번에 조회하기 때문에 효율성 측면에서는 LAZY보다 높지만 작성되는 쿼리문의 구조를 통제할 수 없고 N + 1 문제가 발생할 수 있다.
  • LAZY의 경우 효율성은 떨어지지만 쿼리문의 구조를 통제할 수 있다. 이 경우 repository에 fetch join을 이용해 효율성을 높일 수 있다.
  • 연관관계를 사용하지 않고 repository의 메서드를 이용해 직접 조회한 데이터를 넣는 방식이 더 효율적인 경우도 존재한다. 해당 방법을 이용한 구현도 고려해보자.
  • cascade 옵션을 이용해 연관관계에 있는 대상에 대해서 각각 persist()나 merge()를 수행할 필요없이 한 번의 실행으로 처리가 가능해져 효율적이다.

- @CreatedDate

기능 : Order 인스턴스 생성시 일시를 자동 생성한다.

분석 : 주문 일시를 저장하기위해 직접 메서드를 사용해 초기화할 필요가 없기 때문에 코드가 간결해진다.


2. Field

 

- private Long orderId

기능 : 기본키로 사용하기 위한 필드이다.

분석 : private는 필드에 대한 접근 제한을 위해 사용하였고, null을 확실히 구분하기 위해 Wrapper Long 사용하였다.

 

- private LocalDateTime orderDateTime

기능 : 주문 일시를 저장하기 위한 필드이다.

분석 : Order에 주문 일시가 저장된 이유는 OrderItem 각각에 주문 일시를 저장하는 것보다 효율적이라 판단했기 때문이다. 만약 OrderItem에서 주문 일시가 필요하다면 해당하는 Order를 조회해야 하지만 현재 OrderItem 만 따로 호출하는 로직은 OrderItem의 상태 변화밖에 없어 문제되지 않는다.

 

- private Member member

기능 : 주문과 연관관계에 있는 회원을 저장하는 필드이다.

분석 : 주문 조회의 경우 토큰에서 memberId를 추출하여 조회가 가능하기 때문에 굳이 Order와 Member 사이의 연관관계가 존재할 필요는 없다고 생각된다. memberId에 대한 유효성을 검증하는 로직만 추가된다면 제거가 가능한 필드이다.

 

- private List<OrderItem> orderItems

기능 : 주문과 연관관계에 있는 orderItem들을 저장하는 필드이다.

분석 : 주문을 조회하는 경우 주문 제품의 정보는 거의 필수적으로 조회되어야 한다고 생각한다. 이에 따라 연관관계 설정은 효율적이라고 생각하지만 repository에서 jpa 쿼리를 직접 구현하는 방법도 고려해볼 필요가 있다.


3. Method

 

- public static Order createOrder(Member member, OrderItem... orderItems)

기능 : 매개변수로 받은 Member와 OrderItems를 이용해 Order 인스턴스를 생성한다.

분석 :

  • 정적 팩토리 메서드를 이용해 인스턴스 단위가 아닌 클래스 단위로 접근하므로 메모리 사용을 줄일 수 있으며 인스턴스 생성의 의도를 명확히 할 수 있다.
  • 따로 초기화할 필드가 없어 @Setter를 사용할 필요가 없다.
  • 매개변수와의 의존성이 너무 깊진 않은 지 고민할 필요가 있다.
  • 주문이 생성되면 더 이상 주문 제품이 추가될 수도 포함된 내용들이 변경될 수도 없다. 오직 바뀌는 것은 주문 상태이며 필요없어진 경우 해당 주문을 취소하는 방법뿐이다. 따라서 주문 생성시 생성된 OrderItem을 매개변수로 받는 것이 적합하다.
  • Member는 Order에서 저장할 필요가 없기 때문에 매개변수 Member는 삭제되어야 한다.

- public void setMember(Member member)

기능 : 매개변수로 들어온 Member의 Order 필드를 해당 Order로 초기화 한다.

분석 :

Member가 제거되기 때문에 Member 관련 메서드도 같이 제거되어야 한다.

 

- public void addOrderItem(OrderItem orderItem)

기능 : 매개변수로 받은 OrderItem을 orderItems 리스트에 저장한다.

분석 : 생성자를 통해 OrderItem을 초기화하고 더 이상 추가할 수 없기 때문에 해당 메서드를 제거하고 생성자에서 list.add를 이용하는 것이 적합하다.

 

- public OrderInfoDto toInfoDto()

기능 : Order를 OrderInfoDto 형태로 변환한다.

분석 : OrderInfoDto에서 Order를 받아 처리하는 방식으로도 구현이 가능하다. 두 경우 모두 의존성이 깊기 때문에 이후 Convertor 클래스를 새로 작성해 Dto와 Entity, Dto와 Dto 간의 변환을 처리하도록 구현하는 것을 염두에 두자.


Order 리팩토링

수정된 경우  색, 추가된 경우  색, 삭제된 경우 색으로 표시하였다. 수정된 코드는 다음과 같다.

 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "orders")
@EntityListeners(AuditingEntityListener.class)
public class Order {

    @Id @GeneratedValue
    private Long orderId;
    private Long memberId;
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();
    @CreatedDate
    private LocalDateTime orderDateTime;

    private Order(Long memberId, List<OrderItem> orderItems) {
        this.memberId = memberId;
        for (OrderItem orderItem : orderItems) {
            this.orderItems.add(orderItem);
            orderItem.setOrder(this);
        }
    }

    public static Order createOrder(Long memberId, List<OrderItem> orderItems) {
        return new Order(memberId, orderItems);
    }

    private int totalPrice() {
        int price = 0;

        for (OrderItem orderItem : orderItems)
            price += orderItem.getQuantity() * orderItem.getProduct().getPrice();
        return price;
    }

    public OrderInfoDto toInfoDto() {
        List<OrderItemInfoDto> orderItemInfoDtoList = new ArrayList<>();

        for (OrderItem orderItem : orderItems)
            orderItemInfoDtoList.add(orderItem.toInfoDto());
        return new OrderInfoDto(this.orderId, orderItemInfoDtoList);
    }
}

 

1. Annotaion

 

- @NoArgsConstructor(access = AccessLevel.PROTECTED)

access 옵션을 추가하여 Order의 생성을 제한하였다. 이제 JPA와 상속 대상만 사용이 가능하며, Order의 생성은 정적 메서드인 createOrde를 통해서만 가능해졌다.

 

- @GeneratedValue

현재 상태에선 시퀀스 효율성에 대해서 배제하고 진행하기로 하였다. 따라서 strategy 옵션을 제거하였다.

 

- @ManyToOne(fetch = FetchType.LAZY)

Order에서 Member의 정보를 저장할 필요가 없기 때문에 제거하였다.

 

- @JoinColumn(name = "member_id")

Order에서 Member의 정보를 저장할 필요가 없기 때문에 제거하였다.


2. Field

 

- private Member member

Order에서 Member와의 연관관계를 저장할 필요가 없기 때문에 제거하였다.

 

- private Long memberId

Order를 주문한 회원이 누구인 지는 구분이 가능해야 하기 때문에 회원의 기본키를 넣어주었다.


3. Method

 

- private Order(Long memberId, List<OrderItem> orderItems)

매개변수로 받은 리스트를 이용해 Order와 OrderItem 간의 연관관계를 설정하고 외래키로 memberId를 저장한다.

 

- private int totalPrice()

가격의 총합은 OrderItem을 리스트로 가지고 있는 Order에서 처리해야 적절하다고 생각한다. 만약 가격에 대한 할인 로직이 추가되는 경우 private 접근 제어자를 수정을 고려해야 한다.

 

- public static Order createOrder(Long memberId, List<OrderItem> orderItems)

외래키로 memberId를 저장하고 새로 구현된 생성자를 이용해 생성한 order를 반환하는 기능을 한다.

 

- public void setMember(Member member)

Order에서 Member의 정보를 저장할 필요가 없기 때문에 제거하였다.

 

- public void addOrderItem(OrderItem orderItem)

주문은 생성된 후에 더 이상 주문 제품이나 정보를 수정할 수 없다. 기존 orderItem을 추가하는 방식도 Order에 정의된 orderItems.add를 이용하기 때문에 필요없다고 판단하였다.

 

@Transactional
@Override
public Long save(Long memberId, OrderSaveDto dto) {
    memberRepository.findById(memberId).orElseThrow(MEMBER_NOT_FOUND::exception);

    List<OrderItem> orderItems = new ArrayList<>();

    dto.getOrderItemSaveDtoList()
            .forEach(oi -> {
                Product product = productRepository.findById(oi.getProductId())
                        .orElseThrow(PRODUCT_NOT_FOUND::exception);
                orderItems.add(OrderItem.createOrderItem(oi, product));
            });

    Order order = Order.createOrder(memberId, orderItems);

    return orderRepository.save(order).getOrderId();
}

 

 

 

Link

댓글