1:N(@OneToMany) = 하나의 엔티티가 여러 엔티티를 참조(게시글 - 댓글)N:1(@ManyToOne) = 여러 엔티티가 하나의 엔티티를 참조(댓글 - 게시글)1:1(@OneToOne) = 하나의 엔티티가 하나의 엔티티를 참조(사용자 - 프로필)N:M(@ManyToMany) = 여러 엔티티가 서로 다수의 엔티티를 참조(학생 - 수업)N:M은 되도록 사용하지 않고 중간 매핑 테이블을 둬서 1:N + N:1로 풀어내는 방식을 사용| 구분 | 설명 | 특징 |
|---|---|---|
| 단방향 | 한쪽에서만 참조 | 설계 단순함, 관리 쉬움 |
| 양방향 | 양쪽에서 참조 | 객체 그래프 탐색 유리, 하지만 관리 주의 필요, 양 매핑 필드 관리 요구됨 |
JPA에서는 양방향 매핑 시 반드시 연관관계의 주인을 정해야 함
주인은 실제로 외래키를 관리하는 엔티티로 선정, 반대쪽은 단순히 읽기 전용으로 사용
주인 객체의 선정은 비즈니스적인 중요도와는 별개, 즉 게시글과 댓글을 매핑할 때 게시글이 비즈니스 적으로 중요하다고 하더라도 외래키를 관리하는 댓글이 주인객체가 됨
코드 예시
@Entity
class Post(
@Id
@GeneratedValue
val id: Long? = null,
@OneToMany(mappedBy = "post", cascade = [CascadeType.ALL], orphanRemoval = true)
val comments: MutableList<Comment> = mutableListOf()
)
@Entity
class Comment(
@Id
@GeneratedValue
val id: Long? = null,
// 주인객체(외래 키 관리)
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "post_id")
var post: Post
)
mappedBy 사용된 쪽)은 읽기 전용이므로 insert/update 쿼리를 발생시키지 않음| 전략 | 어노테이션 | 특징 |
|---|---|---|
| LAZY (지연 로딩) | fetch = FetchType.LAZY |
실제 참조할 때 SELECT 발생 |
| EAGER (즉시 로딩) | fetch = FetchType.EAGER |
연관된 엔티티를 즉시 가져옴 |
LAZY가 기본 표준처럼 사용됨EAGER는 불필요한 조인, N + 1 문제 발생 위험이 크기 때문에 사용을 지양하는 편EAGER 형식으로 조회할 때 루프마다 추가 SELECT가 나가는 문제SELECT가 나가는 문제Fetch Join 사용
@Query("SELECT p FROM Post p JOIN FETCH p.comments")
fun findAllWithComments(): List<Post>
N + 1 문제를 해결EntityGraph
@EntityGraph(attributePaths = ["comments"])
@Query("SELECT p FROM Post p")
fun findAllWithEntityGraph(): List<Post>
fetch join과 비슷하게 동작Batch Size
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 100
FK를 묶어서 IN 쿼리로 영속성 관리Projection 또는 DTO 조회로 전환
@Query("SELECT new com.example.PostWithCommentCountDto(p.id, p.title, COUNT(c))
FROM Post p LEFT JOIN p.comments c
GROUP BY p.id")
fun findPostsWithCommentCount(): List<PostWithCommentCountDto>