-
JPA : Entity 연관 관계 / @JoinColumnBE/스프링 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) 컬럼을 명시적으로 설정하기 위해 사용
관계의 주인을 설정하며, 외래 키가 위치할 엔티티와 컬럼 이름을 지정하는 역할을 함주요 속성
- 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();
'BE > 스프링' 카테고리의 다른 글
JPA Auditing (0) 2024.11.14 QueryDSL + Projection (1) 2024.11.13