티스토리 뷰

배경

홈페이지에 선거 기능을 추가하고자 API 개발 도중 데이터베이스 설계상 컬럼 2개를 사용하여 복합키를 가지는 테이블을 발견하였습니다. 이전까지의 테이블은 하나의 컬럼을 통해 데이터를 식별하도록 설계하였기에 @Id 어노테이션을 사용하여 간단히 엔티티로 생성할 수 있었지만 복합키를 가지는 경우는 처음 마주쳤기에 이를 해결하는 과정을 정리해보고자 합니다.

복합키 단점

1. FK를 맺을 때 사이드 이펙트가 크다

CREATE TABLE library (
    region_no varchar(10),
    library_name varchar(50),
    CONSTRAINT PK_library PRIMARY KEY (region_no, library_name)
);

CREATE TABLE book (
    book_id int,
    author varchar(50),
    name varchar(100),
    CONSTRAINT PK_book PRIMARY KEY (book_id)
)

지역 번호(region_no)도서관 이름(library_name)을 복합키로 가지는 library 테이블과 책 식별자(book_id)를 PK로 가지는 book 테이블을 임시로 구성해보았습니다.

 

책 식별자로 구별되는 책이 어느 도서관에 있는지 알기 위해 도서관 테이블과 FK 관계를 구성해야 하는데 이를 위해서는 아래와 같이 region_nolibrary_name 모두 book에 속해야 합니다. 복합키로 인해서 불필요한 데이터를 과도하게 가지는 문제가 발생합니다. 상황에 따라 중간 테이블을 생성하는 등 부가적인 작업이 발생할 수도 있습니다.

CREATE TABLE book (
    book_id int,
    author varchar(50),
    name varchar(100),
    region_no varchar(10), /* FK를 위해 추가 */
    library_name varchar(50), /* FK를 위해 추가 */
    CONSTRAINT PK_book PRIMARY KEY (book_id)
)

2. 인덱스에 좋은 영향을 주지 못한다

인덱스는 데이터베이스 테이블에 대한 검색 성능을 높여주는 자료구조로 특정 컬럼에 인덱스를 생성하면 해당 컬럼의 데이터들을 정렬하여 별도의 메모리 공간에 물리적인 주소와 함께 저장됩니다. PK로 지정한 컬럼은 자동으로 인덱스가 생성되는데 복합키 사용 시 복합키 중 하나의 컬럼만을 조회에 사용한다면 인덱스가 적용되지 않습니다. 생성해둔 인덱스를 활용하지 못한다는 단점이 존재합니다.

3. 제약조건 변경 시 PK 전체 수정이 필요하다

애플리케이션 기획 변경으로 제약조건이 변경된다면 PK 전체 수정이 발생할 수 있습니다. 다른 테이블과 FK를 맺고 있다면 해당 테이블의 내용도 전부 바꿔줘야 하는 문제가 발생합니다.

그럼에도 복합키를 사용하는 이유

특정 데이터를 식별하는 게 크게 의미가 없다면 복합키를 사용해도 좋습니다. 수치와 같은 조회를 위한 통계성 데이터들이 여기에 해당합니다. ID라는 하나의 식별자를 통해 데이터를 조작할 일도 없고 복합키를 통한 조회로 인덱스도 잘 활용할 수 있습니다.

JPA 복합키 사용 방법

1. @Embeddable

@Embeddable 어노테이션을 이용한 식별자 클래스를 생성하고 @EmbeddedId를 통해 해당 식별자 클래스를 식별자로 가지는 엔티티를 만드는 방법입니다. 이를 위해서는 몇 가지 제약조건이 존재합니다.

  • 식별자 클래스에 @Embeddable 어노테이션 추가
  • 디폴트 생성자가 존재
  • 식별자 클래스의 접근 제어자는 public
  • Serializable을 상속
  • equals, hashCode 구현

제가 구현해야 하는 상황은 다음과 같습니다.

CREATE TABLE election_voter (
    voter_id INT NOT NULL DEFAULT 1,
    election_id INT NOT NULL DEFAULT 1,
    is_voted TINYINT NOT NULL DEFAULT 0,
    PRIMARY KEY (election_id, voter_id)
)

선거 투표 현황 통계를 위한 테이블로 투표자 ID와 선거 ID를 복합키로 가지는 테이블입니다. is_voted 컬럼은 투표를 참여했는지를 알기 위한 컬럼입니다.

 

위의 제약사항을 바탕으로 식별자 클래스는 다음과 같이 작성할 수 있습니다.

해당 복합키를 사용하는 엔티티 클래스는 다음과 같이 작성할 수 있습니다.

2. @IdClass

저는 위의 방법을 사용했지만 @IdClass를 사용하는 방법도 존재합니다. 이 방법 또한 몇 가지 제약사항이 존재합니다.

  • 식별자 클래스의 변수명과 엔티티 클래스 변수명이 동일
  • 디폴트 생성자가 존재
  • 식별자 클래스의 접근 제어자는 public
  • Serializable을 상속
  • equals, hashCode 구현

위의 제약사항을 바탕으로 식별자 클래스는 다음과 같이 작성할 수 있습니다.

@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class ElectionVoterPK implements Serializable {

    private Long voter;
    private Long election;

}

해당 복합키를 사용하는 엔티티 클래스는 다음과 같이 작성할 수 있습니다.

@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@IdClass(ElectionVoterPK.class)
@Table(name = "election_voter")
public class ElectionVoterEntity {

    @Id
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "voter_id")
    private MemberEntity voter;

    @Id
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "election_id")
    private ElectionEntity election;

    @Column(nullable = false)
    private Boolean isVoted;

}

둘 중 어느 것을 사용해야 할까

@Embeddable 어노테이션은 적절히 분리되어 있기에 객체 지향적인 방식이고, @IdClass 어노테이션은 중복되는 컬럼이 발생하지만, 테이블과 같은 형태의 엔티티를 가질 수 있기에 데이터베이스 중심적인 방식입니다.

 

@Embeddable 사용 시 컬럼을 참조하는 상황에서 entityVoter.getEntityVoterPK().getVoter()와 같이 장황해지는 단점이 존재하지만 엔티티 작성 시 드는 비용을 줄일 수 있고, @IdClass를 사용 시 컬럼을 참조하는 상황에서 entityVoter.getVoter()와 같이 직관적으로 참조할 수 있지만 엔티티 작성 시 드는 비용이 증가할 수 있습니다.

 

상황에 맞춰 적절한 방법을 선택하여 복합키를 생성하면 될 것 같습니다.

댓글
최근에 올라온 글
최근에 달린 댓글
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Total
Today
Yesterday