일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- builder
- 일급 객체
- Dependency Injection
- Volatile
- 일급 컬렉션
- middleware
- Google OAuth
- lombok
- java
- synchronized
- Spring
- spring security
- nestjs
- OAuth 2.0
- factory
- Today
- Total
HJW's IT Blog
JPA - Entity들 사이의 연관관계 본문
0. 들어가며
JPA 의 entity 사이에 연관관계를 올바르게 설정하는것은 매우 중요하다. 데이터 무결성, 성능, 유지보수성 측면 모두에서 영향을 끼칠 수 있는것이 어떻게 연관 관계를 설정하는가 이다. 이번 글에서는, 연관관계의 주인 개념을 명확하게 짚고 넘어가겠다.
1. 연관관계란 무엇인가
1.1 객체 지향 모델링 관점에서의 영향관계
객체 지향 프로그래밍에서 연관관계는 하나의 객체가 다른 객체를 참조하거나 포함하는 구조를 의미한다. 즉, 객체간의 상호작용을 모델링할 수 있고, 현실의 개념을 더욱 직관적으로 표현할 수 있다.
예를 들어 User
와 Order
이 있다고 가정하자. User
는 여러 Order
를 가질 수 있으며, Order
는 한명의 User
를 가진다. 이러한 관계를 어떻게 표현하고 설계할 것인지가 관건이다.
1.2 데이터베이스 테이블 간의 관계와 매핑
관계형 DB에선 테이블 간의 관계를 통해 데이터를 저장하고 관리한다. 이때, FK를 사용하여 한 테이블이 다른 테이블을 참조하고 있음을 나타낸다. 위와 같은 User
, Order
상황에서, Order
테이블은 Member
의 PK 를 FK 로 가지고 있을 것이다.
1.3 연관관계의 종류
1.3.1 One to One
한 엔티티가 다른 하나의 엔티티와 1 : 1 로 연관되는 관계이다. 예를 들어 User
와 UserDetail
이 있다면, 하나의 User
는 하나의 UserDetail
을 가지고, 하나의 UserDetail
은 한명의 User
를 가질 것이다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@OneToOne
@JoinColumn(name = "user_detail_id")
private UserDetail userDetail;
}
1.3.2 One to Many
하나의 엔티티가 여러 개의 다른 엔티티와 관계를 가지는 구조이다. 예를 들어 User
와 Order
는 One to Many 관계이다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "user")
private List<Order> orders = new ArrayList<>();
}
1.3.3 Many to One
여러 엔티티가 하나의 엔티티를 참조하는 관계이다. Order
와 User
는 Many to One 관계이다.
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
1.3.4 Many to Many
여러 엔티티가 여러 엔티티와 관계를 가지는 구조이다. 대표적인 예로 Student
와 Course
가 있는데, 한명의 Student
는 여러 Course
를 들을 수 있고, 하나의 Course
는 여러 Student
를 가질 수 있다.
@Entity
public class Student {
@Id @GeneratedValue
private Long id;
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private List<Course> courses = new ArrayList<>();
}
이와 같이 JoinTable 을 통해 중간 테이블을 생성해 주어야 한다.
2. 단방향 vs 양방향 연관 관계
단방향 연관관계
한 엔티티가 다른 엔티티를 참조하지만 반대 방향에서는 참조하지 않는 상황이다.
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
Order
는 User
를 참조하지만, User
는 Order
를 참조하지 않는다.
양방향 연관관계
양쪽 엔티티가 서로를 참조하는 구조이다. 즉, User
와 Order
모두 서로를 참조한다.
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
}
양방향 연관관계를 사용할 때는 주의해야 할 점이 있다.
- 순환 참조 문제 : 양방향 관계를 설정할 때, toString(), hashCode(), equals() 를 잘못 구현하면 무한 루프가 생긴다
- 성능 문제 : 양방향 관계를 사용할 경우 불필요한 쿼리가 증가한다. (fetch = FetchType.LAZY 를 적극 활용해야 한다)
- 연관관계의 주인 설정 : 양방향 관계에선 반드시 주인 을 지정해야 한다 (
mappedBy
)
3. 왜 연관관계 주인이 필요한가
JPA 는 객체와 RDB 사이의 mapping 을 자동으로 관리할 수 있게 해준다. 하지만 어느 엔티티가 FK 를 관리할 것인지를 명확하게 지정해야 데이터 정합성을 지킬 수 있다. 잘못된 연관관계로 인한 문제점은 : 잘못된 데이터 저장, 중복 쿼리 발생, 데이터 정합성 등이 있다.
JPA 는 영속성 컨텍스트를 통해 엔티티를 관리한다. 이때, 엔티티의 연관관계가 어떻게 설정되었는가에 따라 DB에 변동사항을 감지하고 적절한 쿼리를 실행하게 된다. 하지만 주인이 없는 경우, JPA 는 관계를 올바르게 저장하지 못할 가능성이 있다. 그렇기에 어느 엔티티가 외래 키를 관리할 지 명확하게 설정해야 한다.
3.1 mappedBy
mappedBy
속성은 연관관계의 주인이 아닌 엔티티에서 사용된다. 이때, 관계를 READONLY로 설정한다.
즉, 연관관계의 주인이 아닌 엔티티에서 외래 키를 관리하지 않도록 지정하는 역할을 한다.
@Entity
public class User {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "user")
private List<Order> orders = new ArrayList<>();
}
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "user_id") // 연관관계의 주인
private User user;
}
- 이렇게 설계하면, JPA 가 데이터 변경을 올바르게 반영할 수 있으며, 주인이 아닌 엔티티에서 불필요한 업데이트 쿼리의 실행을 방지할 수 있다.
3.2 관계 별 주인 가이드라인
One to One
- 외래키를 가진 엔티티가 주인
One to Many, Many to One
- 일반적으로 Many (N) 쪽이 주인이 된다 (FK 를 관리하기 때문)
Many to Many
- Many to Many 관계는 보통 중간 테이블을 사용하여 1:N, N:1 관계르 매핑한는 것이 일반적이다. 즉, 둘 중 어떤 엔티티가 주인이 될 지는 도메인 모델 설계에 따라 결정된다.
4. 연관관계 설정시 주의 사항
4.1 잘못된 주인 설정의 문제점
연관관계의 주인을 잘못 설정하게 되면 데이터 일관성 문제가 발생한다. 양방향 연관관계에서 비주인 엔티티에서만 관계를 설정하면, 데이터베이스에 반영이 되지 않는다. 이 관계는 JAVA 코드 내에서의 관계이며 비주인 엔티티의 필드는 READ ONLY 이기 때문이다.
Member member = new Member();
Order order = new Order();
member.getOrders().add(order); // 관계 설정
em.persist(member);
order 가 관계의 주인일 경우, 이 코드는 member 와 order 의 관계가 설정되지 않는다. order.setMember(member);
를 호출하지 않으면 order_id
가 저장되지 않는다.
4.2 무한루프 발생 및 StackOverflow
양방향 연관관계를 맺고, JSON 으로 객체를 직렬화 할때, 한 객체에서 다른 객체를 참조, 다시 반대편에서도 기존 객체를 참조하는 구조라면, 무한 재귀 호출이 발생한다.
4.3 N + 1 문제
연관 관계가 설정된 엔티티를 조회할 경우에 조회된 데이터 갯수(n) 만큼 연관관계의 조회 쿼리가 추가로 발생하여 데이터를 읽어오는 현상을 n + 1
문제라 한다. 예를 들어, 하나의 Member를 불러올 때, 연관된 여러 Order를 같이 조회하려 하면 발생하는 문제이다.
@Entity
public class Member {
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
@Entity
public class Order {
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
}
이 경우, 한 'Member' 엔티티를 조회할 때 연관된 'Order' 엔티티들도 함께 조회하려고 하면, 'Order'의 수만큼 추가적인 쿼리가 발생한다.
해결방법
JPQL Fetch Join
List<Order> orders = em.createQuery( "SELECT o FROM Order o JOIN FETCH o.member", Order.class).getResultList();
위와 같이 개별적인 쿼리를 여러번 보내는 것이 아닌, 하나의 쿼리로 보든 데이터를 조회할 수 있다.
EntityGraph
@EntityGraph(attributePaths = {"member"})
@Query("SELECT o FROM Order o")
List<Order> findAllWithMember();
이 두가지 해결방법에 대해서는 추가 공부 후 포스팅하겠다
4.4 CascadeType, OrphanRemoval
CascadeType Options
- ALL : 모든 상태 변화를 전파
- PERSIST : 부모 엔티티 저장시, 연관 자식 엔티티도 잠께 저장
- REMOVE : 부모 엔티티 삭제시, 연관된 엔티티도 함께 삭제
- MERGE : 부모 엔티티 병합시, 연관 자식 엔티티도 병합
- DETACH : 부모가 영속성 컨텍스트에서 분리되면 자식 엔티티도 함께 분리
- REFRESH : 부모 엔티티가 새로 고쳐질때, 자식 엔티티도 새로고침
OrphanRemoval
- 연관관계가 끊어진 엔티티를 DB에서도 자동 삭제하는 옵션이다.
-
@Entity public class Member { @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List<Order> orders = new ArrayList<>(); } Member member = em.find(Member.class, 1L); member.getOrders().remove(0); // DB에서도 해당 Order 삭제됨
위 옵션들을 잘못 사용하면 큰 문제가 발생할 수 있다. 예를 들어 CascadeType.ALL
을 사용했을때, 하나의 자식 엔티티가 여러 부모 엔티티와 관계가 있다고 가정하자. 이때 하나의 부모에 이 옵션이 정해져 있다면? 삭제 되지 말아야 할 자식 엔티티도 삭제되는 것이다.
OrphanRemoval 의 경우도 동일하다. 단순히 관계만 끊고 싶은 상황에서, 데이터가 날아가 버릴 수 도 있다.
'JAVA' 카테고리의 다른 글
JPA 연관관계에서 프록시 객체의 역할과 한계 (0) | 2025.02.26 |
---|---|
JPA - EntityManager와 영속성 컨텍스트 이해하기 (0) | 2025.02.25 |
DTO ↔ 엔티티 변환, MapStruct로 자동화하기 (0) | 2025.02.18 |
Spring 없이 의존성 관리와 팩토리 패턴 구현하기 (0) | 2025.01.24 |
JVM 은 어떻게 동작하는가 (1) | 2025.01.24 |