본문으로 건너뛰기
Advertisement

9.4 연관관계 매핑과 N+1 문제

RDBMS에서 테이블들은 외래 키(Foreign Key)를 통해 관계를 맺습니다(예: "게시글(1)은 여러 개의 댓글(N)을 가질 수 있다"). 객체 지향 세계에서 이를 어떻게 표현하고 매핑할지가 JPA의 가장 큰 난제이자 핵심입니다.

1:N (일대다), N:1 (다대일) 매핑

가장 흔한 게시글(Post)과 댓글(Comment)의 관계를 예로 들면 다(N) 쪽인 CommentPost의 PK를 외래 키로 가집니다. JPA에서는 외래 키를 가진 쪽(다 쪽)이 관계의 주인(Owner) 이 됩니다. 이것이 곧 N:1 매핑입니다.

// 다(N) 쪽 - Comment 엔티티
@Entity
public class Comment {
@Id @GeneratedValue
private Long id;

private String content;

@ManyToOne(fetch = FetchType.LAZY) // 실무에선 반드시 지연 로딩 사용
@JoinColumn(name = "post_id") // 실제 DB 테이블 생성 시 FK 컬럼명
private Post post;
}
// 일(1) 쪽 - Post 엔티티
@Entity
public class Post {
@Id @GeneratedValue
private Long id;

private String title;

// 양방향 매핑 (옵션). mappedBy는 반대쪽 객체에서 내 객체를 참조하는 필드명입니다.
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
private List<Comment> comments = new ArrayList<>();
}

데이터를 저장할 때는 comment.setPost(savedPost)처럼 다(N)쪽에 외래 키 객체를 주입하고 save()하면 알맞은 외래 키 값이 담겨서 INSERT 됩니다.

영속성 전이 (Cascade)

부모 엔티티(예: Post)를 저장하거나 지울 때, 그와 연관된 자식 엔티티들(Comment)도 자동으로 함께 저장하거나 지워지게 영속성을 전이시키고 싶다면 @OneToMany 쪽에 cascade = CascadeType.ALL 옵션을 줍니다. 이를 통해 게시글 삭제 시 게시글에 달린 수십 개의 댓글이 DB에서 한 번에 묶여 삭제(또는 저장)될 수 있습니다.

지연 로딩 (Lazy Loading)과 N+1 문제의 위협

JPA로 데이터베이스를 조회할 때 발생할 수 있는 가장 무서운 성능 저하 이슈가 바로 N+1 문제입니다.

  • 연관된 엔티티가 있을 때, 쿼리를 실행하면 부모 엔티티만 SELECT 하고(1), 연관된 컬렉션을 순회하며 부를 때마다 자식 쿼리(N번)가 따로 나가는 현상입니다.
  • 리스트에 댓글이 100개 달려 있다면 SELECT 쿼리가 1번(게시글) + 100번(댓글들) = 101번이나 날아가 서버 성능에 최악의 영향을 줍니다.
  • 이를 막기 위해 실무에서는 @xxxToOne 애너테이션의 기본값인 즉시 로딩(EAGER)을 절대 쓰지 않고 fetch = FetchType.LAZY 로 강제한 뒤, 진짜 필요할 때만 연관 데이터를 한 번의 SQL JOIN으로 확 퍼오는 기술인 Fetch Join(JOIN FETCH) 이나 EntityGraph 를 사용하게 됩니다.

JOIN FETCH, @EntityGraph, ON 조건, 페이징 시 주의점 등은 9.6 JPQL 조인과 고급 조회에서 상세히 다룹니다.

Advertisement