BE/스프링

JPA : Entity 연관 관계 / @JoinColumn

sonoopy 2024. 11. 4. 22:05

 

JPA  엔티티 간의 연관 관계

JPA에서는 객체 간의 다양한 연관관계를 설정할 수 있으며, 각 연관관계는 주인 엔티티와 외래 키(FK)의 위치에 따라 결정

 

 

  • 1:1 관계 (One-to-One): 한 엔티티가 다른 엔티티와 1:1로 매핑되는 관계
  • 1:N 관계 (One-to-Many): 한 엔티티가 여러 엔티티와 매핑되는 관계
  • N:1 관계 (Many-to-One): 여러 엔티티가 한 엔티티와 매핑되는 관계
  • N:M 관계 (Many-to-Many): 여러 엔티티가 서로 여러 엔티티와 매핑되는 관계

 

연관관계 JPA Annotation
1:1 @OneToOne
1:N @OneToMany
N:1 @ManyToOne
N:M @ManyToMany

 

 

 


 

 

1.  1:1 (One-to-One) 관계

 

1:1 관계에서는 외래 키를 어느 테이블에 두어도 관계가 성립

 

-  단방향: 한 엔티티에서만 참조하는 방식

-  양방향: 두 엔티티가 서로 참조하는 방식으로, mappedBy를 통해 관계의 주인을 설정

 

 

@Entity
public class Customer {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToOne
    @JoinColumn(name = "customer_detail_id", unique = true)
    private CustomerDetail customerDetail;
}

@Entity
public class CustomerDetail {
    @Id @GeneratedValue
    private Long id;
    private String address;
    private String phoneNumber;

    @OneToOne(mappedBy = "customerDetail") // 관계의 주인을 Customer로 설정
    private Customer customer;
}

 

 

  • 각 고객은 하나의 상세 정보를 가지고, 고객 상세 정보도 하나의 고객과만 연결
  • 외래 키가 있는 Customer 엔티티가 관계의 주인
  • CustomerDetail은 mappedBy를 통해 주인이 아님을 명시
  • 1:1 관계에서는 외래 키 컬럼에 unique = true를 설정하여 중복을 방지

 

 


 

 

2.  1:N / N:1 (One-to-Many / Many-to-One) 관계

 

 

-  단방향: 한 엔티티만 상대방을 참조하며, 외래 키는 N쪽 엔티티에 위치

-  양방향: 양쪽 엔티티가 서로를 참조하며, @OneToMany 쪽에서는 mappedBy로 주인을 지정

 

 

Customer가 여러 Order를 가질 수 있으며, Order는 특정 Customer에 속하는 단방향 예제

@Entity
public class Customer {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany
    @JoinColumn(name = "customer_id") // FK는 Order에 위치
    private List<Order> orders = new ArrayList<>();
}

 

 

Customer Order의 관계를 양방향으로 설정하여 Order에서도 Customer를 참조할 수 있도록한 양방향 예제

 

@Entity
public class Customer {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "customer")
    private List<Order> orders = new ArrayList<>();
}

@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;

    private String orderNumber;
    private LocalDateTime orderDate;

    @ManyToOne
    @JoinColumn(name = "customer_id") // FK 설정
    private Customer customer;
}

 

  • 한 명의 Customer가 여러 개의 Order를 생성
  • Order 엔티티가 외래 키를 가지고 있어 주인이며, Customer 엔티티에서는 mappedBy = "customer"로 참조
  • 지연 로딩(Lazy Loading): @OneToMany는 기본적으로 LAZY이며, 필요할 때만 조회
  • N+1 문제: @OneToMany 관계는 컬렉션 조회 시 추가 쿼리가 발생 가능 ->  fetch join 또는 @BatchSize 같은 최적화 기법 사용

 


 

3.  N:M (Many-to-Many) 관계와 연결 엔티티

 

실무에서는 중간 테이블(연결 엔티티)을 사용해 다대다 관계를 1과 N:1로 분해하는 것이 일반적

연결 엔티티는 다대다 관계에 추가 정보 저장 가능

 

 

Customer FoodItem 사이의 연결 엔티티 OrderItem

@Entity
public class OrderItem {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "order_id")
    private Order order;

    @ManyToOne
    @JoinColumn(name = "food_item_id")
    private FoodItem foodItem;

    private int quantity;
    private int price;
    private LocalDateTime orderDate;
}

 

 

  • OrderItem은 Order와 FoodItem 사이의 중간 테이블 역할  -> quantity, price, orderDate와 같은 추가 정보 저장 가능
  • 다중 @ManyToOne 설정: OrderItem에서 각각의 @ManyToOne 관계를 통해 Order와 FoodItem을 연결
@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;

    private String orderNumber;
    private LocalDateTime orderDate;

    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<>();
}

@Entity
public class FoodItem {
    @Id @GeneratedValue
    private Long id;

    private String name;
    private int price;

    @OneToMany(mappedBy = "foodItem")
    private List<OrderItem> orderItems = new ArrayList<>();
}

 

 

  • 다대다 관계를 1:1과 N:1로 분해 -> Order와 FoodItem은 각각 OrderItem과 1:1 관계를 가짐
  • 연결 엔티티를 통한 관계 관리: OrderItem을 통해 구매 수량과 가격 같은 정보를 유연하게 관리 가능

 


 

 

@JoinColumn이란?

@JoinColumn은 외래 키(FK) 컬럼을 명시적으로 설정하기 위해 사용
관계의 주인을 설정하며, 외래 키가 위치할 엔티티와 컬럼 이름을 지정하는 역할을 함

 

주요 속성

  1. name: 외래 키 컬럼의 이름을 지정합니다. 기본값은 참조 필드명_대상 테이블의 기본 키 이름 형식입니다.
  2. referencedColumnName: FK가 참조할 대상 테이블의 컬럼을 지정합니다. 생략 시 기본값은 대상 테이블의 기본 키 컬럼입니다.

 


 

연관관계별 @JoinColumn 위치와 역할

 

1.  1:1 관계

  • 외래 키가 위치한 엔티티가 관계의 주인

 

Customer가 CustomerDetail을 참조할 때, Customer에 @JoinColumn(name = "customer_detail_id") 설정.

 

@Entity
public class Customer {
    @Id @GeneratedValue
    private Long id;

    @OneToOne
    @JoinColumn(name = "customer_detail_id")
    private CustomerDetail customerDetail;
}

 

 

2.  1:N / N:1 관계

  • N:1 쪽 엔티티가 외래 키를 가지고 관계의 주인
  • 주로 @ManyToOne 쪽에 @JoinColumn을 설정하여 외래 키를 관리

 

Order에서 Customer를 참조할 때, Order @JoinColumn(name = "customer_id") 설정.

 

@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;
}

 

 

3.  N:M 관계

  • 중간 테이블을 생성해 다대다 관계를 1과 N:1로 풀어냄
  • 중간 테이블을 명시적으로 연결 엔티티로 생성하고, 각각의 @ManyToOne 관계에 @JoinColumn을 설정

 

Order에서 Customer를 참조할 때, Order @JoinColumn(name = "customer_id") 설정.

 

@Entity
public class OrderItem {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne
    @JoinColumn(name = "order_id")
    private Order order;

    @ManyToOne
    @JoinColumn(name = "food_item_id")
    private FoodItem foodItem;
}

 

 


 

 

연속성 전이 (Cascade)

연속성 전이란 한 엔티티가 다른 엔티티와 연관 관계를 맺고 있을 때, 특정 작업(예: 저장, 삭제, 업데이트 등)을 수행하면 자동으로 연관된 엔티티에도 해당 작업이 전이되도록 하는 기능

연속성 전이를 설정하면 여러 엔티티를 하나의 작업으로 관리할 수 있어 편리함

 

 

Cascade Type 종류

  • CascadeType.PERSIST: 연관된 엔티티를 함께 저장
  • CascadeType.MERGE: 연관된 엔티티를 함께 병합(업데이트)
  • CascadeType.REMOVE: 연관된 엔티티를 함께 삭제
  • CascadeType.REFRESH: 연관된 엔티티를 새로 고침
  • CascadeType.DETACH: 연관된 엔티티의 영속성 컨텍스트를 분리
  • CascadeType.ALL: 위의 모든 전이 유형을 포함

 

Customer를 저장할 때 해당 Customer에 연결된 Order들도 함께 저장

@Entity
public class Customer {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "customer", cascade = CascadeType.PERSIST)
    private List<Order> orders = new ArrayList<>();
}

@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;

    private String orderNumber;
    private LocalDateTime orderDate;

    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;
}

 

 


 

 

지연 로딩 (Lazy Loading)

지연 로딩은 연관된 엔티티를 실제로 필요할 때 조회하는 방식으로, 메모리와 성능 최적화에 도움을 줌
반대 개념으로 즉시 로딩(Eager Loading)이 있으며, 이는 엔티티를 로드할 때 연관된 모든 엔티티를 즉시 조회하는 방식

 

지연 로딩 설정

  • @ManyToOne @OneToOne은 기본적으로 즉시 로딩(EAGER)으로 설정
  • @OneToMany @ManyToMany는 기본적으로 지연 로딩(LAZY)으로 설정

 

지연 로딩 명시적 설정 예제

@OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
private List<Order> orders = new ArrayList<>();

 

 

Customer Order의 관계에서 Order를 지연 로딩하도록 설정한 예제

->  Customer 엔티티를 조회할 때 orders 리스트는 초기화되지 않으며, 실제로 접근하는 시점에 데이터베이스에서 조회

Customer customer = customerRepository.findById(customerId).orElseThrow();
// 여기서는 orders가 아직 로딩되지 않음
List<Order> orders = customer.getOrders(); // 이 시점에 데이터베이스에서 조회

 

 

N+1 문제

지연 로딩은 성능 최적화에 유용하지만, 반복적인 데이터베이스 접근을 초래할 수 있음

 

예를 들어, Customer리스트를 조회하면서 각각의 Order를 지연 로딩으로 가져오면 Customer 조회(1번) + Order 조회(N번)으로 인해 N+1 문제가 발생할 수 있음

-> Fetch Join을 사용하거나 @BatchSize를 설정하여 최적화

 

 

Fetch Join 사용 예제

: Customer Order를 한 번에 조회하여 N+1 문제를 해결

List<Customer> customers = em.createQuery("SELECT c FROM Customer c JOIN FETCH c.orders", Customer.class).getResultList();