본문 바로가기
Web Development/Application

[JPA / Hibernate] 복합키(Composite Key)를 갖는 객체(Entity) 간의 연관관계 매핑

by saltyzun 2022. 12. 7.

보통의 JPA 예제에서는 단일 컬럼을 PK로 갖는 엔티티 간의 연관관계 매핑을 주로 다룹니다. 그러나 실제로 Legacy System을 JPA 기반의 어플리케이션으로 개발하다보면 기존의 데이터 모델이 복합키를 갖고, 식별관계로 매핑된 경우를 경우를 쉽게 볼 수 있습니다. 이번 글을 통해 이런 경우 어떻게 JPA를 활용할 수 있는지 다뤄보고자 합니다.

 

목차

  • 기본적인 1:N 연관관계 매핑
  • @IdClass를 통한 복합키(Composite Key) 설정
  • DB에서 식별관계를 갖는 객체의 연관관계 매핑
  • 요약

기본적인 1:N 연관관계 매핑

아래의 이미지는 이번 포스팅에서 예시를 들기 위해 설계한 1:N 관계의 엔티티 입니다. 회원을 의미하는 Member 와 회원별 주문내역을 다루는 MemberOrderHistory 로, 우선은 각각이 갖는 Id 값인 memberId 와 orderId 를 이용해 식별된다고 가정하겠습니다. 이 때 연관관계의 주인은 N-side 의 MemberOrderHistory가 되겠습니다. 

 

이를 실제 JAVA의 Entity Class 로 만들어 보면 아래와 같이 코드를 작성할 수 있습니다. 기본적인 @NoArgsConstructor, @Getter, @ToString 등의 어노테이션은 가독성을 위해 제거했으니, 필요에 따라 추가하시면 되겠습니다.

@Entity
public class Member {

    @Id
    @GeneratedValue
    private Long memberId;

    private String memberName;


}

@Entity
@IdClass(MemberOrderHistoryPK.class)
public class MemberOrderHistory {

    @Id
    @GeneratedValue
    private Long orderId;

    private LocalDate orderDate;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

}

 

@IdClass를 통한 복합키(Composite Key) 설정

그런데 여기서 문제가 생깁니다. '특정 회원이 일정 기간동안 주문한 내역을 모두 조회할 수 있어야 한다'는 요구사항을 개발하려고 보니 위에서 설계한 엔티티 구조로는 쿼리 성능이 잘 나오기 힘든 상황입니다. 이럴 때 RDB 데이터 모델에서는 보통 식별관계로 테이블을 설계하고, 복합키를 사용해 Index를 구성하기 때문입니다. 어플리케이션을 만들기 위해 참조해야 하는 데이터 모델이 아래와 같다고 가정하겠습니다.

이와 같이 복합키를 갖는 데이터 모델을 엔티티로 만들기 위해 JPA는 @EmbeddedID@IdClass 두 가지 방식을 지원합니다. 두 방식을 모두 구체적으로 다뤄보면 좋겠지만, 이번 글에서는 제가 직접 사용했던 @IdClass를 활용한 방식만 다뤄보도록 하겠습니다.

 

우선, @IdClass 방식을 활용하기 위해 실제 데이터 모델의 복합키에 해당하는 Id 클래스를 하나 더 만들어 줘야 합니다. 이 때 복합키 설정을 위해 개발자가 직접 만든 PK 클래스는 Serializable 인터페이스를 구현해야 하는데, equals()hashCode() 메서드는 가급적 IDE에서 지원하는 방식으로 오버라이딩 해야 문제가 생기지 않는다는 점을 유의해주세요!

 

MemberOrderHistoryPK

@NoArgsConstructor
public class MemberOrderHistoryPK implements Serializable {

    private Member member;

    private Long orderId;

    private LocalDate orderDate;

    @Override
    public boolean equals(Object o) { ... }

    @Override
    public int hashCode() { ... }
    
}

 

그리고 MemberOrderHistory 엔티티가 위에 정의한 복합키 PK 클래스를 IdClass로 사용할 수 있도록 기존의 엔티티를 수정해주겠습니다.

 

MemberOrderHistory (CompositeKey)

@Entity
@IdClass(MemberOrderHistoryPK.class)
public class MemberOrderHistory {

    @Id
    @GeneratedValue
    private Long orderId;

    @Id
    private LocalDate orderDate;

    @Id
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "MEMBER_ID")
    private Member member;


}

 

이렇게 엔티티를 수정한 뒤 em.find(MemberOrderHistory.class, memberOrderHistoryPK) 명령어를 이용해  MemberOrderHistory를 조회해보면 아래와 같이 복합키를 이용해 원하는 객체를 DB에서 조회해오는 것을 확인하실 수 있습니다.

 


 

DB에서 식별관계를 갖는 객체의 연관관계 매핑

아마 JPA에 대해 잘 아시는 분들은 위에서 실행된 쿼리를 보고 이상함을 느끼셨을거라 생각됩니다. 분명 FetchType을 Lazy로 설정하고, MemberOrderHistory 에서 Member로 객체 그래프 탐색을 하지 않았음에도 불구하고 Eager 옵션을 사용한 것처럼 의도치 않게 Member 객체를 함께 조회해오는 문제가 생기고 있습니다. 

 

memberId 값만 갖고 MemberOrderHistory 를 조회하지 못하고 무조건 Member 를 조회해서 가져와야 하는 일종의 커플링이 생기는건데 만약 어플리케이션에서 조건에 맞는 MemberOrderHistory 만을 조회해오는 REST API를 클라이언트에 제공한다면 API가 호출될 때마다 쿼리가 위와 같은 식으로 수행될 겁니다. 따라서 위와 같은 어플리케이션은 식별관계로 설계된 DB 모델의 장점을 전혀 살리지 못 한다고 할 수 있습니다.

 

문제는 @Id@ManyToOne 을 한 필드에 모두 부여함으로써 해당 필드가 단순히 연관관계를 맺을 뿐 아니라 실제 매핑까지 사용되는 데 있었습니다. 따라서 연관관계를 맺는 @ManyToOne과 실제 그 값이 식별, 매핑에 이용되는 @Id 필드를 분리하고자 했고, @JoinColumn 의 필드에 updatable = false, insertable = false 옵션을 사용함으로써 이를 구현할 수 있었습니다.

 

@Entity
@IdClass(MemberOrderHistoryPK.class)
public class MemberOrderHistory {

    @Id
    @GeneratedValue
    private Long orderId;

    @Id
    private LocalDate orderDate;

    @Id
    private Long memberId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "memberId", updatable = false, insertable = false
            , foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
    private Member member;


}

 

위와 같이 엔티티를 한번 더 수정해준 뒤 같은 명령어를 이용해 MemberOrderHistory 를 조회해보면 아래와 같이 한번의 SELECT 쿼리에 원하는 객체를 조회해올 수 있음을 확인할 수 있습니다.

 


 

요약

 

- @EmbeddedId 또는 @IdClass 를 활용해 JPA Entity에 복합키를 설정해줄 수 있다

 

- @IdClass 방식을 사용하는 경우 개발자가 매번 PK 클래스를 정의해야하는데, 이 클래스의 경우 Serializable 인터페이스를 구현해야 하고, equals()  hashCode() 메서드를 재정의 해야한다

 

- 연관관계를 설정하는 @ManyToOne 과 실제 식별, 매핑에 값이 사용되는 @Id 를 같은 필드에 사용하면 데이터 모델과 무관하게 불필요한 커플링이 생길 수 있다

 

- @Id@ManyToOne 을 분리하고, updatable = false, insertable = false 옵션을 사용함으로써 문제를 해결할 수 있다

 

Reference

https://techblog.woowahan.com/2595/

 

 

반응형

댓글