JPA 사용 시 주의할 점
영속 상태 보장
JPA Entity는 영속상태인가, 아닌가에 따라서 동작 상 제한사항이 있을 수 있다.
예를 들면, 영속 상태의 entity는 delete가 가능하지만 비영속 상태의 entity는 불가능하다.
특정 로직을 수행하는 경우 영속 상태를 인지하고 있어야한다.
@Transactional
public void deleteQuestion(User loginUser, Question question) throws CannotDeleteException {
if (question.isDeleted()) {
throw new CannotDeleteException("이미 삭제된 질문입니다.");
}
if (!question.isOwner(loginUser)) {
throw new CannotDeleteException("질문을 삭제할 권한이 없습니다.");
}
List<Answer> answers = question.getAnswers();
for (Answer answer : answers) {
if (!answer.isOwner(loginUser)) {
throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다.");
List<DeleteHistory> deleteHistories = new ArrayList<>();
question.setDeleted(true);
deleteHistories.add(new DeleteHistory(ContentType.QUESTION, question.getId(), question.getWriter(), LocalDateTime.now()));
for (Answer answer : answers) {
answer.setDeleted(true);
deleteHistories.add(new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriter(), LocalDateTime.now()));
}
deleteHistoryService.saveAll(deleteHistories);
}
위의 코드는 질문에 쓰여진 답변을 삭제하는 서비스 메소드이다.
메소드 명은 deleteQuestion
이며 매개변수로 Question
을 넘겨주고 있다.
entity를 삭제하기 위해서는 넘겨받는 Question
이 반드시 영속 상태여야 하지만
코드 사용자 측에서는 실수로 비영속 상태의 Question
을 넘겨 버릴 가능성이 있다.
Question
대신 question_id
를 넘기도록 수정하면 영속 상태를 보장할 수 있다.
@Transactional
public void deleteQuestion(long loginUserId, long questionId) throws CannotDeleteException {
Question question =
questionRepository.findById(questionId)
.orElseThrow(() -> new CannotDeleteException("질문을 찾을 수 없습니다."));
if (question.isDeleted()) {
throw new CannotDeleteException("이미 삭제된 질문입니다.");
}
User loginUser =
userRepository.findById(loginUserId)
.orElseThrow(() -> new NotFoundException("사용자를 찾을 수 없습니다."));
if (!question.isOwner(loginUser)) {
throw new CannotDeleteException("질문을 삭제할 권한이 없습니다.");
}
List<Answer> answers = question.getAnswers();
for (Answer answer : answers) {
if (!answer.isOwner(loginUser)) {
throw new CannotDeleteException("다른 사람이 쓴 답변이 있어 삭제할 수 없습니다.");
}
}
List<DeleteHistory> deleteHistories = new ArrayList<>();
question.delete();
deleteHistories.add(new DeleteHistory(ContentType.QUESTION, question.getId(), question.getWriter(), LocalDateTime.now()));
for (Answer answer : answers) {
question.deleteAnswer(answer);
deleteHistories.add(new DeleteHistory(ContentType.ANSWER, answer.getId(), answer.getWriter(), LocalDateTime.now()));
}
deleteHistoryService.saveAll(deleteHistories);
}
의존성 사이클
의존성 세미나 자료
의존성 사이클에 관한 문제는 JPA만의 문제점은 아니다.
다만 JPA에서 다대일 관계를 표현하면 양방향으로 의존하는 소스 코드가 간혹 나타나기 때문에 함께 적었다.
객체 참조를 통해 더 객체지향적인 코드를 작성할 수 있지만 결합도가 크게 상승하여 문제가 될 수 있다.
질문과 답변의 예시를 살펴볼건데, 의존성은 아래처럼 순환하고 있다.
Answer
→ Question
→ QuestionAnswers
→ Answer
→ ...
여기서 QuestionAnswers
는 List<Answer>
를 일급 콜렉션으로 포장한 클래스이다.
@Entity
public class Answer extends BaseEntity implements Serializable {
private static final long serialVersionUID = 951549577740790162L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false)
private Long id;
@ManyToOne
@JoinColumn(name = "writer_id", foreignKey = @ForeignKey(name = "fk_answer_writer"))
private User writer;
@ManyToOne
@JoinColumn(name = "question_id", foreignKey = @ForeignKey(name = "fk_answer_to_question"))
private Question question;
@Column(columnDefinition = "LONGTEXT")
private String contents;
@Column
private boolean deleted = false;
}
@Entity
public class Question extends BaseEntity implements Serializable {
private static final long serialVersionUID = -5316964078122252034L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false)
private Long id;
@Column(length = 100)
private String title;
@Column(columnDefinition = "LONGTEXT")
private String contents;
@ManyToOne
@JoinColumn(name = "writer_id", foreignKey = @ForeignKey(name = "fk_question_writer"))
private User writer;
@Embedded
private QuestionAnswers questionAnswers;
@Column
private boolean deleted = false;
}
@Embeddable
public class QuestionAnswers implements Serializable {
private static final long serialVersionUID = 8457250053092405727L;
@OneToMany(mappedBy = "question")
@Where(clause = "deleted = 0")
private final Set<Answer> answers = new HashSet<>();
}
사이클 관계를 이루는 구조에서 하나의 클래스만 수정해도 다른 클래스에 전부 영향이 가게 된다.
예를 들어 QuestionAnswers
의 Set<Answer>
를 List<Answer>
로 교체한다고 하면 Answer
나 Question
에서 참조하는 부분을 모두 수정해주어야 한다.
@Entity
public class Answer extends BaseEntity implements Serializable {
private static final long serialVersionUID = 951549577740790162L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(nullable = false)
private Long id;
@ManyToOne
@JoinColumn(name = "writer_id", foreignKey = @ForeignKey(name = "fk_answer_writer"))
private User writer;
@Column // Question -> question_id
private Long questionId;
@Column(columnDefinition = "LONGTEXT")
private String contents;
@Column
private boolean deleted = false;
}
Answer
의 필드였던 Question
을 questionId
로 바꿔주면서 사이클이 완전히 끊겼다.
필드 참조를 통해 얻어갈 수 있던 객체지향적인 장점은 잃게 되지만 결합도를 낮춰 코드 수정 위험도를 줄일 수 있다.
'Java > JPA' 카테고리의 다른 글
[JPA] 다양한 연관관계 매핑 (0) | 2020.04.07 |
---|---|
[JPA] 연관관계 매핑 기초 (1) | 2020.02.26 |
[JPA] 엔티티 매핑 (0) | 2020.02.22 |
[JPA] EntityManager, 영속성 컨텍스트 (0) | 2020.02.17 |