[JAVA] Entity 연관관계 완전 정복 – 1:N, N:M, Cascade까지 실무 중심 정리

Spring Data JPA를 쓰다 보면, 엔티티 간 관계 설정은 피할 수 없는 주제입니다. 잘못 설계하면 성능 문제부터 N+1, 무한 루프, 삭제 오류 등 여러 문제가 발생하죠

1:N, N:M 연관관계의 개념과 설정법, 그리고 Cascade 옵션의 의미와 주의할 점까지 실무 위주로 정리합니다.

 

1. 연관관계의 기본 개념

JPA는 객체지향적인 데이터 모델을 추구합니다. 이를 위해 @OneToMany, @ManyToOne, @ManyToMany 같은 어노테이션을 사용해 엔티티 간 관계를 정의합니다.

  • 1:N: 하나의 부모가 여러 자식을 가짐 (ex. 하나의 게시글에 여러 댓글)
  • N:1: 여러 자식이 하나의 부모를 참조함 (ex. 여러 댓글이 하나의 게시글을 참조)
  • N:M: 양쪽 다 다수 관계 (ex. 사용자와 역할 - 유저는 여러 역할을, 역할은 여러 유저를 가질 수 있음)

2. 1:N / N:1 연관관계 설정

예시: 게시글(Post)과 댓글(Comment)

@Entity
public class Post {
    @Id @GeneratedValue
    private Long id;

    private String title;

    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Comment> comments = new ArrayList<>();
}
@Entity
public class Comment {
    @Id @GeneratedValue
    private Long id;

    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;
}

주의할 점:

  • mappedBy는 연관관계의 주인이 아님을 의미 – 실제 외래키는 Comment 쪽이 관리
  • fetch = LAZY를 명시하지 않으면, 무조건 즉시 로딩(EAGER)되어 성능 문제가 발생할 수 있음

 

3. N:M 연관관계 설정

N:M은 가능하면 중간 테이블을 엔티티로 분리하는 것을 추천합니다.

예시: User와 Role

@Entity
public class User {
    @Id @GeneratedValue
    private Long id;

    private String username;

    @ManyToMany
    @JoinTable(name = "user_role",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id"))
    private Set<Role> roles = new HashSet<>();
}
@Entity
public class Role {
    @Id @GeneratedValue
    private Long id;

    private String roleName;

    @ManyToMany(mappedBy = "roles")
    private Set<User> users = new HashSet<>();
}

그러나 실무에서는 user_role 테이블 자체를 엔티티(UserRole 등)로 분리하여 생성일, 승인여부 등 추가 필드를 넣는 경우가 많습니다.

 

4. Cascade 옵션

Cascade는 연관된 엔티티의 생명주기를 함께 관리하겠다는 의미입니다.

  • PERSIST – 부모 저장 시 자식도 저장
  • MERGE – 병합 시 같이 병합
  • REMOVE – 삭제 시 자식도 삭제
  • ALL – 위 모든 옵션 포함

주의!

  • DELETE ON CASCADE처럼 작동하지만, 트랜잭션 단위로 동작하기 때문에 주의 깊게 설정해야 합니다.
  • 회원 - 주문 관계에서 주문 삭제 시 회원이 삭제되면 안 되듯, 항상 비즈니스 도메인에 맞게 Cascade를 결정하세요.

 

5. orphanRemoval 옵션

orphanRemoval = true는 컬렉션에서 빠진 자식 엔티티를 자동으로 삭제합니다. 단, 실제 DB DELETE가 발생하므로 정말 필요할 때만 설정해야 합니다.

 

6. 정리 및 실무 팁

  • 연관관계의 주인은 항상 @ManyToOne 쪽
  • LAZY 로딩을 기본값으로, 필요한 경우에만 EAGER
  • 복잡한 N:M은 중간 테이블을 별도 엔티티로 설계
  • 불필요한 Cascade 설정은 지양 – 삭제 오류, 데이터 유실 위험
  • orphanRemoval은 컬렉션 변경 시 DELETE 실행됨