요구사항 정의
- 시험은 N개의 문제를 가진다.
- 문제 등록은 시험이 있어야 가능하다.
- 문제의 생성, 수정, 삭제, 조회는 시험을 통해서만 가능하다.
- 시험이 삭제되면, 문제들도 삭제된다.
- 시험과 문제는 각각 유니크한 식별자를 가진다.
하위 엔티티들을 관리하기 위한 전략
- 시험에 있어서 문제는 관리해야 할 하위 엔티티이다. 시험을 통해서만 문제가 등록되도록 영속성 전이 옵션 (cascade)을 사용한다.
- 생성, 수정, 삭제의 관점에서 보자. 생성은 상위 엔티티의 컬렉션에 하위 엔티티를 넣어 영속 상태로 만들면 JPA에서 insert 해준다.
- 수정은 조회 후 컬렉션의 원소를 수정하여 영속 상태로 만들면 JPA에서 update 해준다.
- 삭제는 조회 후 컬렉션의 원소를 삭제하여 영속 상태로 만들면 JPA에서 delete 해준다.
- 이 때, 도메인 모델을 구현하기 위해, 상위 엔티티와 연결이 끊겼다면 삭제해주는 옵션인 orphanremoval 옵션을 사용한다.
일대다 단방향
- 객체 관점 : 시험은 문제를 알고 있지만, 문제는 시험을 모른다.
- 테이블 관점 : 시험의 PK를 문제의 FK로 가진다.
코드
package com.onewayjpatest.demo.entity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@NoArgsConstructor
@Getter
public class Exam {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL,orphanRemoval = true)
private List<Question> questions = new ArrayList<>();
public Exam(String content) {
this.content = content;
}
public void addQuestion(Question question){
this.questions.add(question);
}
}
package com.onewayjpatest.demo.entity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Entity
@NoArgsConstructor
@Getter
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "content")
private String content;
public Question(String content){
this.content = content;
}
}
위와 같이 작성해놓고 Exam의 리스트에 Question을 넣어 영속화하는 테스트를 해보자. exam을 insert하고, question을 insert하는 쿼리가 함께 날라가게 된다.
그런데, 이상한 점이 있다. exam의 PK를 알고 있기 때문에 question을 insert할 때 FK를 바로 할당할 수 있을 것 같은데, FK를 초기화하기 위한 쿼리가 두 번 날라간다.
왜 이런 현상이 발생하는걸까? JPA의 관점에서 보자. exam에선 question을 알지만, question은 exam을 모른다. 따라서 question의 FK 값을 넣기 위해서 exam에게 값을 물어봐야 하는 것이다.
- 단점 : 불필요한 쿼리가 한 번 더 날라간다.
- 장점 : 객체 관점에서 question이 exam의 존재를 몰라야 하므로 설계상 이점이 있다.
다대일 양방향
- 객체 관점 : 시험과 문제는 서로를 알고 있다.
- 테이블 관점 : 시험의 PK를 문제의 FK로 가진다.
package com.onewayjpatest.demo.entity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@NoArgsConstructor
@Getter
public class Exam {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
@OneToMany(mappedBy = "exam", fetch = FetchType.LAZY, cascade = CascadeType.ALL,orphanRemoval = true)
private List<Question> questions = new ArrayList<>();
public Exam(String content) {
this.content = content;
}
public void addQuestion(Question question){ //연관관계 편의 메서드
question.changeExam(this);
this.questions.add(question);
}
}
package com.onewayjpatest.demo.entity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Entity
@NoArgsConstructor
@Getter
public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "content")
private String content;
public Question(String content){
this.content = content;
}
public void changeExam(Exam exam){
this.exam = exam;
}
}
mappedBy로 연관관계의 주인이 question인걸 표시하고, question이 exam의 존재를 알게 됐다.
- 단점 : question이 exam의 존재를 알게 되었기 때문에, 설계상 손해를 보게 된다.
- 장점 : 불필요한 쿼리가 날라가지 않는다.
고찰
- 성능을 고려하는 상황이 아니라면, 일대다 단방향을 선택하고 반대라면 양방향을 선택하는 것이 좋겠다.
- 개인적인 생각으로는 성능 문제는 언젠간 만나게 될 이슈이고, 애플리케이션을 운영하면서도 문제 파악을 어렵게 하는 주범이기 때문에 미리 양방향으로 연결하는게 좋을 것 같다.
- 양방향으로 연결은 하되, 애초에 연관관계를 설정할 때 상위 엔티티에서만 설정하는 것도 좋은 방법인 것 같다.
'Spring' 카테고리의 다른 글
오늘의 질문: jpql 프로젝션 대상이 둘 이상이면 반환 값을 어떻게 받나? (0) | 2021.11.05 |
---|---|
오늘의 질문 : JPA 엔티티 수정 방식 (0) | 2021.08.10 |
JPA 생성 날짜, 수정 날짜 자동으로 넣어주기 (0) | 2021.07.16 |
소프트웨어 공학 쇼핑몰 만들기 (0) | 2021.05.20 |
Spring [IoC와 DI] (0) | 2021.03.25 |