JPA : Entity 연관 관계 / @JoinColumn
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) 컬럼을 명시적으로 설정하기 위해 사용
관계의 주인을 설정하며, 외래 키가 위치할 엔티티와 컬럼 이름을 지정하는 역할을 함
주요 속성
- name: 외래 키 컬럼의 이름을 지정합니다. 기본값은 참조 필드명_대상 테이블의 기본 키 이름 형식입니다.
- 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();