JPA 엔티티 구현하기

dasd412·2024년 1월 3일
0

cotella

목록 보기
7/9

연관 관계 매핑 (다대일, 다대다)

단방향보다 양방향 매핑이 훨씬 복잡하며, 로직도 잘 관리해야 합니다.
양방향이 단방향보다 유일하게 장점인 것은 반대 방향으로의 객체 그래프 탐색 기능이 추가된 것 뿐입니다.

따라서 처음엔 단방향으로 설계한 다음, 필요하면 양방향을 추가하면 된다고 합니다. 단방향으로만 설계해도 DB에선 이미 왜래키 관계가 설정되어 있기 때문이죠.

또한 양방향으로 만드려면 양방향 연관 관계를 설정하는 편의 메서드도 추가해야 하는 번거로움이 있습니다.

그래서 다대일, 다대다 전부 처음엔 단방향으로 설정하였습니다.

참고 자료에서도 단방향과 양방향은 아무런 성능 차이가 없으며, 전부 단방향으로 처리한 다음에 역방향 참조가 필요할 때만 직접 join query를 날리면 된다고 설명합니다.

fetch join으로 한 번에 조회가 필요할 때 양방향 매핑 추가를 고려해봐야겠습니다.

코드 (다대일)

@Entity
@Getter
@Table(name = "interview_question")
public class InterviewQuestion {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "interview_question_id")
  private Long id;

  private String questionContent;

  @ManyToOne
  @JoinColumn(name = "interview_keyword_id")
  private InterviewKeyword interviewKeyword;
  
}

코드 (다대다)

@Entity
@Getter
@Table(name = "answer")
public class Answer {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "answer_id")
  private Long id;

  private String answerContent;

  private int likesCount;

  private int dislikesCount;

  @ManyToOne
  @JoinColumn(name = "interview_question_id")
  private InterviewQuestion interviewQuestion;

  @ManyToOne
  @JoinColumn(name = "interview_session_id")
  private InterviewSession interviewSession;
  
}

참고 자료

https://www.inflearn.com/questions/268269/%EC%96%91%EB%B0%A9%ED%96%A5-%EB%A7%A4%ED%95%91%EC%9D%B4-%EC%96%B8%EC%A0%9C-%ED%95%84%EC%9A%94%ED%95%9C%EC%A7%80-%EC%97%AC%EC%AD%A4%EB%B3%B4%EA%B3%A0-%EC%8B%B6%EC%8A%B5%EB%8B%88%EB%8B%A4


연관 관계 매핑 (클로져 테이블)

면접 질문은 계층형 구조이므로 클로져 테이블을 활용했습니다.

클로져 테이블 스키마

복합키는 여러 개의 필드를 조합해서 테이블의 기본키를 구성한 키입니다.

그리고 아래 스키마처럼 클로져 테이블은 부모 테이블인 'interview_question'의 기본키가 자식 테이블인 클로져 테이블의 기본키로 활용되고 있습니다.

따라서 클로져 테이블은 복합키를 활용하며 면접 질문 테이블과 식별 관계라고 할 수 있겠습니다.

식별 관계를 사용하면, 자식 테이블에서 부모 테이블을 참조하는 레코드는 항상 존재하게 되므로 데이터 무결성을 보장합니다.

그러나 부모 테이블 기본키를 항상 자식 테이블 기본키가 가져야하므로 비식별 관계보다 테이블 구조가 유연하지 못합니다.

그런데 면접 질문은 꼬리 질문이라는 계층형 구조가 필요하고, 해당 구조는 계층이 깨지지 않도록 해야 하므로 엄격한 데이터 무결성이 필요합니다.

CREATE TABLE `question_closure_table` (
	`ancestor`	int	NOT NULL	COMMENT '부모 노드를 뜻함.',
	`descendant`	int	NOT NULL	COMMENT '자식 노드를 뜻함.',
	`depth`	int	NOT NULL	COMMENT '부모 노드와 자식 노드 간의 깊이',
	PRIMARY KEY (`ancestor`,`descendant`) COMMENT 'ansestor 칼럼과 descendant 칼럼 합친 합성키'
)ENGINE=InnoDB;

JPA

클로져 테이블 엔티티

복합키를 가지기 때문에 @IdClass를 활용했습니다.
DB 스키마에서 보았듯이, 'interview_question'의 기본키를 'question_closure_table'의 기본키로 사용하고 있지만, 외래키로는 사용되지 않고 있습니다.

따라서 @ManyToOne은 부착하지 않았습니다.

@Getter
@IdClass(QuestionClosureId.class)
@Entity
@Table(name = "question_closure_table")
public class QuestionClosureTable extends BaseTimeEntity {

  @Id
  private Integer ancestor;

  @Id
  private Integer descendant;

  private int depth;
}

복합키

복합키를 식별하는 클래스는 equals()와 hashCode()를 오버라이드해야 하며 Serializable 구현도 필요합니다.

이 복합키는 논리적으로 동등하려면, ancestor와 descendant가 같아야 동등한 엔티티입니다. 따라서 equals()에 두 필드를 비교하게 했습니다.

public class QuestionClosureId implements Serializable {

  private Integer ancestor;
  
  private Integer descendant;

  @Override
  public int hashCode() {
    return Objects.hash(ancestor, descendant);
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null || getClass() != obj.getClass()) {
      return false;
    }
    QuestionClosureId target = (QuestionClosureId) obj;
    return Objects.equals(this.ancestor, target.ancestor) & Objects.equals(this.descendant,
        target.descendant);
  }
}

equals()와 hashcode() 재정의 참고 자료

https://velog.io/@sonypark/Java-equals-hascode-%EB%A9%94%EC%84%9C%EB%93%9C%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%9E%AC%EC%A0%95%EC%9D%98%ED%95%B4%EC%95%BC-%ED%95%A0%EA%B9%8C


잠깐의 복습들

JPA를 거진 1년 만에 해서 복습하는 중이라 까먹거나 헷갈리는게 있었습니다.

기본키 생성 전략 (IDENTITY)와 save()

오해가 있던 코드는 다음과 같습니다. 참고로 코드 내 엔티티들의 기본키 생성 전략은 IDENTITY입니다.

    InterviewUser user = InterviewUser.builder().name("test").email("test@test.com")
        .password("test").build();

    session = new InterviewSession(user);

    question = InterviewQuestion.builder().questionContent("test").build();

    interviewUserRepository.save(user);

    interviewSessionRepository.save(session);

    interviewQuestionRepository.save(question);

    answer = Answer.builder().answerContent("test").likesCount(0).dislikesCount(0)
        .interviewSession(session).interviewQuestion(question).build();
        
	answer = intervieweeAnswerRepository.save(answer);

오해가 있던 로그는 다음과 같습니다.

insert는 당연한 건데, 왜 select하고 있는지 처음엔 영문을 몰랐었습니다.
lazy loading이나 n+1 문제 관련 문제였나 싶었습니다.

하지만 딱히 연관 관계가 있는 엔티티에 대해 조회 쿼리를 날린 것은 없었습니다.

Hibernate: insert into interview_user (created_at,email,name,password,provider,provider_id,updated_at,interview_user_id) values (?,?,?,?,?,?,?,default)
Hibernate: insert into interview_session (created_at,interview_user_id,updated_at,interview_session_id) values (?,?,?,default)
Hibernate: insert into interview_question (created_at,interview_keyword_id,question_content,updated_at,interview_question_id) values (?,?,?,?,default)
Hibernate: insert into answer (answer_content,created_at,dislikes_count,interview_question_id,interview_session_id,likes_count,updated_at,answer_id) values (?,?,?,?,?,?,?,default)
Hibernate: select a1_0.answer_id,a1_0.answer_content,a1_0.created_at,a1_0.dislikes_count,a1_0.interview_question_id,a1_0.interview_session_id,a1_0.likes_count,a1_0.updated_at from answer a1_0
Hibernate: select i1_0.interview_question_id,i1_0.created_at,i1_0.interview_keyword_id,i1_0.question_content,i1_0.updated_at from interview_question i1_0
Hibernate: select i1_0.interview_session_id,i1_0.created_at,i1_0.interview_user_id,i1_0.updated_at from interview_session i1_0
Hibernate: select i1_0.interview_user_id,i1_0.created_at,i1_0.email,i1_0.name,i1_0.password,i1_0.provider,i1_0.provider_id,i1_0.updated_at from interview_user i1_0

찾아보니 IDENTITY 전략은 DB에 생성을 하고나서 auto_increment로 기본키가 생성됩니다. 그리고 이를 기본키로 조회해야 하기 때문에 또다시 select가 발생하는 것 같습니다.

혹시 몰라서 아무런 관계를 맺지 않고 interview_user 엔티티만 저장했더니 역시나 insert 후에 select가 나온 것을 확인할 수 있었습니다.

참고 자료

https://velog.io/@gudnr1451/GeneratedValue-%EC%A0%95%EB%A6%AC


profile
시스템 아키텍쳐 설계에 관심이 많은 백엔드 개발자입니다. (Go/Python/MSA/graphql/Spring)

0개의 댓글