Session 3JPA Workshop

연관관계 매핑 기초

Entity 간의 관계를 매핑하는 방법, 단방향과 양방향의 차이, 연관관계의 주인 개념을 학습합니다.

학습 목표

  • 객체 참조와 외래키의 차이를 이해한다
  • 단방향과 양방향 연관관계를 구분하고 선택할 수 있다
  • 연관관계의 주인 개념을 이해하고 올바르게 설정할 수 있다
  • @ManyToOne, @OneToMany, @OneToOne을 적절히 사용할 수 있다
  • @ManyToMany의 문제점과 대안을 이해한다

1. 연관관계가 필요한 이유

객체 vs 테이블의 연관관계 차이

객체

  • 참조(Reference)로 연관관계 맺음
  • 방향성 존재 (단방향/양방향)
  • order.getCustomer() 가능
  • customer.getOrders() 별도 설정 필요

테이블

  • 외래키(FK)로 연관관계 맺음
  • 방향성 없음 (항상 양방향)
  • JOIN으로 양쪽 탐색 가능
  • orders JOIN customers
연관관계 없이 설계한 경우 (안티패턴)
// 외래키를 그대로 필드로 가지는 경우
@Entity
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private Long customerId;  // 외래키 직접 보관 (객체지향적이지 않음)
    private Long productId;
}

// 사용 시 불편함
Order order = orderRepository.findById(orderId).orElseThrow();
Long customerId = order.getCustomerId();
Customer customer = customerRepository.findById(customerId).orElseThrow();  // 추가 조회 필요

// 객체 그래프 탐색 불가
// order.getCustomer().getName()  ← 불가능!
연관관계 매핑 적용 (권장)
// 객체 참조로 연관관계 설정
@Entity
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private Customer customer;  // 객체 참조!
}

// 사용 시 편리함
Order order = orderRepository.findById(orderId).orElseThrow();
Customer customer = order.getCustomer();  // 바로 접근 가능!
String customerName = order.getCustomer().getName();  // 객체 그래프 탐색

1.1 연관관계 용어 정리

방향 (Direction)

단방향: 한쪽만 참조 / 양방향: 양쪽 모두 참조

다중성 (Multiplicity)

다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)

연관관계의 주인 (Owner)

양방향 관계에서 외래키를 관리하는 쪽. 주인만 외래키 등록/수정 가능

1.2 이커머스 도메인 관계

Customer

고객

1 : N

Order

주문

Order

주문

1 : N

OrderItem

주문상품

Category

카테고리

1 : N

Product

상품

2. 단방향 연관관계

가장 기본적인 연관관계입니다. 한쪽 Entity에서만 다른 Entity를 참조합니다.실무에서는 단방향으로 시작하고, 필요할 때만 양방향을 추가하는 것이 좋습니다.

2.1 다대일(N:1) 단방향

Order → Customer (N:1)

여러 주문이 한 고객에게 속합니다. 외래키가 있는 Order 쪽에서 Customer를 참조합니다.

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    // 다대일 단방향
    @ManyToOne(fetch = FetchType.LAZY)  // 지연 로딩 권장
    @JoinColumn(name = "customer_id")   // FK 컬럼명
    private Customer customer;
    
    private LocalDateTime orderDate;
    private BigDecimal totalAmount;
    
    // 연관관계 편의 메서드
    public void setCustomer(Customer customer) {
        this.customer = customer;
    }
}

@Entity
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    private String email;
    
    // Customer에서는 Order를 참조하지 않음 (단방향)
}
사용 예시
// 저장
Customer customer = customerRepository.findById(customerId).orElseThrow();

Order order = new Order();
order.setCustomer(customer);  // 연관관계 설정
order.setOrderDate(LocalDateTime.now());
order.setTotalAmount(new BigDecimal("50000"));

orderRepository.save(order);
// INSERT INTO orders (customer_id, order_date, total_amount) VALUES (?, ?, ?)

// 조회
Order order = orderRepository.findById(orderId).orElseThrow();
Customer customer = order.getCustomer();  // 객체 그래프 탐색
System.out.println(customer.getName());   // 지연 로딩 시 이 시점에 SELECT

2.2 @JoinColumn 상세

속성설명기본값
name외래키 컬럼명필드명_참조테이블PK
referencedColumnName참조하는 대상 테이블의 컬럼명참조 테이블의 PK
foreignKey외래키 제약조건 이름 (DDL)-
nullablenull 허용 여부true
// 기존 DB 외래키 컬럼명에 맞추기
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(
    name = "cust_id",                    // 기존 FK 컬럼명
    referencedColumnName = "customer_id", // 참조 대상 PK (보통 생략)
    foreignKey = @ForeignKey(name = "fk_order_customer")  // FK 제약조건명
)
private Customer customer;

2.3 일대일(1:1) 단방향

Order → Delivery (1:1)
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private Customer customer;
    
    // 일대일 단방향
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;
}

@Entity
public class Delivery {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Embedded
    private Address address;
    
    @Enumerated(EnumType.STRING)
    private DeliveryStatus status;
}

단방향 연관관계의 장점

  • • 구조가 단순하고 이해하기 쉬움
  • • 순환 참조 위험 없음
  • • 필요한 방향으로만 탐색 가능
  • • 대부분의 비즈니스 로직은 단방향으로 충분

권장: 처음에는 단방향으로 설계하고, 양방향이 꼭 필요할 때만 추가

3. 양방향 연관관계와 연관관계의 주인

양방향 연관관계는 양쪽 Entity가 서로를 참조합니다. 이때 연관관계의 주인을 정해야 합니다.

3.1 연관관계의 주인 (Owner)

핵심 규칙

  • 연관관계의 주인만 외래키를 등록/수정할 수 있다
  • 주인이 아닌 쪽은 읽기만 가능 (mappedBy 사용)
  • 외래키가 있는 곳을 주인으로 정한다
  • 다대일에서는 항상 "다" 쪽이 주인
양방향 매핑 예시: Order ↔ Customer
// Order: 연관관계의 주인 (외래키 보유)
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    // 주인: 외래키 관리
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private Customer customer;
    
    // 연관관계 편의 메서드
    public void setCustomer(Customer customer) {
        this.customer = customer;
        customer.getOrders().add(this);  // 양방향 동기화
    }
}

// Customer: 주인이 아님 (mappedBy)
@Entity
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    // 주인이 아님: mappedBy로 읽기 전용
    // "order의 customer 필드에 의해 매핑됨"
    @OneToMany(mappedBy = "customer")
    private List<Order> orders = new ArrayList<>();
    
    // 연관관계 편의 메서드
    public void addOrder(Order order) {
        orders.add(order);
        order.setCustomer(this);
    }
}

3.2 mappedBy 이해하기

주인 (Owner)

  • • @JoinColumn 사용
  • • 외래키 등록/수정 가능
  • • 값을 설정해야 DB에 반영
order.setCustomer(customer);

주인 아님 (Non-Owner)

  • • mappedBy 사용
  • • 읽기만 가능
  • • 값 설정해도 DB에 반영 안됨
@OneToMany(mappedBy = "customer")

3.3 양방향 매핑 시 주의사항

흔한 실수: 주인이 아닌 쪽에만 값 설정
// 잘못된 코드 - DB에 반영 안됨!
Customer customer = new Customer();
Order order = new Order();

customer.getOrders().add(order);  // 주인이 아닌 쪽에만 설정
// order.setCustomer(customer);   // 주인 쪽 설정 누락!

customerRepository.save(customer);
orderRepository.save(order);
// 결과: orders 테이블의 customer_id = NULL

// 올바른 코드 - 양쪽 모두 설정
Customer customer = new Customer();
Order order = new Order();

order.setCustomer(customer);      // 주인 쪽 설정 (필수!)
customer.getOrders().add(order);  // 객체 그래프 일관성 (권장)

// 또는 연관관계 편의 메서드 사용
customer.addOrder(order);  // 내부에서 양쪽 모두 설정
연관관계 편의 메서드

양방향 관계에서 양쪽 객체를 동기화하는 메서드를 한쪽에 만들어 사용합니다.

@Entity
public class Order {
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private Customer customer;
    
    // 연관관계 편의 메서드 (주인 쪽에 작성)
    public void changeCustomer(Customer customer) {
        // 기존 관계 제거
        if (this.customer != null) {
            this.customer.getOrders().remove(this);
        }
        // 새 관계 설정
        this.customer = customer;
        if (customer != null) {
            customer.getOrders().add(this);
        }
    }
}

// 사용
Order order = new Order();
order.changeCustomer(customer);  // 양쪽 모두 자동 설정

양방향 매핑 정리

  • 단방향으로 충분: 대부분의 경우 단방향만으로 충분합니다
  • 양방향은 조회 편의: 역방향 조회가 필요할 때만 추가
  • 주인은 외래키 위치: 외래키가 있는 테이블의 Entity가 주인
  • 편의 메서드 필수: 양방향 시 양쪽 동기화 메서드 작성

4. @ManyToOne / @OneToMany 상세

4.1 @ManyToOne 속성

속성설명기본값
fetch로딩 전략EAGER (즉시 로딩)
cascade영속성 전이없음
optionalnull 허용 여부true
targetEntity연관 Entity 타입자동 추론
// 권장 설정
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;

4.2 @OneToMany 속성

속성설명기본값
mappedBy연관관계 주인 필드명-
fetch로딩 전략LAZY (지연 로딩)
cascade영속성 전이없음
orphanRemoval고아 객체 자동 삭제false
// 양방향 매핑
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();

4.3 일대다 단방향 (비권장)

일대다 단방향의 문제점

"일" 쪽에서 외래키를 관리하면 추가 UPDATE 쿼리가 발생합니다.

// 일대다 단방향 (비권장)
@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;
    
    // 외래키가 Member 테이블에 있지만 Team에서 관리
    @OneToMany
    @JoinColumn(name = "team_id")  // Member 테이블의 FK
    private List<Member> members = new ArrayList<>();
}

// 문제: INSERT 후 UPDATE 추가 발생
team.getMembers().add(member);
// INSERT INTO member (name) VALUES (?)
// UPDATE member SET team_id = ? WHERE id = ?  ← 추가 쿼리!

권장: 다대일 양방향으로 변경하세요

4.4 이커머스 예제: Order - OrderItem

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private Customer customer;
    
    // 양방향: Order가 OrderItem의 생명주기 관리
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> orderItems = new ArrayList<>();
    
    private LocalDateTime orderDate;
    
    @Enumerated(EnumType.STRING)
    private OrderStatus status;
    
    // 연관관계 편의 메서드
    public void addOrderItem(OrderItem orderItem) {
        orderItems.add(orderItem);
        orderItem.setOrder(this);
    }
    
    public void removeOrderItem(OrderItem orderItem) {
        orderItems.remove(orderItem);
        orderItem.setOrder(null);
    }
    
    // 비즈니스 메서드
    public BigDecimal getTotalPrice() {
        return orderItems.stream()
            .map(OrderItem::getTotalPrice)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

@Entity
public class OrderItem {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;
    
    private Integer quantity;
    private BigDecimal orderPrice;  // 주문 시점 가격
    
    public BigDecimal getTotalPrice() {
        return orderPrice.multiply(BigDecimal.valueOf(quantity));
    }
    
    // 생성 메서드
    public static OrderItem createOrderItem(Product product, int quantity) {
        OrderItem orderItem = new OrderItem();
        orderItem.product = product;
        orderItem.quantity = quantity;
        orderItem.orderPrice = product.getPrice();
        product.removeStock(quantity);  // 재고 차감
        return orderItem;
    }
}
사용 예시
// 주문 생성
@Transactional
public Long createOrder(Long customerId, List<OrderItemRequest> items) {
    Customer customer = customerRepository.findById(customerId).orElseThrow();
    
    Order order = new Order();
    order.setCustomer(customer);
    order.setOrderDate(LocalDateTime.now());
    order.setStatus(OrderStatus.PENDING);
    
    for (OrderItemRequest item : items) {
        Product product = productRepository.findById(item.getProductId()).orElseThrow();
        OrderItem orderItem = OrderItem.createOrderItem(product, item.getQuantity());
        order.addOrderItem(orderItem);  // 연관관계 편의 메서드
    }
    
    orderRepository.save(order);  // cascade로 OrderItem도 함께 저장
    return order.getId();
}

5. @OneToOne 일대일 관계

일대일 관계는 양쪽 모두 하나의 대상만 가집니다. 외래키를 어느 테이블에 둘지 선택해야 합니다.

5.1 외래키 위치 선택

주 테이블에 외래키 (권장)

주로 접근하는 테이블에 FK 배치

  • • 객체지향적 (주 객체가 대상 참조)
  • • JPA 매핑 편리
  • • 주 테이블만 조회해도 대상 확인 가능
Order(FK) → Delivery

대상 테이블에 외래키

일대일 → 일대다 변경 시 유리

  • • 테이블 관계 변경에 유연
  • • 프록시 기능 제한 (지연 로딩 불가)
  • • 양방향 필수
Order ← Delivery(FK)

5.2 주 테이블에 외래키 (단방향)

// Order가 주 테이블, Delivery가 대상 테이블
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    // 일대일 단방향: Order → Delivery
    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "delivery_id", unique = true)
    private Delivery delivery;
    
    // 배송 정보 설정
    public void setDelivery(Delivery delivery) {
        this.delivery = delivery;
    }
}

@Entity
public class Delivery {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Embedded
    private Address address;
    
    @Enumerated(EnumType.STRING)
    private DeliveryStatus status = DeliveryStatus.READY;
    
    // Order를 참조하지 않음 (단방향)
}

5.3 주 테이블에 외래키 (양방향)

@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    // 주인: 외래키 관리
    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;
    
    // 연관관계 편의 메서드
    public void setDelivery(Delivery delivery) {
        this.delivery = delivery;
        delivery.setOrder(this);
    }
}

@Entity
public class Delivery {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    // 주인 아님: mappedBy
    @OneToOne(mappedBy = "delivery", fetch = FetchType.LAZY)
    private Order order;
    
    @Embedded
    private Address address;
    
    @Enumerated(EnumType.STRING)
    private DeliveryStatus status = DeliveryStatus.READY;
    
    public void setOrder(Order order) {
        this.order = order;
    }
}

5.4 이커머스 예제: Customer - CustomerDetail

고객 상세 정보 분리

자주 조회하는 기본 정보와 가끔 필요한 상세 정보를 분리합니다.

@Entity
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String email;
    private String name;
    
    // 상세 정보는 필요할 때만 로딩
    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "customer_detail_id")
    private CustomerDetail detail;
}

@Entity
public class CustomerDetail {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @OneToOne(mappedBy = "detail")
    private Customer customer;
    
    private LocalDate birthDate;
    private String phoneNumber;
    
    @Embedded
    private Address address;
    
    @Lob
    private String memo;
    
    private Integer point = 0;
    private String grade = "BRONZE";
}

일대일 관계 설계 팁

  • 주 테이블에 FK: 주로 접근하는 Entity에 외래키 배치
  • 지연 로딩: @OneToOne도 fetch = LAZY 설정 권장
  • cascade: 생명주기가 같으면 CascadeType.ALL 사용
  • unique 제약: @JoinColumn에 unique = true 추가

6. @ManyToMany 다대다 관계 (주의)

실무에서 @ManyToMany 사용 금지!

  • • 중간 테이블에 추가 컬럼을 넣을 수 없음
  • • 예상치 못한 쿼리 발생
  • • 중간 테이블을 Entity로 승격시켜 사용

6.1 @ManyToMany의 문제점

문제가 있는 코드
// @ManyToMany 사용 (비권장)
@Entity
public class Product {
    @Id @GeneratedValue
    private Long id;
    
    @ManyToMany
    @JoinTable(
        name = "product_category",
        joinColumns = @JoinColumn(name = "product_id"),
        inverseJoinColumns = @JoinColumn(name = "category_id")
    )
    private List<Category> categories = new ArrayList<>();
}

@Entity
public class Category {
    @Id @GeneratedValue
    private Long id;
    
    @ManyToMany(mappedBy = "categories")
    private List<Product> products = new ArrayList<>();
}

// 문제점:
// 1. 중간 테이블(product_category)에 추가 컬럼 불가
//    - 등록일, 순서, 메인 카테고리 여부 등 저장 불가
// 2. 중간 테이블이 숨겨져 있어 예상치 못한 쿼리 발생
// 3. 비즈니스 요구사항 변경에 대응 어려움

6.2 해결책: 중간 Entity 생성

권장: 연결 Entity 사용

다대다 관계를 일대다 + 다대일로 풀어서 설계합니다.

// 중간 Entity: ProductCategory
@Entity
@Table(name = "product_category")
public class ProductCategory {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "category_id")
    private Category category;
    
    // 추가 컬럼 가능!
    private Integer displayOrder;      // 표시 순서
    private Boolean isMainCategory;    // 메인 카테고리 여부
    private LocalDateTime createdAt;   // 등록일
    
    // 생성 메서드
    public static ProductCategory create(Product product, Category category, 
                                          int order, boolean isMain) {
        ProductCategory pc = new ProductCategory();
        pc.product = product;
        pc.category = category;
        pc.displayOrder = order;
        pc.isMainCategory = isMain;
        pc.createdAt = LocalDateTime.now();
        return pc;
    }
}

@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<ProductCategory> productCategories = new ArrayList<>();
    
    // 카테고리 추가
    public void addCategory(Category category, int order, boolean isMain) {
        ProductCategory pc = ProductCategory.create(this, category, order, isMain);
        productCategories.add(pc);
    }
    
    // 메인 카테고리 조회
    public Category getMainCategory() {
        return productCategories.stream()
            .filter(ProductCategory::getIsMainCategory)
            .findFirst()
            .map(ProductCategory::getCategory)
            .orElse(null);
    }
}

@Entity
public class Category {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "category")
    private List<ProductCategory> productCategories = new ArrayList<>();
    
    // 해당 카테고리의 상품 수
    public int getProductCount() {
        return productCategories.size();
    }
}

6.3 사용 예시

// 상품에 카테고리 추가
@Transactional
public void addCategoryToProduct(Long productId, Long categoryId, 
                                  int order, boolean isMain) {
    Product product = productRepository.findById(productId).orElseThrow();
    Category category = categoryRepository.findById(categoryId).orElseThrow();
    
    product.addCategory(category, order, isMain);
}

// 상품의 카테고리 목록 조회
public List<CategoryDto> getProductCategories(Long productId) {
    Product product = productRepository.findById(productId).orElseThrow();
    
    return product.getProductCategories().stream()
        .sorted(Comparator.comparing(ProductCategory::getDisplayOrder))
        .map(pc -> new CategoryDto(
            pc.getCategory().getId(),
            pc.getCategory().getName(),
            pc.getIsMainCategory()
        ))
        .toList();
}

// 카테고리별 상품 조회 (JPQL)
@Query("SELECT pc.product FROM ProductCategory pc " +
       "WHERE pc.category.id = :categoryId " +
       "ORDER BY pc.displayOrder")
List<Product> findProductsByCategory(@Param("categoryId") Long categoryId);

다대다 관계 정리

@ManyToMany (금지)

  • • 추가 컬럼 불가
  • • 숨겨진 중간 테이블
  • • 예측 어려운 쿼리

연결 Entity (권장)

  • • 추가 컬럼 자유롭게
  • • 명시적인 관계
  • • 비즈니스 로직 추가 가능

7. 실습: 이커머스 연관관계 설계

전체 Entity 관계도

Customer (1) ←──────── (N) Order (1) ←──────── (N) OrderItem (N) ──────→ (1) Product
    │                        │                                                  │
    │                        │                                                  │
    └── CustomerDetail       └── Delivery                                       │
         (1:1)                    (1:1)                                         │
                                                                                │
                                                              ProductCategory ──┘
                                                                (N)    (N)
                                                                 │      │
                                                                 └──────┘
                                                                 Category
완성된 Entity 구조
// Customer
@Entity
public class Customer extends BaseEntity {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String email;
    private String name;
    
    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "customer_detail_id")
    private CustomerDetail detail;
    
    @OneToMany(mappedBy = "customer")
    private List<Order> orders = new ArrayList<>();
}

// Order
@Entity
@Table(name = "orders")
public class Order extends BaseEntity {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private Customer customer;
    
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> orderItems = new ArrayList<>();
    
    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id")
    private Delivery delivery;
    
    @Enumerated(EnumType.STRING)
    private OrderStatus status;
    
    private LocalDateTime orderDate;
}

// OrderItem
@Entity
public class OrderItem {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id")
    private Product product;
    
    private Integer quantity;
    private BigDecimal orderPrice;
}

// Product
@Entity
public class Product extends BaseEntity {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    private BigDecimal price;
    private Integer stockQuantity;
    
    @OneToMany(mappedBy = "product", cascade = CascadeType.ALL)
    private List<ProductCategory> productCategories = new ArrayList<>();
}

// Category
@Entity
public class Category {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Category parent;
    
    @OneToMany(mappedBy = "parent")
    private List<Category> children = new ArrayList<>();
    
    @OneToMany(mappedBy = "category")
    private List<ProductCategory> productCategories = new ArrayList<>();
}

7.1 정리

단방향으로 시작

필요할 때만 양방향 추가

외래키가 있는 곳이 연관관계의 주인

다대일에서 "다" 쪽이 주인

양방향은 편의 메서드 필수

양쪽 객체 동기화

@ManyToMany 사용 금지

연결 Entity로 풀어서 설계

fetch = LAZY 기본 설정

N+1 문제 예방 (다음 세션에서 상세)

7.2 핵심 키워드

@ManyToOne@OneToMany@OneToOne@JoinColumnmappedBy연관관계의 주인단방향양방향편의 메서드cascadeorphanRemovalFetchType.LAZY

7.3 다음 세션 예고

Session 4: 고급 연관관계와 N+1 문제

다음 세션에서는 연관관계의 성능 문제와 해결 방법을 학습합니다.

N+1 문제란?

연관 Entity 조회 시 발생하는 추가 쿼리 문제

Fetch Join

JPQL JOIN FETCH로 한 번에 조회

@EntityGraph

어노테이션 기반 페치 전략

@BatchSize

IN 쿼리로 배치 로딩