이 글은 김영한님의 Inflearn 강의를 학습한 내용을 정리하였습니다.
글에 포함된 그림의 출처는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의와 자바 ORM 표준 JPA 프로그래밍입니다.
객체의 상속 관계를 데이터베이스에 어떻게 매핑하는 방법인 상속 관계 매핑과 데이터베이스의 식별자가 하나 이상일 때 매핑하는 방법인 복합 키와 식별 관계 매핑에 대해 알아본다.
관계형 데이터베이스는 객체지향 언어에서 다루는 상속이라는 개념이 없다. 대신 슈퍼타입 서브타입 관계 (Super-Type Sub-Type Relationship) 라는 모델링 기법이 객체의 상속 개념과 가장 유사하다. ORM에서 이야기하는 상속 관계 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것이다.
슈퍼타입 서브타입 논리 모델을 실제 물리 모델인 테이블로 구현할 때는 3가지 방법을 선택할 수 있다.
각각의 테이블로 변환
모두 테이블로 만들고 조회할 때 조인을 사용한다. (JPA에서는 조인 전략이라 한다.)
통합 테이블로 변환
모든 칼럼을 하나의 테이블로 구성한다. (JPA에서는 단일 테이블 전략이라 한다.)
서브타입 테이블로 변환
서브 타입마다 하나의 테이블을 만든다. (JPA에서는 구현 클래스마다 테이블 전략이라 한다.)
예제와 함께 각각의 방법을 알아보자.
조인 전략은 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다.
조회할 때 조인을 자주 사용한다.
부모 테이블의 기본 키를 받아서 기본 키 + 외래 키를 사용하는 전략이기 때문에 조회할 때 조인을 자주 사용한다.
타입을 구분하는 컬럼을 추가한다.
객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없기 때문에 타입 구분을 위한 칼럼을 추가한다.
예제 코드와 사용된 주요 어노테이션을 보자.
Item.java
Album.java
Movie.java
Book.java
예제 코드에서 사용된 주요 어노테이션을 알아보자.
@Inheritance(strategy = InherianceType.JOINED)
상속 매핑은 부모 클래스에 @Inheritance를 사용해야 한다. 그리고 매핑 전략을 지정한다. 여기서는 조인 전략을 사용하므로 strategy 속성으로 InheritanceType.JOINED를 설정했다.
@DiscriminatorColumn(name = "DTYPE")
부모 클래스에 구분 컬럼을 지정한다. 이 컬럼으로 저장된 자식 테이블을 구분할 수 있다. (기본값은 DTYPE이다.)
@Disciminator("M")
엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다. 만약 Movie 엔티티를 저장하면 구분 컬럼인 DTYPE에 M
이 저장된다.
@PrimaryKeyJoinColumn(name = "book_id")
자식 테이블의 기본 키 컬럼명을 지정한다. Book 엔티티에 기본 키 + 외래 키의 컬럼명은 book_id
로 된다.
단일 테이블 전략은 슈퍼타입 서브타입의 모든 칼럼을 하나의 테이블로 구성한다.
@DiscriminatorColumn
)을 반드시 사용한다. 조인 전략과 같이 이번 예제에서도 Item, Album, Book, Movie 엔티티를 사용한다. 그리고 조인 전략과 차이가 있는 Item 코드와 실행 결과 SQL을 확인해보자.
Item.java
단일 테이블 전략을 사용했을 때 SQL은 아래와 같다.
Item 테이블이 만들어지고 컬럼으로 Item, Album, Book, Movie 엔티티에 작성한 속성 값이 모두 들어간다.
구현 클래스마다 테이블 전략은 자식 엔티티마다 테이블을 만든다. (이때 슈퍼타입을 위한 테이블은 생성되지 않는다.) 그리고 자식 테이블 각각에 필요한 컬럼이 모두 있다.
이번 예제도 Item
엔티티만 살펴본다.
Item 엔티티에 매핑되는 테이블은 생성되지 않는다. 하지만 Item 엔티티의 속성들은 자식 엔티티의 속성에 추가되어 테이블 칼럼으로 추가된다.
@MappedSuperclass
는 부모 클래스를 테이블과 매핑하지 않고 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공한다. (실제 테이블과 매핑되지 않는다.)
위 그림에 해당하는 코드와 결과를 확인해보자.
BaseEntity에는 객체들의 공통 매핑 정보를 정의했다. 그리고 상속을 통해 공통 매핑 정보를 물려받았다. 이때 BaseEntity에 해당하는 테이블은 생성되지 않고 상속받은 객체들에게 매핑 정보만 제공한다.
부모로부터 물려받은 매핑 정보를 그대로 사용할 수 있고, 재정의하여 사용할 수 있다.
특히 @Id
는 @Column(name = "")
을 통해 매핑 정보를 지정하는 경우가 흔하다.
매핑 정보를 재정의하려면 @AttributeOverrides
또는 @AttributeOverride
를 사용하고, 연관관계를 재정의하려면 @AssociationOverrides
또는 @AssociationOverride
를 사용한다.
코드를 보며 사용법을 익혀보자.
둘 이상을 재정의하려면 @AttributeOverrides
를 사용하면 된다.
@MappedSuperclass
로 지정한 클래스는 엔티티가 아니므로 em.find()
또는 JPQL에서 사용할 수 없다. 복합 키를 매핑하는 방법과 식별 관계, 비식별 관계를 매핑하는 방법을 알아보자.
데이터베이스 테이블 사이에 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별 관계와 비식별 관계로 구분한다.
식별 관계는 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계다.
비식별 관계는 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계다.
비식별 관계는 외래 키에 NULL을 허용하는지에 따라 필수적 비식별 관계와 선택적 비식별 관계로 나눈다.
최근 동향
데이터베이스 테이블을 설계할 때 식별 관계나 비식별 관계 중 하나를 선택해야 한다. 글이 작성된 시점에는 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 추세다.
기본 키 구성하는 컬럼이 하나일 때 매핑 방식
@Entity
public class Hello {
@Id
private String id;
}
기본 키 구성하는 컬럼이 하나 이상일 때 매핑 방식
@Entity
public class Hello {
@Id
private String id1;
@Id
private String id2; //실행 시점에 매핑 예외 발생
}
JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용한다. 그리고 식별자를 구분하기 위해 equals와 hashCode를 사용해서 동등성 비교를 한다. 식별자 필드가 하나일 때는 보통 자바의 기본 타입을 사용하므로 문제가 없지만, 식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 그곳에 equals와 hashCode를 구현해야 한다.
JPA는 복합 키를 지원하기 위해 @IdClass와 @EmbeddedId 2가지 방법을 제공하는데 @IdClass는 관계형 데이터베이스에 가까운 방법이고, @EmbeddedId는 객체지향에 가까운 방법이다.
위 그림에서 PARENT 테이블을 보면 기본 키를 PARENT_ID1, PARENT_ID2로 묶은 복합 키로 구성했다. 따라서 복합 키를 매핑하기 위해 식별자 클래스를 별도로 만들어야 한다.
@IdClass
를 사용할 때 식별자 클래스의 조건
1. 식별자 클래스의 속성명과 엔티티에서 사용하는 속성명이 같아야한다.
2. Serializable 인터페이스를 구현해야 한다.
3. equals, hashCode를 구현해야 한다.
4. 기본 생성자가 있어야 한다.
5. 식별자 클래스는 public이어야 한다.
아래는 저장과 조회에 대한 코드와 SQL이다.
저장 코드를 보면 식별자 클래스인 ParentId가 보이지 않는데, em.persist()를 호출하면 영속성 컨텍스트에 엔티티를 등록하기 직전에 내부에서 Parent.id1, Parent.id2 값을 사용해서 식별자 클래스인 ParentId를 생성하고 영속성 컨텍스트의 키로 사용한다.
Parent의 복합키를 Child에 비식별 관계 매핑해보자.
부모 테이블의 기본 키 컬럼이 복합 키이므로 자식 테이블의 외래 키도 복합 키다. 따라서 외래 키 매핑 시 여러 컬럼을 매핑해야 하므로 @JoinColumns 어노테이션을 사용하고 각각의 외래 키 컬럼을 @JoinColumn으로 매핑한다.
@EmbeddedId
는 @IdClass
보다 객체지향적인 방법이다.
예제로 알아보자.
@EmbeddedId
를 사용할 때 식별자 클래스의 조건
1.@Embeddable
어노테이션을 붙여주어야 한다.
2. Serializable 인터페이스를 구현해야 한다.
3. equals, hashCode를 구현해야 한다.
4. 기본 생성자가 있어야 한다.
5. 식별자 클래스는 public이어야 한다.
@EmbeddedId
를 사용하는 코드를 보자.
저장
저장하는 코드를 보면 식별자 클래스 parentId를 직접 생성해서 사용한다.
조회
조회 코드도 식별자 클래스 parentId를 직접 사용한다.
@IdClass
와 @EmbeddedId
를 사용했을 때 JPQL은 아래와 같다.
em.createQuery("SELECT p.id1, p.id2, FROM parent p"); //@IdClass
em.createQuery("SELECT p.id.id1, p.id.id2 FROM parent p"); //@EmbeddedId
주의!
복합 키에는@GenerateValue
를 사용할 수 없다. 복합 키를 구성하는 여러 컬럼 중 하나에도 사용할 수 없다.
아래의 복합 키 방식의 식별 관계 매핑의 예제를 알아보자.
위 그림을 보면 각 테이블의 기본 키는 관계 테이블의 기본 키 + a 의 형태이다.
식별 관계에서 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야 하므로 @IdClass
또는 @EmbeddedId
를 사용해서 식별자를 매핑해야 한다.
먼저 @IdClass
로 식별 관계를 매핑해보자.
위 그림에 해당하는 예제 코드를 통해 알아보자.
식별 관계는 기본 키와 외래 키를 같이 매핑해야 한다. 따라서 식별자 매핑인 @Id와 연관관계 매핑인 @ManyToOne을 같이 사용하면 된다.
@Id
@ManyToOne
@JoinColumn(name = "parent_id")
pulic Parent parent;
@EmbeddedId
로 식별 관계를 구성할 때는 @MapsId
를 사용해야 한다.
예제를 통해 알아보자.
데이터베이스 설계 관점에서 보면 다음과 같은 이유로 식별 관계보다는 비식별 관계를 선호한다.
식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어난다.
예를 들어 부모 테이블은 기본 키 컬럼이 하나였지만 자식 테이블은 기본 키 컬럼이 2개, 손자 테이블은 기본 키 컬럼이 3개로 점점 늘어난다. 결국 조인할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다.
식별 관계를 사용할 때 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많다. 반면에 비식별 관계의 기본 키는 비즈니스와 전혀 관계없는 대리 키를 주로 사용한다.
비즈니스 요구사항은 시간이 지남에 따라 언젠가는 변한다. 식별 관계의 자연 키 컬럼들이 자식에 손자까지 전파되면 변경하기 힘들다.
식별 관계는 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하므로 비식별 관계보다 테이블의 구조가 유연하지 못하다.
객체 관계 매핑의 관점에서 보면 다음과 같은 이유로 비식별 관계를 선호한다.
일대일 관계를 제외하고 식별 관계는 2개 이상의 컬럼으 묶은 복합 기본 키를 사용한다. JPA에서 복합 키는 별도의 복합 키 클래스를 만들어서 사용해야 한다. 따라서 컬럼이 하나인 기본 키를 매핑하는 것보다 많은 노력이 필요하다.
비식별 관계의 기본 키는 주로 대리 키를 사용하는데 JPA는 @GenerateValue
처럼 대리 키를 생성하기 위한 편리한 방법을 제공한다.
물론 식별 관계가 갖는 장점도 있다.
기본 키 인덱스를 활용하기 좋다.
상위 테이블들의 기본 키 컬럼을 자식, 손자 테이블들이 가지고 있으므로 특정 상황에 조인 없이 하위 테이블만으로 검색을 완료할 수 있다.
이처럼 식별 관계가 가지는 장점도 있으므로 꼭 필요한 곳에는 적절하게 사용하는 것이 데이터베이스 테이블 설계의 묘를 살리는 방법이다.
정리
ORM 신규 프로젝트 진행시 추천하는 방법은 될 수 있으면 비식별 관계를 사용하고 기본 키는 Long 타입의 대리 키를 사용하는 것이다.
대리 키는 비즈니스와 아무 관련이 없다. 따라서 비즈니스가 변경되어도 유연한 대처가 가능하다는 장점이 있다.
JPA는@GeneratedValue
를 통해 간편하게 대리 키를 생성할 수 있다. 그리고 식별자 컬럼이 하나여서 쉽게 매핑할 수 있다.
그리고 선택적 비식별 관계보다는 필수적 비식별 관계를 사용하길 권장한다. 선택적인 비식별 관계는 NULL을 허용하므로 조인할 때에 외부 조인을 사용해야 한다. 반면에 필수적 관계는 NOT NULL로 항상 관계가 있다는 것을 보장하므로 내부 조인만 사용해도 된다.