JPA에서 @OneToMany 와 @ManyToOne 은 관계를 설정할 때 너무나 자주 사용하는 어노테이션입니다.
각각 1:N , N:1 의 관계를 의미하고, 관계성이 있는 엔티티를 설정할 때 필수적으로 사용되기에, 간단하게 정리하고자 합니다.

어노테이션에 대한 설명에 앞서, 간단하게 엔티티 간의 연관관계를 설명하고자 합니다.
| 연관관계 | JPA 어노테이션 |
|---|---|
| 1:1 | @OneToOne |
| 1:N | @OneToMany |
| N:1 | @ManyToOne |
| N:M | @ManyToMany |
위와 같이, 엔티티 간에는 여러 종류의 연관관계가 설정될 수 있습니다.
그 중, 1:N 과 N:1 에 해당하는 @OneToMany 와 @ManyToOne 가 가장 많이 쓰입니다.
예를 들어 1:N 연관관계는 1에 해당하는 부모엔티티에 N에 해당하는 자식 엔티티가 연관되어 있는 것을 의미합니다.
예를 들어, 위 그림에서 Post 엔티티는 Comment 엔티티 객체 list인 comments, Tag 엔티티 객체 list인 tags를 각각 컬럼으로 가지고 있습니다.
이와 같이 관계를 설정하면서, 부모 엔티티가 삭제되었을 때 자식 엔티티를 처리하는 것, 부모 엔티티의 영속성 설정 시 자식 엔티티로의 영향도 설정 등 DB 관리시 편리한 옵션을 많이 사용할 수 있게 됩니다.
@Entity
class Post(
createdBy: String,
title: String,
content: String,
tags: List<String> = emptyList(),
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long
var createdBy: String = createdBy
private set
var title: String = title
private set
var content: String = content
private set
@OneToMany(mappedBy = "post", orphanRemoval = true, cascade = [CascadeType.ALL])
var comments: MutableList<Comment> = mutableListOf()
protected set
@OneToMany(mappedBy = "post", orphanRemoval = true, cascade = [CascadeType.ALL])
var tags: MutableList<Comment> = mutableListOf()
protected set
}
@Entity
public class Comment(
createdBy: String,
content: String,
post: Post,
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L
var createdBy: String = createdBy
protected set
var content: String = content
protected set
@ManyToOne(fetch = FetchType.LAZY)
var post: Post = post
protected set
}
@Entity
@Getter @Setter
Tag(
name: String,
post: Post,
createdBy: String,
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L
var name: String = name
protected set
@ManyToOne(fetch = FetchType.LAZY)
// 외래키 제약 조건을 제거하기 위해 NO_CONSTRAINT 설정
@JoinColumn(foreignKey = ForeignKey(ConstraintMode.NO_CONSTRAINT))
var post: Post = post
protected set
}
위와 같이 엔티티를 정의하면서, 포스트 처음에 있었던 다이어그램의 엔티티 3개(Post, Comment, Tag) 가 정의되었습니다.
양방향의 1:N(N:1) 연관관계 2개가 설정되었습니다.
부모 엔티티인 Post에서는 두 개의 자식 엔티티인 Comment와 Tag에 @OneToMany 어노테이션으로 1:N 연관관계를 설정하였고,
자식 엔티티인 Comment와 Tag에서는 @ManyToOne 어노테이션으로 부모 엔티티인 Post에 N:1 연관관계를 설정하였습니다.
이제 위 코드에서 사용된 옵션과 어노테이션들에 대해 조금 더 상세히 알아보겠습니다.
@id
객체의 필드를 DB 테이블의 기본 키(PK)와 매핑시키기 위해 사용되는 어노테이션입니다.
모든 엔티티에 사용된 id 필드를 데이터베이스 테이블의 기본 키(PK)에 매핑시키기 위해 @Id 어노테이션을 사용하였습니다.
만약 @Id만 사용할 경우 기본 키를 직접 할당해 주어야 합니다.
위 예시에서는 이를 피하기 위해 데이터베이스가 생성해주는 값을 사용하기로 하였고, 이를 위해 @GeneratedValue를 사용하였습니다.
@GeneratedValue
기본 키를 자동 생성해주는 어노테이션입니다.
@GeneratedValue는 기본 키를 자동으로 생성하는 어노테이션이고, 그 전략을 설정합니다.
전략의 종류에는 아래와 같이 있습니다.
| 전략 | 내용 |
|---|---|
| GenerationType.AUTO | 기본 설정 값, 각 DB에 따라 기본키를 자동으로 생성 |
| GenerationType.IDENTITY | 기본키 생성을 DB에게 위임하는 방식. id값을 따로 할당하지 않아도 DB가 자동으로 AUTO_INCREMENT를 하여 기본키를 생성 |
| GenerationType.SEQUNCE | DB의 Sequence Object를 사용하여 DB가 자동으로 기본키를 생성 |
| GenerationType.TABLE | 키를 생성하는 테이블을 사용하는 방법으로 Sequence와 유사 |
위 예시에서는 GenerationType.IDENTITY 를 사용하여 DB가 자동으로 기본키를 생성하는 전략을 취했습니다.
연관관계의 관리 주체, 즉 주인을 설정하기 위해
mappedBy옵션을 사용합니다.
어떤 엔티티가 DB에 저장되는 FK를 삽입, 수정, 삭제를 담당할 지를 정하는 과정에서, 주인 엔티티가 정해집니다.
즉, FK 관리의 책임에 따라 정해지는 것이므로, 부모 엔티티와 자식 엔티티 모두 주인 엔티티가 될 수 있습니다.
헷갈린다면, FK(외래 키)가 있는 곳을 주인으로 지정하면 됩니다.
@OneToMany(mappedBy = "post", orphanRemoval = true, cascade = [CascadeType.ALL])
위 예시에서도 자신이 이 연관관계의 주인이 아님을 표시하기 위해 1:N 에서 1에 해당하는 Post 에서 mappedBy 옵션 설정을 하였습니다.
mappedBy = "post" 와 같은 형식으로 반대쪽(Comment Tag)에 자신이 매핑되어 있는 필드명(post)을 써주었습니다.
orphanRemoval옵션은 연관관계가 끊긴 엔티티에 대해서 REMOVE 작업을 진행하고 전파할 지에 대한 옵션입니다.
부모 엔티티와 자식 엔티티 사이의 연관관계를 삭제할때, 해당 자식 엔티티는 고아객체가 됩니다.
즉, 고아객체는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 가리킵니다.
여기서 orphanRemoval = true 로 하면, 이 고아객체들을 자동으로 삭제 처리해줍니다.
위 예시에서, Post 엔티티의 데이터가 삭제되면, 이와 연결되어 있던 자식 엔티티인 Content 와 Tag 엔티티가 자동으로 함께 삭제될 것입니다.
다만, CASCADE.REMOVE 설정을 걸기 위해 중복 적용을 막고자 orphanRemoval = false 설정을 하는 경우도 있습니다.
영속성 전파를 설정하게 되면, 객체에 해당 작업이 이루어질 때 자식엔티티에도 작업이 전파됩니다.
CascadeType으로 6가지를 줄 수 있습니다.
| CascadeType | 내용 |
|---|---|
| CascadeType.ALL | 아래 6개의 모든 Cascade를 적용 |
| CascadeType.PERSIST | 엔티티를 영속화할 때, 자식 엔티티도 함께 유지 |
| CascadeType.MERGE | 엔티티 상태를 병합(Merge)할 때, 자식 엔티티도 모두 병합 |
| CascadeType.REMOVE | 엔티티를 제거할 때, 자식 엔티티도 모두 제거 |
| CascadeType.DETACH | 영속성 컨텍스트에서 엔티티 제거. 부모 엔티티를 detach() 수행하면, 자식 엔티티도 detach()상태가 되어 변경 사항을 반영하지 않음 |
| CascadeType.REFRESH | 부모 엔티티를 새로고침(Refresh)할 때, 자식 엔티티도 모두 새로고침 |
해당 객체를 DB에서 조회할 때, 연관관계에 있는 엔티티의 정보를 즉시 같이 로딩을 할 것인지, 아니면 조회를 하는 시점에 지연 로딩을 할 것인지에 대한 옵션입니다.
Lazy Fetch
엔티티에 접근하는 시점에 DB에 쿼리를 날려 엔티티를 조회합니다.
비즈니스 로직 설계 시, 자식 엔티티와 부모 엔티티가 같이 사용되는 일이 많이 없다면, 자식 엔티티를 조회할 때마다 모든 엔티티에 쿼리를 날릴 필요가 없습니다.
그렇다면 자식 엔티티 @ManyToOne 어노테이션의 fetch 속성을 FetchType.LAZY 로 지정하여 팀 엔티티의 조회 시점을 실제 해당 객체가 사용될때로 늦출 수 있습니다.
Eager Fetch
상대 엔티티의 조회 여부와 상관없이, 쿼리가 발생하게 됩니다.
반대로 로직 설계 시, 자식 엔티티와 부모 엔티티가 거의 항상 같이 사용된다면, 자식 엔티티 @ManyToOne 어노테이션의 fetch 속성을 FetchType.EAGER 로 지정하여 부모 엔티티의 조회 시점을 자식 엔티티의 조회 시점과 동일 시 하게 할 수 있습니다.
사실 이 부분은 따로 다른 포스트에서 따로 다룰 예정입니다.
내용이 워낙 크기 때문에 간단하게만 짚고 넘어가도록 합시다.
@JoinColumn
주로 외래 키(Foreign Key) 매핑을 세부적으로 설정할 때 사용됩니다.
자주 사용되는 속성은 name 으로, 매핑할 외래키의 이름을 지정할 때 사용합니다.
하지만 위 예시에서는 다른 옵션을 사용했습니다.
@JoinColumn(foreignKey = ForeignKey(ConstraintMode.NO_CONSTRAINT))
위 예시에서는 외래키 제약 조건을 물리적으로 제거하기 위해 위와 같은 옵션을 사용하였습니다.
https://velog.io/@goniieee/JPA-OneToMany-ManyToOne으로-연관관계-관리하기
https://ttl-blog.tistory.com/123
https://velog.io/@gudnr1451/GeneratedValue-정리