연관관계 편의 메서드 그리고 Cascade

나민혁·2024년 9월 10일

들어가며

이번에도 리뷰받은 내용을 리팩토링하면서 겪은 문제이다. 그리고 또 무지하게 그냥 되던데? 식으로 넘어갈뻔했다. 정확하게 Cascade와 연관관계 편의 메서드를 알아보자

이번에도 좋은 리뷰를 달아주셨다. 그런데 처음에는 정확하게 무슨 말인지 못알아먹었고.. 그리고 다시 물어봤을 때 연관관계 편의 메서드에 대해서 알아보는게 좋을 것 같다고 말씀하셔서 공부해보았다.

내가 이해한 연관관계 편의메서드는 "JPA가 ORM이기 때문에 데이터베이스를 떼어놓고 봐도 객체관점에서 생각해보았을 때 자식에게도 추가 되어야 된다" 라고 이해했다. 김영한 강사님의 강의에서 나온다고 하나 사서 들은건 아니고 블로그를 열심히 찾다보니 정리한 내용이 이러했다.

리팩토링

리팩토링 시작

내 코드 중 리팩토링 대상이 되어야 할 코드였다.

Order.java

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

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "order_id")
    private Long id;

    @Column(nullable = false, length = 50)
    private String email;

    @Embedded
    private Address address;

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL , orphanRemoval = true)
    private List<OrderProduct> orderProducts = new ArrayList<>();

    @Builder
    private Order(String email, String address, String postcode, List<Product> products, Map<Long, Integer> orderProducts) {
        this.email = email;
        this.address = Address.builder()
            .address(address)
            .postcode(postcode)
            .build();
        this.orderStatus = OrderStatus.ORDERED;
        this.orderProducts = products.stream()
            .map(product -> new OrderProduct(this, product, orderProducts.get(product.getId())))
            .collect(Collectors.toList());
    }
    
    public void updateStatus(OrderStatus orderStatus) {
        this.orderStatus = orderStatus;
    }
}

OrderProduct.java

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "order_items")
public class OrderProduct extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "seq")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    private Product product;

    @Column(nullable = false)
    private int quantity;

    public OrderProduct(Order order, Product product, int quantity) {
        this.order = order;
        this.product = product;
        this.quantity = quantity;
    }
}
// OrderService.java
public OrderResponse createOrder(OrderCreateServiceRequest request) {
        Set<Long> productIds = request.getOrderProductQuantity().keySet();
        List<Product> products = productRepository.findAllById(productIds);
        if(productIds.size() != products.size()) {
            throw new IllegalArgumentException("주문 상품 id 중 존재하지 않는 상품이 존재합니다.");
        }
        Order order = Order.builder()
            .email(request.getEmail())
            .address(request.getAddress())
            .postcode(request.getPostcode())
            .orderProducts(request.getOrderProductQuantity())
            .products(products)
            .build();
        Order savedOrder = orderRepository.save(order);

        return OrderResponse.of(savedOrder);
    }

Builder를 통해 생성하고 new를 통해 orderproduct를 만들고 있었기 때문에 당연히 서비스에서 orderRepository.save(order)를 할 때 저장이 된다고 생각했다.

그리고 실제로 그렇게 되고있었다.

작성한 테스트 모두 통과하였고, 그리고 h2 console을 통해서 확인했을 때도 제대로 들어갔다.

당연히 지금 작성한 코드에 문제는 없지만 연관관계 편의 메서드를 학습해보기 위해서 리팩토링하였다.

리팩토링 중 만난 문제

cascade

엔티티 내부에 연관관계 편의 메서드를 작성해야 했으므로 코드를 내리다보니 CASCADE가 ALL이 걸려있는게 마음에 걸렸다. 지양해야한다고 들었던 기억이있어서 REMOVE로 바꾸고 리팩토링을 진행했다.

솔직히 말하면 나중에 1차 리팩토링이 모두 진행되고나서 알게 된 내용이었지만 지금 짚고가는게 좋다고 생각해서 앞단에 작성한다.

또 이걸 작성하면서 깨달은게 joincolumn을 지정을 안해서 외래키 이름이 자기 맘대로 생겼다.. 수정해야겠다. 하여튼 left join으로 보면 order_items 테이블에는 아무것도 안들어간것을 볼 수 있다. 나는 단순히 @OneToMany(mappedBy = "order", cascade = CascadeType.REMOVE) cascadeType만 ALL 에서 REMOVE로 바꿨을 뿐이다. cascade에 대해 잘 모르고 사용했던 것이다. cascade에 대한 내용은 뒤에 후술하려고 한다.

테스트의 함정

내가 cascade가 문제점이라고 생각하지 못한 이유가 있다.

테스트가 아무일도 없다는 듯이 통과한다.. 테스트가 제대로 서비스를 커버하고있지 못했던 것이다.

그래서 서비스레이어에서 order_items 테이블에 데이터가 들어갔는지 확인하는 테스트코드를 추가했다.

@DisplayName("상품을 주문한다.")
    @Test
    void createOrder() {
        //given
        Product product1 = Product.builder()
            .name("스타벅스 원두")
            .category("원두")
            .price(50000L)
            .description("에티오피아산")
            .build();

        Product product2 = Product.builder()
            .name("스타벅스 라떼")
            .category("음료")
            .price(3000L)
            .description("에스프레소")
            .build();

        productRepository.saveAll(List.of(product1, product2));

        OrderCreateServiceRequest request = OrderCreateServiceRequest.builder()
            .email("test@gmail.com")
            .address("서울시 강남구")
            .postcode("125454")
            .orderProductQuantity(
                List.of(
                    OrderProductQuantity.builder()
                        .productId(1L)
                        .quantity(1)
                        .build(),
                    OrderProductQuantity.builder()
                        .productId(2L)
                        .quantity(2)
                        .build()
                )
            )
            .build();

        //when
        OrderResponse response = orderService.createOrder(request);

        List<Order> orders = orderRepository.findAll();

        List<OrderProduct> orderProducts = orderProductRepository.findAll();

        //then
        assertThat(orders).hasSize(1)
            .extracting("id", "email", "address.address", "address.postcode", "orderStatus")
            .containsExactlyInAnyOrder(
                tuple(orders.get(0).getId(), "test@gmail.com", "서울시 강남구", "125454", ORDERED)
            );
		// orderProducts 검증 추가
        assertThat(orderProducts).hasSize(2)
            .extracting("order.id", "product.id", "quantity")
            .containsExactlyInAnyOrder(
                tuple(orders.get(0).getId(), product1.getId(), 1),
                tuple(orders.get(0).getId(), product2.getId(), 2)
            );

        assertThat(response)
            .extracting("id", "email", "address", "postcode", "orderStatus")
            .containsExactlyInAnyOrder(
                response.getId(), "test@gmail.com", "서울시 강남구", "125454", ORDERED
            );

        assertThat(response.getOrderDetails())
            .hasSize(2)
            .extracting("category", "price", "quantity")
            .containsExactlyInAnyOrder(
                tuple("원두", 50000L, 1),
                tuple("음료", 3000L, 2)
            );
    }

기존에는 orderProducts에 대한 검증을 진행하고있지 않았다. 테스트를 추가로 구현하여 orderProducts를 검증하도록 하여 이러한 테스트의 함정에 빠지지 않도록 하였다.

어찌됐든 올바르게 테스트를 작성했으니 테스트를 믿고 리팩토링을 진행해보자

반복문을 돌면서 select 문을 날리면 안좋은거 아니야 ?

일단 엔티티 편의 메서드를 이용해서 리팩토링을 진행했다.

Order.java

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

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "order_id")
    private Long id;

    @Column(nullable = false, length = 50)
    private String email;

    @Embedded
    private Address address;

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL , orphanRemoval = true)
    private List<OrderProduct> orderProducts = new ArrayList<>();

    @Builder
    private Order(String email, String address, String postcode) {
        this.email = email;
        this.address =Address.builder()
            .address(address)
            .postcode(postcode)
            .build();
        this.orderStatus = OrderStatus.ORDERED;
    }

    public void updateStatus(OrderStatus orderStatus) {
        this.orderStatus = orderStatus;
    }

    public void addOrderProduct(OrderProduct orderProduct) {
        this.orderProducts.add(orderProduct);
    }
}

OrderProduct.java

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "order_items")
public class OrderProduct extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "seq")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    private Product product;

    @Column(nullable = false)
    private int quantity;

    public OrderProduct(Order order, Product product, int quantity) {
        this.order = order;
        this.product = product;
        this.quantity = quantity;
        order.addOrderProduct(this);
    }
}

OrderService.java

// OrderSerive.java
public OrderResponse createOrder(OrderCreateServiceRequest request) {
        Order order = Order.builder()
            .email(request.getEmail())
            .address(request.getAddress())
            .postcode(request.getPostcode())
            .build();

        Order savedOrder = orderRepository.save(order);

        for(Long productId : request.getOrderProductQuantity().keySet()) {
            Product product = productRepository.findById(productId)
                .orElseThrow(() -> new IllegalArgumentException("해당 상품 id : " + productId + "를 가진 상품이 존재하지 않습니다."));

            OrderProduct orderProduct = new OrderProduct(savedOrder, product, request.getOrderProductQuantity().get(productId));
            orderProductRepository.save(orderProduct);
        }

        return OrderResponse.of(savedOrder);
    }

팀원이 리뷰주신 내용을 토대로 그리고 팀원의 코드와 다른 블로그 내용들을 참고하여 코드를 작성하였다.

먼저 order에 대해서 저장해주고 order에서 List<OrderProduct>new ArrayList<>(); 로 되어있으니 리스트에 추가해주는 형식으로 구현하였다.

그런데 느낀게 있었다. 다른 글에서도 썼던 것 처럼 select 문이 하나씩 나가는게 마음에 안들었다. 이렇게 되면 100개 1000개 10000개가 되면 select를 그 갯수만큼 보내서 Product를 찾아야 한다. 성능상에 문제가 있지않을까 ? 라는 의문을 품게 되었고 기존에 겪었던 문제처럼 IN 절을 이용해서 해결하기로 했다.

IN 절을 사용해보자 또다른 문제

서비스의 코드만 수정하였다.

public OrderResponse createOrder(OrderCreateServiceRequest request) {

    Order order = Order.builder()
        .email(request.getEmail())
        .address(request.getAddress())
        .postcode(request.getPostcode())
        .build();

    Order savedOrder = orderRepository.save(order);

    Set<Long> productIds = request.getOrderProductQuantity().keySet();
    List<Product> products = productRepository.findAllById(productIds);

    for (Product product : products) {
        OrderProduct orderProduct = new OrderProduct(savedOrder, product, request.getOrderProductQuantity().get(product.getId()));
        orderProductRepository.save(orderProduct);
    }

    return OrderResponse.of(savedOrder);
}

IN 절을 사용하니 예외처리하기가 어려웠다. 예를 들어 1,2,5의 id를 가지고 있지만 실제론 1,2만 존재한다 가정하면 반환하는 List는 id가 1,2 인 Product를 가지게 되고 예외를 발생시키지 않는다.

이 부분은 그래도 크게 문제가 되는 부분이 아니라고 생각했다. 단순하게 생각하면 내가 IN절을 검색할 Id들의 개수와 검색한 결과의 개수를 비교해서 같지 않으면 IllegalArgumentException을 내면 되는거 아닐까?

그래서 아래와 같이 코드를 추가했다.

public OrderResponse createOrder(OrderCreateServiceRequest request) {

    Order order = Order.builder()
        .email(request.getEmail())
        .address(request.getAddress())
        .postcode(request.getPostcode())
        .build();

    Order savedOrder = orderRepository.save(order);

    Set<Long> productIds = request.getOrderProductQuantity().keySet();
    List<Product> products = productRepository.findAllById(productIds);

    if (productIds.size() != products.size()) {
        throw new IllegalArgumentException("주문 상품 id 중 존재하지 않는 상품이 존재합니다.");
    }

    for (Product product : products) {
        OrderProduct orderProduct = new OrderProduct(savedOrder, product, request.getOrderProductQuantity().get(product.getId()));
        orderProductRepository.save(orderProduct);
    }

    return OrderResponse.of(savedOrder);
}

나중에 정말 서비스가 커지면 BatchSize를 통해서 IN절의 개수를 조절해주긴 해야 할 것이다. IN절이라고 만능은 아니니 하지만 지금은 이정도에서 리팩토링을 마치도록 하자

리팩토링 결과

Order.java

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

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "order_id")
    private Long id;

    @Column(nullable = false, length = 50)
    private String email;

    @Embedded
    private Address address;

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL , orphanRemoval = true)
    private List<OrderProduct> orderProducts = new ArrayList<>();

    @Builder
    private Order(String email, String address, String postcode) {
        this.email = email;
        this.address =Address.builder()
            .address(address)
            .postcode(postcode)
            .build();
        this.orderStatus = OrderStatus.ORDERED;
    }

    public void updateStatus(OrderStatus orderStatus) {
        this.orderStatus = orderStatus;
    }

    public void addOrderProduct(OrderProduct orderProduct) {
        this.orderProducts.add(orderProduct);
    }
}

OrderProduct.java

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "order_items")
public class OrderProduct extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "seq")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    private Product product;

    @Column(nullable = false)
    private int quantity;

    public OrderProduct(Order order, Product product, int quantity) {
        this.order = order;
        this.product = product;
        this.quantity = quantity;
        order.addOrderProduct(this);
    }
}

OrderService.java

public OrderResponse createOrder(OrderCreateServiceRequest request) {

    Order order = Order.builder()
        .email(request.getEmail())
        .address(request.getAddress())
        .postcode(request.getPostcode())
        .build();

    Order savedOrder = orderRepository.save(order);

    Set<Long> productIds = request.getOrderProductQuantity().keySet();
    List<Product> products = productRepository.findAllById(productIds);

    for (Product product : products) {
        OrderProduct orderProduct = new OrderProduct(savedOrder, product, request.getOrderProductQuantity().get(product.getId()));
        orderProductRepository.save(orderProduct);
    }

    return OrderResponse.of(savedOrder);
}

팀원이 의견주신 대로 연관관계 편의메서드를 이용해 리팩토링을 진행하여 정상적으로 db에도 잘 들어가고 테스트도 통과하게 되었다.

테스트는 계속 똑같은 사진만쓰는거같지만 계속 돌리면서 캡처하는거다..

그리고 배운 개념들에 대해서 정리해야한다. 이대로 또 됐다 땡 하고 가면 지식 초기화다

CASCADE

전체적인 흐름은 연관관계 편의 메서드로 리팩토링 한 것이지만 내 코드를 먼저 알 필요가 있다. 내 코드는 왜 상대 객체를 추가해주지 않아도 됐을까 ?

바로 CascadeType.ALL의 특징 때문이었다.

Cascade란?

cascade란 특정 엔티티를 영속 상태로 만들 경우, 연관된 엔티티도 함께 영속 상태로 만들고 싶을 경우 영속성 전이를 사용한다.
JPA에서는 영속성 전이를 Cascade옵션을 통해서 설정하고 관리할 수 있습니다. 쉽게 말해서 부모 엔티티를 통해 자식엔티티를 다루기 위해 사용합니다.

CascadeType에는 사진과 같이 ALL, DETACH,MERGE,PERSIST,REFRESH,REMOVE 6가지가 있지만 중요하고 자주사용하는 ALL, REMOVE, PERSIST에 대해서만 알아보자

내가 사용한 CascadeType.ALL이 뭘까?

CascadeType.ALL 을 알기 위해서는 CascadeType.PERSISTCascadeType.REMOVE를 먼저 알아야한다. 왜냐하면 CascadeType.ALL은 그냥 persist와 remove를 포함할 뿐만 아니라 detach, merge 등 전부를 포함한 것이다.

CascadeType.PERSIST

PERSIST는 부모와 자식 엔티티를 한번에 영속화를 진행 할 수 있다.

@Entity
public class Order extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "order_id")
    private Long id;

    @Column(nullable = false, length = 50)
    private String email;

    @Embedded
    private Address address;

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus;

    @OneToMany(mappedBy = "order", cascade = CascadeType.PERSIST, orphanRemoval = true)
    private List<OrderProduct> orderProducts = new ArrayList<>();
    
}

내 코드의 경우는 Order를 영속화를 할 경우 OrderProduct까지 영속화가 진행되게 하기 위한 옵션을 CascadType.PERSIST라고 보면 된다.

CascadeType.REMOVE

remove또한 간단하다 영속화된 객체를 지우기 위해 사용한다. 부모엔티티가 삭제 될 때 자식엔티티도 삭제되도록 설정하는 것이다.

@Entity
public class Order extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "order_id")
    private Long id;

    @Column(nullable = false, length = 50)
    private String email;

    @Embedded
    private Address address;

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus;

    @OneToMany(mappedBy = "order", cascade = CascadeType.REMOVE, orphanRemoval = true)
    private List<OrderProduct> orderProducts = new ArrayList<>();
    
}

내 코드에서는 Order가 삭제 되면 OrderProduct 엔티티도 삭제된다고 보면 된다. cascade를 자식이 부모의 생명주기를 따라가기 때문에 부모가 삭제될때 같이 삭제된다고 이해하면 되겠다.

orphanRemoval = true 는 뭘까?

CascadeType.REMOVE를 걸었는데도 orphanRemoval = true가 걸려있다.

orphanRemoval은 고아객체를 삭제하는 옵션이다. CascadeType.REMOVE는 연관관계가 풀어지더라도 DB에 남아있게 된다. 하지만 orphanRemoval = true를 걸어두게 되면 연관관계가 풀어지면 고아객체로 취급하여서 DB에서 삭제하는 것이다.

Cascade는 조심히 사용하자

  1. 참조 무결성 제약조건 위반 가능성
  • 엔티티 삭제 시 연관된 엔티티들이 전부 삭제가 되기 때문에 의도치 않게 참조 무결성 제약조건을 위반할 수도 있다.
  1. 양방향 연관관계 매핑 시 충돌 가능성
  • 영속화에 대한 관리 지점이 두 곳이면 데이터값을 예측할 수 없는 문제가 발생합니다. ‘영속성 전이(cascade)는 관리하는 부모가 단 하나일 때 사용해야 한다'라는 주장이 나온 배경도 비슷한 맥락이다.
  • 자식이 여럿일 경우 편의메서드를 사용해서 지워주어야한다.

언제 사용하는게 좋을까?

게시판의 경우 게시물과 댓글과 같이 명확하게 부모-자식 구조로 표현 할 수 있을 때는 Cascade를 사용하는 것이 괜찮다고 한다. Cascade를 사용하면 리팩토링 이전에 내가 사용한 코드처럼 편리함을 가져다 줄 수 있다.
하지만 하나의 자식에 여러 부모가 대응되는 경우는 지양하는 것이 좋다.

결론

나는 CascadeType.ALL을 쓰고 있었으니까 자식 객체까지 같이 영속화되어서 연관관계 편의 메서드를 사용하지 않아도 저장이 되고 있었던 것이다. 그래서 ALL 에서 REMOVE로 낮추니까 자식 테이블에 들어가지 않았던 것이고
앞으로 Cascade에 대해서 조금은 알아봤으니 사용 할 때 유의하면서 사용해야겠다. 정말 정말 필요할지 엔티티 설계할 때 생각해보고 설계 하도록 해야겠다. 지금의 경우는 주문과 주문품목의 경우는 완벽한 부모-자식 구조가 맞지 않을까? 라고 생각하긴한다.

연관관계 편의 메서드

그렇다면 도대체 연관관계 편의 메서드는 뭘까 ?
김영한 강사님이 처음으로 얘기하신건지는 모르겠지만 강의에서 나온다고 한다. 그래서 검색하면 다 똑같은 예제이다. Team과 Member 처음에는 이게 뭘 얘기하고자 하는건지 몰랐다. 정확히 말하자면 왜 해야하는지 몰랐다. 왜냐면 Cascade에 대해서 정확하게 이해하지 못했고, 그에 따라서 당연히 연관관계 매핑되어있으니까 객체가 저장되어야되는거아니야 ? 라고 생각했다.

하지만 Cascade에 대해서 공부하고 나서는 무슨 뜻인지 이해가 되기 시작했고 그에 따라서 코드를 작성하게 되었다.

양방향 관계를 안전하게 유지하기 위해서 사용한다고 한다. 나는 아래처럼 구현하였다.


@Table(name = "orders")
@Entity
public class Order extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "order_id")
    private Long id;
    
    // 기타 컬럼

    @OneToMany(mappedBy = "order", cascade = CascadeType.REMOVE, orphanRemoval = true)
    private List<OrderProduct> orderProducts = new ArrayList<>();

    public void addOrderProduct(OrderProduct orderProduct) {
        this.orderProducts.add(orderProduct);
    }
}
@Entity
@Table(name = "order_items")
public class OrderProduct extends BaseEntity {

	@Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "seq")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    private Product product;

    @Column(nullable = false)
    private int quantity;

    public OrderProduct(Order order, Product product, int quantity) {
        this.order = order;
        this.product = product;
        this.quantity = quantity;
        order.addOrderProduct(this);
    }

}

근데 또 이렇게 생각 할 수 있다. Product도 ManyToOne인데 연관관계 편의 메서드 써야되는거아니야 ? 라고 생각할 수 있는데 나는 양방향 매핑을 사용하지 않았다 Product의 경우는 주문한 상품에 대해서 Product가 알아야될 필요가 있을까? 양방향 관계는 항상 조심스럽게 사용해야 한다고 한다.

연관관계 편의메서드는 사실은 너무 복잡하게 생각해서였던 것 같다. 객체의 관점에서 바라봤으면 당연히 추가해주는게 맞으니 사용해야하는게 맞지않을까? 라고 생각한다.

결론

느낀점

연관관계 편의 메서드 어디서 얼핏 들어본거같기도 하고 처음보는것 같기도 하면서 어려웠다. 하지만 차근차근 내 코드를 리팩토링 해나가고 강의를 듣진 못했지만 어떤 의도로 말씀하신걸까 계속 곱씹으면서 내가 리팩토링 한 코드를 살펴보았다. 그러다보니 Cascade에 대해서도 공부하고 편의메서드도 공부하고 하게 된 것 아닐까?
나는 정말 코드리뷰시간이 즐겁다고 해야하나? 혼자만의 힘으로 모든 것을 공부 할 순 없다고 생각한다. 이렇게 생각할 거리를 던져주시니 공부도 하게되고 기존에 쓰고있었지만 제대로 모르던 부분도 복습할 수 있었다.

그리고 무엇보다 테스트의 함정에 빠져서 조금 헤멘거 같지만 테스트를 수정해서 안정적인 상태로 리팩토링을 할 수 있었다 ! 이건 진짜 기쁘다. 포스트맨에서 하던 시절 생각하면 한세월이다 참.. 느끼는게 많은 리팩토링이었다고 생각한다.
그런데 하필이면 원래 order를 builder를 이용해서 생성해서 테스트를 하고 있었는데 builder의 생성방식이 바뀌면서 사실 테스트가 이곳저곳 다 깨져서.. 수정을 하긴 해야한다. 하지만 별거없다 이건 하면되지 뭐

정리

  • Cascade를 최대한 지양하자 (정말 완벽한 부모-자식 구조라고 생각이 든다면 사용 OK)
  • 연관관계 편의 메서드를 이용하자
  • 테스트를 꼼꼼하게 짜자 테스트의 함정에 빠진다
  • 쓰는 기술에 대해 제대로 알고쓰자

0개의 댓글