@ManyToOne과 @OneToMany
관계형 데이터베이스에서는 외래 키(Foreign Key, FK)를 사용하여 테이블 간의 관계를 맺지만, JPA는 이 DB 테이블 간의 관계를 객체 지향적으로 표현할 수 있도록 어노테이션을 제공하며, @ManyToOne과 @OneToMany가 그 핵심입니다.
@ManyToOne은 여러 개가 하나의 대상과 관계를 맺을 때 "여러 개" 쪽에 붙는 어노테이션입니다.
@OneToMany는 하나의 대상이 여러 개와 관계를 맺을 때 "하나" 쪽에 붙는 어노테이션입니다.
post(게시물)에 comment(댓글)을 적용하는 기능이 있다면 하나의 게시물에 여러 댓글이 달립니다.
@Entity
@Getter @Setter
@NoArgsConstructor
@Table(name = "posts")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
// 양방향 매핑: 이 게시물에 달린 댓글들
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Comment> comments = new ArrayList<>();
// 편의 메서드 (양방향 관계 설정 시 필수!)
// 댓글 추가 시 Comment 객체에도 Post를 설정해줌으로써 양쪽 관계를 일치시킴
public void addComment(Comment comment) {
this.comments.add(comment);
comment.setPost(this);
}
public void removeComment(Comment comment) {
this.comments.remove(comment);
comment.setPost(null);
}
}
@Entity
@Getter @Setter
@NoArgsConstructor
@Table(name = "comments")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String text;
// 다대일(N:1) 관계: 여러 Comment는 하나의 Post에 속함
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false) // null 허용 안함 (댓글은 항상 게시물에 속해야 함)
private Post post; // 외래 키가 매핑될 Post 엔티티 객체
// 편의 메서드 (setter 대신 사용 가능)
public Comment(String text, Post post) {
this.text = text;
this.setPost(post); // setPost 호출하여 양방향 관계 설정
}
}
- 관계의 주인(Owning Side): 외래 키(FK)를 가지고 있는 엔티티가 관계의 주인입니다. @ManyToOne이 있는 쪽이 항상 관계의 주인입니다. Comment 엔티티가 post_id 외래 키를 가지므로, Comment의 post 필드가 관계의 주인입니다.
- mappedBy: 관계의 주인이 아닌 쪽 (@OneToMany 쪽)에서 mappedBy 속성을 사용하여 주인이 누구인지 명시합니다. mappedBy의 값은 관계의 주인 엔티티의 필드명과 일치해야 합니다.
- 연관 관계 편의 메서드: 양방향 관계를 설정할 때, 한쪽만 객체 참조를 변경하면 데이터베이스의 외래 키와 객체 간의 불일치가 발생할 수 있습니다. 이를 방지하기 위해 양쪽 엔티티의 관계를 항상 일치시키는 연관 관계 편의 메서드 를 구현하는 것이 강력히 권장됩니다.
지연로딩 LAZY
지난 포스트에서는 영속성과 생명주기와 관련된 포스트를 작성한 적이 있습니다.
https://mjkim1201.tistory.com/5
Spring Boot JPA 영속성 완벽 가이드: 엔티티 생명주기 이해하기
오늘은 Spring Boot에서의 핵심 개념인 영속성에 대해서 알아보려고 합니다. JPA의 영속성 관리, 엔티티 생명주기, 그리고 영속성 컨텍스트에 대해 자세히 알아보겠습니다.• 영속성이란 무엇인가?
mjkim1201.tistory.com
이름 그대로 "지연" 로딩, 즉 필요할 때까지 로딩을 미루는 방식입니다. 엔티티 조회 시 부모 엔티티(예: Order)를 데이터베이스에서 조회할 때, @OneToMany나 @ManyToOne 등으로 연관된 자식 엔티티 는 즉시 로드되지 않습니다.
// Post 엔티티를 조회
Post post = entityManager.find(Member.class, 1L);
System.out.println("게시글 이름 : " + post.getName()); // Post 엔티티의 이름은 즉시 로드됨
// post.getComment()를 호출하는 순간, comment 목록이 로드됨
// (만약 @OneToMany(mappedBy = "post", fetch = FetchType.LAZY) 였다면)
List<Comment> comments = post.getComment(); // 이 시점에 comment 컬렉션 프록시가 초기화되며 DB에서 주문 목록을 가져옴
for (Comment comment : comments) {
System.out.println("댓글 id: " + comment.getId());
}
// comment.getPost()를 호출하는 순간, Post 엔티티가 로드됨
// (만약 @ManyToOne(fetch = FetchType.LAZY) 였다면)
Comment comment = comments.get(0);
Post PostComment = comment.getPost(); // 이 시점에 Post 프록시가 초기화되며 DB에서 회원 정보를 가져옴
System.out.println("댓글의 게시물 이름: " + PostComment.getName());
필요한 데이터만 로드하므로 메모리 사용량과 네트워크 트래픽을 줄일 수 있습니다. 예를 들어, Post 정보를 조회할 때 항상 모든 comment 목록이 필요한 것은 아닙니다. @OneToMany와 같은 컬렉션 관계는 기본적으로 LAZY이기 때문에, 처음에는 부모 엔티티만 조회하고, 컬렉션에 접근할 때에야 자식 엔티티를 조회합니다. 이렇게 N+1 쿼리를 유발하지 않고 지연 로딩합니다.
(물론, JOIN FETCH를 사용하지 않고 컬렉션을 순회하면 다시 N+1 쿼리 문제가 발생할 수 있습니다.)
필요한 경우에만 명시적으로 JOIN FETCH나 EntityGraph를 사용하여 특정 관계를 EAGER 로딩처럼 미리 가져올 수 있습니다.
caseCade
부모엔터티 영속성 작업을 자식에게 전파하는 행위로서 부모(OneToMany) 객체 에서 set을 하고 저장을 한다면,
자식 엔터티를 영속성으로 적용하지 않아도, 자식 엔터티가 자동으로 영속성으로 반영되고 db에 반영 됩니다.
예시 시나리오 내용 : 주문에 아이템을 넣고 주문을 저장했을때 아이템도 같이 저장된다.
- createOrder() 메서드에서 em.persist(order)만 호출해도, cascade = CascadeType.ALL 덕분에 order에 연결된 item1과 item2도 자동으로 영속성 컨텍스트에 등록(PERSIST)되고, 트랜잭션 커밋 시점에 DB에 함께 INSERT 됩니다.
[ CascadeType.ALL 이 포함하는 내용 ]
- PERSIST: 부모 엔티티를 영속화(저장)할 때, 연관된 자식 엔티티도 함께 영속화합니다.
- MERGE: 부모 엔티티를 병합(수정)할 때, 연관된 자식 엔티티도 함께 병합합니다.
- REMOVE: 부모 엔티티를 삭제할 때, 연관된 자식 엔티티도 함께 삭제합니다.
- REFRESH: 부모 엔티티를 DB에서 다시 로드할 때, 연관된 자식 엔티티도 함께 다시 로드합니다.
- DETACH: 부모 엔티티를 영속성 컨텍스트에서 분리할 때, 연관된 자식 엔티티도 함께 분리합니다.
orphanRemoval
orphanRemoval = true는 CascadeType.REMOVE의 기능을 포함하면서도, 더 강력한 고아(orphan) 객체 제거 기능을 제공합니다. 부모엔터티(OneToMany) 에서 참조를 잃은 자식(ManyToOne) 자식 엔터티에 대한 DB 관리(삭제) 자식엔터티가 부모 없이 독립적으로 존재할 수 없습니다.
부모 엔티티는 그대로 두고, 컬렉션에서 특정 자식 엔티티의 참조만 제거하는 경우 (order.getOrderItems().remove(item)), 해당 자식 엔티티는 DB에서 삭제되지 않습니다. 영속성 컨텍스트는 이 자식 엔티티를 더 이상 부모의 컬렉션에 속하지 않는다고는 알지만, 삭제해야 한다는 지시를 받지 못합니다. 이 자식은 "고아" 상태로 DB에 남게 됩니다.
요약
caseCade.ALL 즉 DELETE 를 적용한다면 -> 부모가 삭제시 자식 삭제 반영
orphanRemoval 을 적용하면 -> 부모의 자식을 삭제시, 자식에 대한 영속성도 제거하여 반영.
JPA에서의 다대다 관계 (연결 엔터티)
다대다 관계에 추가 속성을 포함해야 할 때의 해결책은 간단합니다. JPA의 @ManyToMany 어노테이션을 사용하는 대신, 다대다 관계를 일대다(@OneToMany)와 다대일(@ManyToOne) 관계로 해소해주는 별도의 엔티티를 직접 만드는 것입니다. 이 별도의 엔티티를 '조인 엔티티' 또는 '연결 엔티티' 라고 부릅니다. @ManayToMany 를 제공하고 있지만, 복장성 문제로 중간 연결 엔터티를 통해서 개발하곤 합니다.
이러한 연결(중간)엔터티는 두 엔터티의 key값을 복합키로 관리를 해야합니다. @Embeddable 은 복합키를 적용한 클래스임을 나타내고, 이를 연결 엔터티에서 @EmbeddedId 로 선언하여 사용합니다.
@Embeddable
public static class EnrollmentId implements Serializable { // Serializable 구현 필수
@Column(name = "student_id")
private Long studentId; // Student 엔티티의 ID를 매핑
@Column(name = "course_id")
private Long courseId; // Course 엔티티의 ID를 매핑
// equals()와 hashCode() 구현 필수 (복합 키의 동등성 비교를 위해)
// ... (생략)
}
// Enrollment.java (중간 엔티티)
@Entity
public class Enrollment {
@EmbeddedId // 이 엔티티의 기본 키는 내장된 EnrollmentId 객체임을 선언
private EnrollmentId id;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("studentId") // EnrollmentId의 studentId 필드와 매핑
@JoinColumn(name = "student_id") // 실제 DB 컬럼 매핑
private Student student;
@ManyToOne(fetch = FetchType.LAZY)
@MapsId("courseId") // EnrollmentId의 courseId 필드와 매핑
@JoinColumn(name = "course_id") // 실제 DB 컬럼 매핑
private Course course;
// 관계에 추가되는 속성들
private LocalDate enrollmentDate;
private String grade;
// ... (생략)
}
'개발일지 > SPRINGBOOT' 카테고리의 다른 글
Builder 패턴을 적용한 엔터티 (2) | 2025.06.08 |
---|---|
JPA , 값 객체는 무엇이고 어떻게 활용하는가? (1) | 2025.06.04 |
예외 처리 , Springboot @RestControllerAdvice (1) | 2025.05.22 |
Spring Boot JPA 영속성 완벽 가이드: 엔티티 생명주기 이해하기 (2) | 2025.05.17 |
Spring Boot JPA, 데이터 조회 완벽 정복! (0) | 2025.05.13 |