테이블에 fk를 쓰지 않는 경우 비즈니스 로직을 어떻게 짜야할까요?

24.05.08 17:59 작성 조회수 27

0

이때까지 프로젝트를 하다가 항상 fk를 사용해 왔었습니다.

그런데 테스트를 짜는 경우나, 아니면 요구사항이 변경돼 fk에도 null이 들어가야 하는 경우나 이런 경우때문에 fk를 없애고 연관 관계를 상대 테이블의 id로만 저장하기로 했습니다.

예를들어서,

Order - OrderProduct - Product 관계에서

OrderProduct:Order가 N:1 단방향 매핑

OrderProduct:Product가 N:1 단방향 매핑

으로 엔티티를 작성해 왔습니다.

 

아래는 fk가 존재하는 경우, 3개의 엔티티 코드입니다.

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

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;

   private Integer quantity;

   private Long amount;

   @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "order_id")
   private Order order;

   @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "product_id")
   private Product product;
}
@Entity
@Table(name = "order_tbl")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Order {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;

   @Enumerated(EnumType.STRING)
   private OrderStatus orderStatus;

   private Long usedPoint;

   private Long earnedPoint;

   private Long totalAmount;

   @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "consumer_id")
   private Consumer consumer;
}
@Entity
@Table(name = "product_tbl")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Product {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;

   private String name;

   private String description;

   private Long amount;

   private Integer stock;

   @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "seller_id")
   private Seller seller;

   @Enumerated(EnumType.STRING)
   private ProductStatus productStatus;
}

그리고 아래는 fk를 제거했을 때의 3가지 entity입니다. 큰 차이는 없고, 매핑을 제거해주고 대상 테이블의 id로 변경했습니다.

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

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;

   private Integer quantity;

   private Long amount;

   private Long orderId;

   private Long productId;
}
@Entity
@Table(name = "order_tbl")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Order extends BaseEntity {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;

   @Enumerated(EnumType.STRING)
   private OrderStatus orderStatus;

   private Long usedPoint;

   private Long earnedPoint;

   private Long totalAmount;

   private Long consumerId;
}
@Entity
@Table(name = "product_tbl")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Product extends BaseEntity {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;

   private String name;

   private String description;

   private Long amount;

   private Integer stock;

   private Long sellerId;

   @Enumerated(EnumType.STRING)
   private ProductStatus productStatus;
}

그리고 저는 물건 구매에 대한 비즈니스 로직을 작성했습니다.

아래 코드는 고객이 주문한 정보를 바탕으로 생성된 OrderProduct들을 전부 돌면서, 판매자의 잔고를 증가하는 로직입니다.

아래는 fk가 있는 경우 비즈니스 로직입니다.

private void addSellerBalance(List<OrderProduct> orderProducts) {
   for (OrderProduct orderProduct : orderProducts) {
      long amount = getSalesAmountExcludeFee(orderProduct);
      Seller seller = orderProduct.getProduct().getSeller();
      seller.addBalance(amount);
   }
} 

 

그리고 fk가 없이 해당 로직을 다시 작성하려하니 의문점이 생겼습니다.

OrderProduct에서 Product엔티티를 찾으려고 쿼리를 1번 수행하고,

Product에서 Seller를 찾으려고 쿼리를 1번 더 수행했습니다.

private void addSellerBalance(List<OrderProduct> orderProducts) {
   for (OrderProduct orderProduct : orderProducts) {
      long amount = getSalesAmountExcludeFee(orderProduct);
      Product product = productReadService.findById(orderProduct.getProductId());
      Seller seller = sellerReadService.findById(product.getSellerId());
      seller.addBalance(amount);
   }
}

이렇게 연관 관계가 있는 필드를 조회할 때마다 쿼리를 추가로 수행해야 하는 걸까요?

아니면 다른 방법이 있는 걸까요?

답변 1

답변을 작성해보세요.

0

인프런 AI 인턴님의 프로필

인프런 AI 인턴

2024.05.09

안녕하세요, 인프런 AI 인턴입니다.

네, 윤중진님의 질문에 대한 답변드리겠습니다.

ForeignKey를 사용하지 않고 연관 관계를 매핑할 경우, 각 연관된 엔티티를 조회하기 위한 별도의 쿼리 수행은 피할 수 없는 부분입니다. 하지만, 성능 최적화를 위해 몇 가지 접근 방법을 고려해볼 수 있습니다.

  1. 일대다 조회 최적화: OrderProduct와 연관된 Product, 그리고 Seller를 조회할 때, JOIN FETCH나 엔티티 그래프 기능을 사용하여 연관된 엔티티들을 한 번의 쿼리로 가져올 수 있습니다. 이 방법은 연관된 데이터의 양이 많지 않을 때 유용합니다.

  2. Batch Size 사용: 하이버네이트의 @BatchSize 애노테이션을 사용하여 연관 엔티티를 조회할 때 N+1 문제를 줄일 수 있습니다. 이는 한 번에 여러 ProductSeller를 조회할 수 있게 해주어 쿼리 호출 수를 줄여줍니다.

  3. 쿼리 최적화: 특정 로직에서 수행되는 쿼리의 수를 줄이기 위해 JPQL이나 QueryDSL 같은 쿼리 언어를 사용하여 필요한 데이터만을 선택적으로 로딩할 수 있습니다. 예를 들어, OrderProduct 리스트를 반복 처리하는 대신, ProductSeller 정보를 한 번에 가져오는 복합 쿼리를 작성할 수 있습니다.

  4. DTO를 활용한 조회: 필요한 정보만을 담은 DTO(Data Transfer Object)를 직접 조회하는 방식을 사용할 수 있습니다. 이 경우, 엔티티의 모든 필드를 로딩하는 대신 필요한 데이터만을 선택하여 데이터베이스로부터 가져올 수 있으므로 성능이 향상될 수 있습니다.

  5. Caching 적용: 자주 사용되는 데이터나, 변동이 적은 데이터에 대해서는 캐싱 전략을 적용하여 DB 접근을 줄일 수 있습니다. 예를 들어, Seller 정보가 자주 변경되지 않는다면, 캐시를 활용하여 성능을 개선할 수 있습니다.

각 방법은 상황에 따라 장단점이 있으므로, 애플리케이션의 요구 사항과 특성을 고려하여 적절한 접근 방법을 선택하는 것이 중요합니다.