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);
}

의존성 사이클

의존성 세미나 자료

https://youtu.be/dJ5C4qRqAgA

[수정본] 우아한 객체지향


의존성 사이클에 관한 문제는 JPA만의 문제점은 아니다.

다만 JPA에서 다대일 관계를 표현하면 양방향으로 의존하는 소스 코드가 간혹 나타나기 때문에 함께 적었다.

객체 참조를 통해 더 객체지향적인 코드를 작성할 수 있지만 결합도가 크게 상승하여 문제가 될 수 있다.

질문과 답변의 예시를 살펴볼건데, 의존성은 아래처럼 순환하고 있다.

AnswerQuestionQuestionAnswersAnswer → ...

여기서 QuestionAnswersList<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<>();
}

사이클 관계를 이루는 구조에서 하나의 클래스만 수정해도 다른 클래스에 전부 영향이 가게 된다.

예를 들어 QuestionAnswersSet<Answer>List<Answer> 로 교체한다고 하면 AnswerQuestion 에서 참조하는 부분을 모두 수정해주어야 한다.

@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 의 필드였던 QuestionquestionId 로 바꿔주면서 사이클이 완전히 끊겼다.

필드 참조를 통해 얻어갈 수 있던 객체지향적인 장점은 잃게 되지만 결합도를 낮춰 코드 수정 위험도를 줄일 수 있다.

'Java > JPA' 카테고리의 다른 글

JPA 사용 시 주의할 점  (0) 2021.07.22
[JPA] 다양한 연관관계 매핑  (0) 2020.04.07
[JPA] 연관관계 매핑 기초  (1) 2020.02.26
[JPA] 엔티티 매핑  (0) 2020.02.22
[JPA] EntityManager, 영속성 컨텍스트  (0) 2020.02.17

+ Recent posts