- 주문 도메인에서 개별 메뉴 정보를 담는 OrderItemEntity 클래스입니다. 이 엔티티 설계에서 가장 중요하게 고려한 부분은 "과거 결제 데이터의 무결성 유지"와 "엔티티 특성에 맞는 Audit(감사) 필드 적용"입니다.
💻 실제 코드 (OrderItemEntity.java)
@Entity
@Table(name = "p_order_item")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class OrderItemEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID orderItemId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false)
private OrderEntity order;
@Column(nullable = false)
private UUID menuId;
@Column(nullable = false)
private Integer quantity;
@Column(nullable = false)
private Integer priceAtOrder; // 주문 시점의 가격
// [Audit] 필수 기능 명세에 따른 기록 전용 필드 (수정/삭제 미발생 도메인 특성 반영)
// 명세서 지침에 따라 BaseEntity 상속 없이 생성 정보만 직접 관리함
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
private UUID createdBy; // [인증/인가] 주문자 ID 연동 완료
// 필수 기능 명세에 따라 데이터 저장 전 생성일(createdAt) 자동 기록
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
}
🤔 왜 이렇게 짰는가? (설계 의도와 이유)
데이터 무결성을 위한 스냅샷 패턴 (priceAtOrder)
- 주문 항목(OrderItemEntity)은 메뉴 테이블(MenuEntity)의 ID(menuId)를 참조하지만, 메뉴의 현재 가격과 별개로 priceAtOrder라는 필드를 따로 두었습니다.
- 상점 주인이 메뉴의 가격을 인상하거나 인하할 수 있습니다. 만약 주문 내역 조회 시 현재 메뉴 테이블의 가격을 조인해서 보여준다면, 과거 주문 내역의 결제 금액 데이터가 틀어지는 심각한 정합성 문제가 발생합니다.
- 이를 방지하기 위해 주문이 생성되는 시점의 가격을 복사하여 고정시키는 스냅샷(Snapshot) 방식을 적용해 데이터 무결성을 유지했습니다.
도메인 특성을 반영한 맞춤형 Audit 관리 (수정/삭제 배제)
- 프로젝트 내 다른 엔티티들은 생성/수정/삭제 시간을 관리하는 BaseAuditEntity를 상속받습니다.
- 하지만 OrderItemEntity는 한 번 생성(결제)되면 비즈니스 정책상 개별 항목만 수정되거나 삭제되는 일이 발생하지 않는 불변(Immutable)에 가까운 특성을 가집니다.
- 마치 '영수증'에 찍힌 개별 항목과 같습니다. 한 번 결제가 끝난 영수증 안에서 콜라 하나만 빼거나 치킨을 피자로 수정하는 일은 없습니다. 변경이 필요하면 주문 전체를 취소하고 새로 주문해야 합니다. 즉, 단일 아이템이 개별적으로 수정되거나 삭제되는 일은 발생하지 않습니다.
- 따라서 불필요한 필드(수정일, 삭제일 등)를 상속받아 DB 공간을 낭비하지 않도록, BaseAuditEntity 상속을 제외하고 오직 생성 시간(createdAt)과 생성자(createdBy)만 @PrePersist를 통해 직접 관리하도록 최적화했습니다.
지연 로딩 (FetchType.LAZY) 적용
- @ManyToOne 관계의 기본 설정은 EAGER(즉시 로딩)입니다. 기본값인 즉시 로딩(EAGER)을 그대로 방치하면, 단일 주문 아이템(OrderItem) 하나만 필요한 상황에서도 불필요하게 부모 엔티티인 전체 주문(Order) 정보까지 항상 조인(JOIN)하여 가져오는 쿼리 오버헤드가 발생합니다.
- 상황: 내가 DB에서 주문 아이템(OrderItem) 데이터 1개(예: '후라이드 치킨 1마리')만 딱 꺼내서 메뉴 아이디만 확인하고 싶습니다.
- 문제: 그런데 EAGER로 설정되어 있으면 스프링이 "어? 이 치킨 아이템은 '주문(Order)' 정보랑 연결되어 있네? 내가 친절하게 부모인 주문 정보도 다 가져와 줄게!" 하면서, 내가 달라고 하지도 않은 전체 주문 정보(고객 ID, 배송지 주소, 배달비, 전체 결제 금액 등)까지 무거운 조인(JOIN) 쿼리를 써서 억지로 한꺼번에 다 끌고 옵니다.
- 결과: 나는 치킨 이름 하나 보려고 했는데, 쓸데없이 거대한 데이터를 DB에서 퍼오느라 서버 성능이 느려집니다.
- 이를 방지하기 위해 FetchType.LAZY로 명시하여, 실제 order 객체가 필요한 시점에만 쿼리가 발생하도록 최적화했습니다.
- 해결: FetchType.LAZY를 걸어두면 스프링이 "오케이, 일단 '후라이드 치킨(OrderItem)' 정보 1줄만 가볍게 가져올게. 나중에 네가 진짜로 주문(Order) 정보가 필요해져서 item.getOrder().getTotalPrice() 같이 부를 때, 그때 다시 DB에 물어볼게!"라고 작동합니다. (이것을 지연시킨다고 해서 '지연 로딩'이라고 부릅니다.)
- 결과: 당장 필요한 데이터만 아주 가벼운 쿼리로 쏙 꺼내오기 때문에, 불필요한 DB 자원 낭비를 막고 응답 속도가 훨씬 좋아집니다.
💡 정리
- 스냅샷 패턴 적용: 메뉴 가격 변동에 대비하여 priceAtOrder 필드에 주문 시점의 가격을 고정 저장했습니다. 이를 통해 RDBMS의 참조 관계(FK)만을 맹신하지 않고, 시간의 흐름에 따른 데이터의 정합성과 결제 기록의 무결성을 비즈니스 요구사항에 맞게 지켜냈습니다.
- 도메인 특성에 따른 유연한 설계: 모든 엔티티에 일괄적으로 공통 Audit 클래스를 상속하기보다는, 데이터의 수정 및 삭제가 일어나지 않는 OrderItem 도메인의 비즈니스적 불변(Immutable) 특성을 분석하여 필요한 컬럼만 정의하는 실용적인 데이터베이스 설계를 취했습니다.
- JPA 연관관계 최적화: @ManyToOne에 지연 로딩(FetchType.LAZY)을 기본으로 적용하여 불필요한 외부 조인을 방지하고 쿼리 성능을 튜닝했습니다.