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

[Toy - Smart] OrderItem 리팩토링

by 사서T 2023. 2. 23.

OrderItem 살펴보기

OrderItem은 주문에 포함된 주문 제품을 의미하며 주문 제품에 대한 데이터를 저장하기 위해 구현하였다. 특징은 다음과 같다.

 

- Entity 객체이며 Order, Product와 연관관계를 가지고 있다.

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

- createOrderItem, cancel, toInfoDto 메서드가 구현되어 있다.

 

@Entity
@Getter @Setter
@NoArgsConstructor
@ToString
public class OrderItem {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long orderItemId;
    private Integer quantity;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;
    private String deliveryStatus;
    private String size;

    public static OrderItem createOrderItem(OrderItemSaveDto orderItemSaveDto, Product product) {
        OrderItem orderItem = new OrderItem();

        orderItem.setProduct(product);
        orderItem.setSize(orderItemSaveDto.getSize());
        orderItem.setQuantity(orderItemSaveDto.getQuantity());
        orderItem.setDeliveryStatus("대기중");
        return orderItem;
    }

    public void cancel(String stats) {
        this.deliveryStatus = stats;
    }

    public OrderItemInfoDto toInfoDto() {
        return new OrderItemInfoDto(this);
    }
}

 


OrderItem 분석하기

1. Anntation

 

- @Entity

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

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

 

- @Getter

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

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

 

- @Setter

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

분석 : 필드 값을 초기화하기 위해 @Setter가 사용되었지만 이는 필드 값 수정에 대한 접근 제한이 너무 낮아 변경 시점을 제대로 파악하지 못 할 위험성이 존재한다.

 

- @NoArgsConstructor

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

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

 

- @ToString

기능 : 클래스에 선언된 필드를 '변수명=값' 형태의 문자열로 출력한다.

분석 : 해당 Annotation은 Test를 위해서만 사용되고 있다. 빌드시 파일의 용량을 굳이 키울 필요가 없기 때문에 Test에서 해당 기능을 대체할 수 있는 메서드를 구현하고 사용하는 것이 더 적합해 보인다.

 

- @Id

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

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

 

- @GeneratedValue(strategy = GenerationType.IDENTITY)

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

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

 

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

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

분석 :

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

- @JoinColumn(name = "order_id")

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

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

 

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

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

분석

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

- @JoinColumn(name = "product_id")

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

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


2. Field

 

- private Long orderItemId

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

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

 

- private Integer quantity

기능 : 수량을 저장하기 위한 필드이다.

분석 : private는 필드에 대한 접근 제한을 위해 사용하였고, null 구분과 통일성을 위해 Wrapper Integer를 사용하였다. 기본 자료형이 메모리를 더 적게 사용하지만 큰 차이는 없다고 판단되며 이후 @NotNull 등의 Annotation이 적용되거나 null이 들어오지 않는 경우를 다른 layer에서 확실히 보장되는 경우 수정될 가능성이 있다.

 

- private String deliveryStatus

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

분석 : private는 필드에 대한 접근 제한을 위해 사용하였고, 문자열을 저장하기위해 String을 사용하였다.

 

- private String size

기능 : 주문 제품의 사이즈를 저장하기 위한 필드이다.

분석 : private는 필드에 대한 접근 제한을 위해 사용하였다. 'Free' 사이즈가 들어오는 경우와 DB에서 데이터의 의미를 명확하게 하고자 String 타입을 선언하였지만, Enum을 정의하여 사용하는 경우 DB에서의 메모리를 줄일 수 있을 것이다. 문자열 형태를 유지하며 메모리 사용량만 줄이는 방법에 대해서도 고민해보자.

 

- private Order order

기능 : 주문 제품을 포함하는 주문을 저장하기 위한 필드이다.

분석 : private는 필드에 대한 접근 제한을 위해 사용하였다. 연관관계를 제거하는 경우 단순히 Long 타입의 'order_id'를 저장하는 형태로 변경될 수 있다.

 

- private Product product

기능 : 주문 제품과 연관된 제품을 저장하기 위한 필드이다.

분석 : private는 필드에 대한 접근 제한을 위해 사용하였다. 연관관계를 제거하는 경우 단순히 Long 타입의 'product_id'를 저장하는 형태로 변경될 수 있다.


3. Method

 

- public static OrderItem createOrderItem(OrderItemSaveDto orderItemSaveDto, Product product)

기능 : 매개변수로 받은 OrderItemSaveDto와 Product를 이용해 OrderItem 인스턴스를 생성한다.

분석 :

  • 정적 팩토리 메서드를 이용해 인스턴스 단위가 아닌 클래스 단위로 접근하므로 메모리 사용을 줄일 수 있으며 인스턴스 생성의 의도를 명확히 할 수 있다.
  • 현재 내부에서 set을 이용한 초기화는 수정될 필요가 있다.
  • 매개변수와의 의존성이 너무 깊진 않은 지 고민할 필요가 있다.

- public void cancel(String stats)

기능 : OrderItem의 현재 주문 상태를 String으로 들어온 상태 정보로 변경한다.

분석 :

  • cancel 메서드가 '취소','환불'에 대해 모두 처리하고 있는 상태로 refund 기능의 분리가 필요하다.
  • String으로 들어온 정보가 잘못된 경우가 고려되어 있지 않다. 따라서 Enum을 따로 정의하여 사용하는 것이 적합하다.
  • 매개변수 stats는 오타로 status로 명칭 수정이 필요하다.
  • 현재 cancel 메서드 대신 @Setter를 이용해 상태 정보가 변경되고 있다.

- public OrderItemInfoDto toInfoDto()

기능 : OrderItem의 정보를 이용해 OrderItemInfoDto 형태로 변환한다.

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


OrderItem 리팩토링

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

 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderItem {

    private enum Status {
        WAIT, CANCEL, REFUND
    }

    @Id @GeneratedValue
    private Long orderItemId;
    private Integer quantity;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;
    @Enumerated(value = EnumType.STRING)
    private Status deliveryStatus;
    private String size;

    private OrderItem(OrderItemSaveDto dto, Product product) {
        this.quantity = dto.getQuantity();
        this.size = dto.getSize();
        this.product = product;
        this.deliveryStatus = Status.WAIT;
    }

    public static OrderItem createOrderItem(OrderItemSaveDto dto, Product product) {
        return new OrderItem(dto, product);
    }

    void setOrder(Order order) {
        this.order = order;
    }

    public void cancel() {
        this.deliveryStatus = Status.CANCEL;
    }

    public void refund() {
        this.deliveryStatus = Status.REFUND;
    }

    public String getDeliveryStatus() {
        return this.deliveryStatus.name();
    }

    public OrderItemInfoDto toInfoDto() {
        return new OrderItemInfoDto(this);
    }
}

 

1. Annotaion

 

- @ToString

구현된 통합 테스트에선 사용되고 있지 않아 제거하였다. 이후 단위 테스트 구현시 필요한 경우 util 클래스에 toString 기능을 수행하는 메서드를 구현하여 사용할 예정이다.

 

- @NoArgsConstructor(access = AccessLevel.PROTECTED)

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

 

- @GeneratedValue

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

 

- @Setter

단순하게 set을 이용한 필드 초기화는 안정성과 유지 보수에 있어서 비효율적이라 판단하였다. access 옵션을 PRIVATE나 PROTECTED로 설정하여 접근을 제한할 수 있지만, PRIVATE의 경우 Order에서 OrderItem의 order 필드에 대한 접근이 막혀 접근 제어자만 다른 동일한 기능의 메서드가 필요해 중복이 발생하고, PROTECTED의 경우 Order에서 무분별한 접근이 가능해져 Order의 권한이 너무 커지게 된다. 따라서 @Setter를 제거하였다.

 

- @ToString

해당 기능은 테스트에서만 사용되기 때문에 메인 코드에선 필요없다고 판단하여 제거하였다.

 

- @Enumerated(value = EnumType.STRING)

새로 구현한 enum Status 타입의 필드를 숫자가 아닌 문자열로 DB에 저장하기위해 deliveryStatus 필드에 선언하였다.


2. Field

 

- private Status deliveryStatus

기존 String 타입을 새로 구현한 enum Status의 타입으로 변경하였다.


3. Method

 

- public static OrderItem createOrderItem(OrderItemSaveDto orderItemSaveDto, Product product)

@Setter 제거 이후 새로 구현한 private 생성자를 이용해 인스턴스를 생성하고 반환한다.

 

- private OrderItem(OrderItemSaveDto dto, Product product)

기존 set을 이용해 필드를 초기화하던 방식이 불가능해졌기 때문에 private 형태의 생성자를 추가해주었다. 해당 생성자는 OrderItem 인스턴스 생성 시만 접근 가능하기 때문에 무분별한 생성을 방지할 수 있다. 의존성에 대해선 현재 고려하지 않기로 하였다. deliveryStatus 필드는 status enum의 값을 이용해 초기화한다.

 

- void setOrder(Order order)

@Setter 제거 이후 Order에서 OrderItem의 order를 초기화하는 것이 불가능해졌다. 이에 따라 Order에 초기화에 대한 권한 부여와 그 이외에서의 접근을 제한하기 위해 default 형태의 메서드를 추가하였다. 물론 같은 패키지 내에 존재하는 클래스에선 여전히 접근 가능하기 때문에 좀 더 고민할 필요는 있어 보인다.

 

- private enum Status

OrderItem의 상태를 처리하기위해 Status enum을 추가하였다. WAIT, CANCEL, REFUND를 가지며 외부에서 접근할 이유가 없다고 판단하였기 때문에 private를 선언하였다. (enum은 기본적으로 static이다.)

 

- public void cancel()

매개변수로 String status를 받아 초기화하던 방식에서 enum으로 정의한 상태값을 받아 초기화하는 방식으로 변경하였다. 이에 따라 OrderItemService의 cancel, refund 메서드가 수정되었다.

@Transactional
@Override
public void cancel(OrderItemCancelDto dto) {
    orderItemRepository
            .findById(dto.getOrderItemId())
            .orElseThrow(ORDER_ITEM_NOT_FOUND::exception)
            .cancel();
}

 

- public void refund()

취소와 환불 처리를 구분하기위해 refund 메서드를 구현하였다. cancel 메서드와 마찬가지로 enum으로 정의한 상태값을 받아 초기화하는 방식으로 구현하였다.

@Transactional
@Override
public void refund(OrderItemCancelDto dto) {
    orderItemRepository
            .findById(dto.getOrderItemId())
            .orElseThrow(ORDER_ITEM_NOT_FOUND::exception)
            .refund();
}

 

- public String getDeliveryStatus()

enum Status를 클래스 내에 구현함으로써 외부에서의 접근이 불가능하게 되었다. OrderItemInfoDto를 초기화하는 과정에서 getDeliveryStatus 메서드를 호출하는 경우 Status 타입을 반환하기 때문에 기존 String 타입으로 선언되어 있던 필드 초기화가 불가능해진다. OrderItem 클래스에 구현된 toInfoDto 메서드의 매개변수로 status를 전달해도 되지만 통일성을 위해 String 타입으로 반환하는 getDeliveryStatus() 메서드를 구현하였다. 기존 @Getter로 생성되는 메서드와의 기능 중복이 발생하지만 해당 문제에 대해선 이후 Entity - Dto 간의 convertor 구현 시 고려하도록 한다.

 

 

Link

댓글