TIL
🤝Entity 연관관계
초비비
2025. 2. 25. 22:24
JPA를 사용하면서 각 Entity간 연관관계를 매핑하는데
필요한 개념을 정리해보자!
왜 연관관계를 가져야 하는가?
객체지향적인 설계를 할 때, 현실 세계의 관계를 코드로 표현하기 위해 엔티티 간 연관관계를 정의한다. JPA에서는 연관관계를 맺어줌으로써 객체 그래프 탐색이 가능해지고, 연관된 데이터들을 효율적으로 조회할 수 있다. 또한, 적절한 연관관계 매핑을 통해 유지보수성을 높이고, 중복 데이터를 줄일 수 있다.
개발자는 현실 세계의 개념을 데이터베이스에 담아야 하는데, 이를 하나의 테이블로 관리할 수 없기 때문에 정규화를 통해 데이터를 나누고 분리하는 과정에서 연관 관계가 생긴다.
1:1 관계 (OneToOne)
하나의 Entity가 다른 Entity와 단 하나의 관계를 가지는 구조
예시
- User Entity
@Entity
public class User {
@Id @GeneratedValue
private Long id;
@OneToOne
@JoinColumn(name = "profile_id")
private UserProfile profile;
}
- User Profile
@Entity
public class UserProfile {
@Id @GeneratedValue
private Long id;
@OneToOne(mappedBy = "profile")
private User user;
}
주의 사항
테이블 설계 시 @OneToOne
관계에서 nullable 여부에 따라 Join을 수행하는 방식이 달라질 수 있다.
기본적으로 EAGER 로딩이므로 LAZY 설정이 필요하며, 외래 키가 있는 쪽에서만 지연 로딩이 정상 동작한다. 외래 키가 없는 쪽에서는 지연 로딩이 제대로 동작하지 않을 수 있다.
1:N 관계 (OneToMany, ManyToOne)
1. 단방향으로 한쪽 Entity에 일방적으로 매핑할 수 있음
@OneToMany
@JoinColumn(name = "team_id") // Member 테이블의 team_id 컬럼에 매핑
private List<Member> members = new ArrayList<>();
특징
@OneToMany 단방향 관계에서 @JoinColumn을 명시하지 않으면 내부적으로 중간 테이블이 자동 생성된다. 위 예시처럼 @JoinColumn을 명시하면 중간 테이블 없이 외래 키 방식으로 동작한다.
2. 양방향으로 각 Entity에 연관관계를 매핑할 수 있음.
// Team.java
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
// Member.java
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
1:N 관계의 주의사항
- 연관관계의 주인은 N쪽(@ManyToOne)이어야 한다
JPA에서는 @ManyToOne 쪽이 연관관계의 주인이 되어야 제대로 동작한다.
따라서 1:N 관계에서는 N쪽(다수의 쪽)이 주인 역할을 하게 된다. - Collection 초기화가 필요하다
예를 들어, List<Member> members = new ArrayList<>();와 같이 컬렉션을 초기화하지 않으면 NullPointerException이 발생할 수 있다. JPA에서는 연관된 엔티티 컬렉션을 초기화해야 하며, 그렇지 않으면 객체가 null 상태로 남아 있을 수 있다. - 성능 문제 해결을 위한 Batch Size 설정(@BatchSize)
1:N 관계에서 다수의 연관된 엔티티를 처리할 때, N+1 문제가 발생할 수 있는데 @BatchSize를 설정하여 해결할 수 있다. 이 설정은 연관된 엔티티를 일괄 처리하는 방식으로 성능을 개선하는 데 도움이 된다.
더보기
- @BatchSize를 사용하지 않을 경우:
- Team 10개를 조회하는 쿼리 1번
- 각 Team에 속한 Member를 조회하는 쿼리 10번 (각 Team마다 1번씩)
- 총 11번의 쿼리 실행
- @BatchSize(size=5)를 사용할 경우:
- Team 10개를 조회하는 쿼리 1번
- Team의 ID를 5개씩 묶어서 IN 절로 Member를 조회하는 쿼리 2번
- 첫 번째 쿼리: WHERE team_id IN (1, 2, 3, 4, 5)
- 두 번째 쿼리: WHERE team_id IN (6, 7, 8, 9, 10)
- 총 3번의 쿼리 실행
즉, 모든 Member 데이터는 다 가져오지만, 효율적으로 일괄 처리하여 쿼리 실행 횟수를 줄이는 것
이를 통해 데이터베이스 통신 오버헤드를 줄이고 성능을 개선할 수 있음
- 예시코드
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "team")
@BatchSize(size = 100) // 최대 100개씩 일괄 조회
private List<Member> members = new ArrayList<>();
}
이유
- 연관관계 주인의 설정은 외래 키 관리의 일관성을 유지하고, 실제 데이터베이스 구조와 맞는 방식으로 연관관계를 관리하는 데 필요
- 컬렉션 초기화는 연관된 객체에 접근하기 전에 null 값을 방지하고, 예상치 못한 오류를 예방하기 위해 필요
- Batch Size 설정은 대량의 데이터를 처리할 때 성능을 최적화하여, N쪽 엔티티를 한 번에 로드하고 처리하는 방식으로 불필요한 쿼리 호출을 줄여 성능을 향상시킬 수 있음
N:M 관계
@JoinTable
을 사용하여 매핑할 수 있다.
Student Entity
@ManyToMany
@JoinTable(name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id"))
private List<Course> courses = new ArrayList<>();
별도의 Entity를 따로 분리하여 매핑할 수 있다.
StudentCourse Entity
@Entity
public class StudentCourse {
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
@ManyToOne
@JoinColumn(name = "course_id")
private Course course;
private LocalDateTime registeredAt; // 추가 필드 가능
}
정리
관계 유형 | 어노테이션 | 주인 설정 위치 |
1:1 | @OneToOne | 접근 빈도 높은 쪽 |
1:N | @OneToMany | 양방향 |
N:1 | @ManyToOne | N쪽 |
N:M | @ManyToMany | 양쪽 다 가능 (접근 빈도 높은 쪽) |
- 1:1 관계
@OneToOne 어노테이션을 사용하며, 주인 설정은 접근 빈도가 높은 쪽에 두는 것이 일반적인 권장 사항
보통 @OneToOne 관계에서 외래 키를 어느 테이블에 두느냐에 따라 주인을 설정 - 1:N 관계
@OneToMany와 @ManyToOne을 양방향 관계로 설정할 때, 주인은 항상 N쪽 (@ManyToOne)
@OneToMany는 N쪽에서 주인을 설정해야 하며, 이를 통해 연관관계를 관리 - N:M 관계
다대다(N:M) 관계는 일반적으로 연결 엔티티(중간 테이블)를 사용하여 관리하며, 이 경우 연결 엔티티가 주인이 됨@ManyToMany 대신 연결 엔티티를 사용하여 관계를 정의하는 경우, 연결 엔티티에서 연관관계의 주인이 됨