해당 포스팅은 인프런에서 제공하는 김영한 님의 '자바 ORM 표준 JPA 프로그래밍 - 기본편'을 수강한 후 정리한 글입니다. 유료 강의를 정리한 내용이기에 제공되는 예제나 몇몇 내용들은 제외하였고, 정리한 내용을 바탕으로 글 작성자인 저의 언어로 다시 작성한 글이기에 서술이 부족하거나 잘못된 내용이 있을 수 있습니다. 그렇기에 해당 글은 개념에 대한 참고 정도만 해주시고, 강의를 통해 학습하시기를 추천합니다.
RDB에서는 객체지향과 같은 상속 개념이 없지만 슈퍼타입과 서브타입 관계라는 모델링 기법이 존재한다. ORM에서의 상속 관계 매핑은 객체의 상속 구조를 RDB의 슈퍼타입-서브타입 관계로 매핑하는 것이다.
상속 관계 매핑 시 선택할 수 있는 방법은 다음과 같은 세가지 방법이 있다.
다음은 상속 관계 매핑시 사용하는 어노테이션과 그 속성 값이다.
1. @Inheritance(stragey = "매핑 전략")
2. @Discriminator(name = 구분 컬럼명)
3. @DiscriminatorValue("구분 컬럼 값")
슈퍼타입과 서브타입, 즉 부모 엔티티와 자식 엔티티를 모두 테이블로 만들고 자식 테이블에 부모 테이블의 기본키를 받아 기본 키 + 외래 키로 사용하는 전략이다. 때문에 조회 시 조인을 자주 사용하며, 타입으로 구분할 수 있는 객체와 달리 타입의 개념이 없는 테이블을 구분하기 위해 구분을 위한 컬럼(DTYPE
)을 추가해야 한다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
...
}
@Entity
@DiscriminatorValue("A") // 생략시 엔티티 이름 기본 사용
public class ChildA extends Parent {
...
}
@Entity
@DiscriminatorValue("B")
public class ChildB extends Parent {
...
}
기본 값으로 자식 테이블은 부모 테일블의 ID 컬럼명을 그대로 사용한다. 자식 테이블의 기본 키 컬럼명을 변경하고 싶은 경우 @PrimaryKeyJoinColumn
을 사용하면 된다.
@Entity
@DiscriminatorValue("A")
@PrimaryKeyJoinColumn(name = "CHILD_A_ID")
public class ChildA extends Parent {
...
}
조인 전략의 장점은 테이블이 정규화 되며, 외래 키 참조 무결성 제약조건을 활용할 수 있고 저장공간을 효율적으로 사용한다는 점이 있다.
그러나 조회할 때 조인이 많이 사용되어 성능이 저하될 수 있고, 조회 쿼리가 복잡하며, 데이터를 등록할 때 INSERT SQL을 두 번 실행한다는 단점도 존재한다.
테이블을 하나만 사용하되 구분 컬럼(DTYPE
)으로 어떤 자식 데이터가 저장되었는지 구분하는 전략이다. 조회할 때 조인을 사용하지 않기 때문에 일반적으로 가장 빠르다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE") // 필수
public abstract class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
...
}
@Entity
@DiscriminatorValue("A") // 생략시 엔티티 이름 기본 사용
public class ChildA extends Parent {
...
}
주의해야할 점은 저장되는 자식 데이터의 컬럼이 아닌 경우 해당 컬럼에 null이 들어가기 때문에 자식 엔티티의 모든 컬럼이 null을 허용해야 한다는 점과 구분 컬럼을 필수로 사용해야 한다는 점이 있다.
단일 테이블 전략의 장점은 조인이 필요 없기에 조회 성능이 가장 빠르다는 점과 조회 쿼리가 단순하다는 점이 있지만 자식 엔티티가 매핑한 컬럼은 모두 null을 허용한다는 점과 단일 테이블에 자식 객체의 데이터를 모두 저장하기에 테이블이 커질 수 있어 상황에 따라 조회 성능이 오히려 느려질 수 있다는 단점도 존재한다.
자식 엔티티마다 테이블을 만들며, 자식 테이블 각각에 필요한 모든 컬럼이 존재한다. 따라서 구분 컬럼을 사용하지 않는다.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
...
}
@Entity
public class ChildA extends Parent {
...
}
서브 타입을 구분해서 처리할 때 효과적이며, not null 제약조건을 사용할 수 있지만 여러 자식 테이블을 함께 조회할 때 UNION을 사용해야 하기에 성능이 느리며, 자식 테이블을 통합해서 쿼리하기 어렵다는 점 때문에 일반적으로 추천하지 않는 전략이다.
@MappedSuperclass
부모 클래스는 테이블과 매핑하지 않고 자식 클래스에게 매핑 정보만 제공하고 싶을 경우 @MappedSuperclass
를 사용한다. 주로 객체들에서 공통적으로 사용되는 공통 매핑 정보를 정의하고 이를 상속을 통해 사용하고 싶은 경우 사용한다.
@MappedSuperclass
public abstract class BaseEntity {
@Id @GeneratedValue
private Long id;
private String name;
...
}
@Entity
public class One extends BaseEntity {
// ID 상속
// NAME 상속
...
}
@Entity
public class AnotherOne extends BaseEntity {
// ID 상속
// NAME 상속
...
}
부모 객체로 부터 물려받은 매핑 정보를 재정의하려면 @AttributeOverrides
나 @AttributeOverride
를 사용하고 연관관계를 재정의하려면 @AssociantionOverrides
나 @AssociantionOverride
를 사용한다.
// 하나의 컬럼 정보 재정의
@Entity
@AttributeOvverride(name = "id", column = @Column(name = "ONE_ID"))
public class One extends BaseEntity {...}
// 여러 개의 컬럼 정보 재정의
@Entity
@AttributeOvverrides({
AttributeOvverride(name = "id", column = @Column(name = "ONE_ID")),
AttributeOvverride(name = "name", column = @Column(name = "ONE_NAME"))
})
public class One extends BaseEntity {...}
@MappedSuperclass
의 특징은 다음과 같다.
@MappedSuperclass
로 지정한 클래스는 엔티티가 아니며, 엔티티매니저.find()
나 JPQL에서 사용할 수 없다.데이터베이스에서 테이블 사이의 관계는 외래 키가 기본 키에 포함되었는지 여부에 따라 식별 관계와 비식별 관계로 구분된다.
식별 관계는 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계를 말한다.
비식별 관계는 부모 테이블의 기본 키를 자식 테이블에서 외래 키로만 사용하는 관계이다. 외래 키에 null을 허용하는지 여부에 따라 필수적 비식별 관계와 선택적 비식별 관계로 나뉜다.
최근에는 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 추세이지만 JPA는 이러한 식별 관계와 비식별 관계를 모두 지원한다.
JPA에서 둘 이상의 컬럼으로 구성된 복합 기본 키를 매핑할 경우 별도의 식별자 클래스를 만들어야 한다.
JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용하며, 이를 구분하기 위해 eauals
와 hashCode
를 사용해 동등성 비교를 한다. 따라서 식별자 필드가 하나일 때 보통 자바의 기본 타입을 사용하므로 문제가 없으나 식별자 필드가 두 개 이상이면 별도의 식별자 클래스를 만들고 eauals
와 hashCode
를 구현해야 한다.
JPA는 복합 키를 지원하기 위해 좀 더 관계형 데이터베이스에 가까운 방법인 @IdClass
와 객체지향에 가까운 방법인 @EmbeddedId
두 가지를 제공한다.
@IdClass
@IdClass
를 사용할 때 식별자 클래스는 다음과 같은 조건들을 만족해야 한다.
Serializable
인터페이스를 구현해야 한다.equals
와 hashCode
를 구현해야 한다.이를 토대로 부모 클래스, 식별자 클래스, 자식 클래스를 구현하면 다음과 같다.
// 부모 클래스
@Entity
@IdClass(ParentId.class)
public class Parent {
@Id
@Column(name = "PARENT_ID1")
private Long id1;
@Id
@Column(name = "PARENT_ID2")
private Long id2;
...
}
// 식별자 클래스
public class ParentId implements Serializable {
// public 이면서 Serializable 인터페이스 구현
private Long id1; // 엔티티의 식별자 속성명과 동일
private Long id2; // 엔티티의 식별자 속성명과 동일
public ParentId() {} // 기본 생성자 존재
public ParentId(Long id1, Long id2) {
this.id1 = id1;
this.id2 = id2;
}
// equals와 hashCode 구현
@Override
public boolean equals(Object o) {...}
@Override
public int hashCode() {...}
}
// 자식 클래스
@Entity
public class child {
@Id
private Long id;
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID1", referencedColumns = "PARENT_ID1"),
@JoinColumn(name = "PARENT_ID2", referencedColumns = "PARENT_ID2")
})
private Parent parent;
}
부모 테이블의 기본 키 컬럼이 복합 키이기 때문에 자식 테이블의 외래 키도 복합 키다. 따라서 외래 키 매핑 시 여러 컬럼을 매핑해야 하므로 @JoinColumns
어노테이션을 사용하고, 각각의 외래 키 컬럼을 @JoinColumn
으로 매핑한다. @JoinColumn
의 name
속성과 referencedColumnName
속성의 값이 같을 경우 referencedColumnName
은 생략이 가능하다.
@EmbeddedId
좀 더 객체지향적인 방법이다. @IdClass
와는 달리 식별자 클래스에 기본 키를 직접 매핑하며, 다음과 같은 조건을 만족해야 한다.
@Embeddable
어노테이션을 선언해주어야 한다.Serializable
인터페이스를 구현해야 한다.equals
, hashCode
를 구현해야 한다.이를 토대로 부모 클래스, 식별자 클래스, 자식 클래스를 구현하면 다음과 같다.
// 부모 클래스
@Entity
public class Parent {
@EmbeddedId
private ParentId id;
...
}
// 식별자 클래스
@Embeddable
public class ParentId implements Serializable {
// public 이면서 Serializable 인터페이스 구현
@Column(name = "PARENT_ID1")
private Long id1;
@Column(name = "PARENT_ID2")
private Long id2;
public ParentId() {} // 기본 생성자 존재
...
// equals와 hashCode 구현
@Override
public boolean equals(Object o) {...}
@Override
public int hashCode() {...}
}
// 자식 클래스
@Entity
public class child {
@Id
private Long id;
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID1", referencedColumns = "PARENT_ID1"),
@JoinColumn(name = "PARENT_ID2", referencedColumns = "PARENT_ID2")
})
private Parent parent;
}
식별 관계에서 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야 하므로 @IdClass
나 @EmbeddedId
를 사용해서 식별자를 매핑해야 한다.
@IdClass
// 부모 클래스
@Entity
public class Parent {
@Id @Column(name = "PARENT_ID")
private Long id;
}
// 자식 클래스
@Entity
@IdClass(ChildId.class)
public class Child {
@Id
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;
@Id @Column(name = "CHILD_ID")
private Long childId;
...
}
// 자식 ID
public class ChildId implements Serializable {
private Long parent; // Child.parent 매핑
private Long childId; // Child.childId 매핑
// equals, hashCode...
}
식별 관계는 기본 키와 외래 키를 같이 매핑해야 하기 때문에 식별자 매핑인 @Id
와 연관관계 매핑인 @ManyToOne
을 같이 사용한다.
@EmbeddedId
@EmbeddedId
로 구성할 때는 @MapsId
를 사용한다.
// 부모 클래스
@Entity
public class Parent {
@Id @Column(name = "PARENT_ID")
private Long id;
}
// 자식 클래스
@Entity
public class Child {
@EmbeddedId
private ChildId id;
@MapsId("parentId") // ChildId.parentId 매핑
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;
...
}
// 자식 ID
@Embeddable
public class ChildId implements Serializable {
private Long parentId; // @MapsId("parentId")로 매핑
@Column(name = "CHILD_ID")
private Long id;
// equals, hashCode...
}
@IdClass
와 다른 점은 @Id
대신에 @MapsId
를 사용한다는 점이며, 이는 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 의미이다. 속성 값은 @EmbeddedId
를 사용한 식별자 클래스의 기본 키 필드를 지정하면 된다.
일대일 식별 관계는 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만을 사용한다. 따라서 부모 키가 복합 키가 아니라면 자식 테이블의 기본 키는 복합 키로 구성하지 않아도 된다.
// 부모 클래스
@Entity
public class Sample {
@Id @GeneratedValue
@Column(name = "SAMPLE_ID")
private Long id;
@OneToOne(mappedBy = "smaple")
private SampleDetail sampleDetail;
}
// 자식 클래스
@Entity
public class SampleDetail {
@Id
private Long id;
@MapsId
@OneToOne
@JoinColumn(name = "SAMPE_ID")
public Sample sample;
...
}
식별자가 단순히 컬럼 하나일 경우 @MapsId
를 사용하고 속성 값을 사용하지 않으면 되며, @Id
를 사용해서 식별자로 지정한 부모 테이블의 기본 키와 매핑된다.
데이터베이스 설계 관점에서는 다음과 같은 이유로 식별 관계보다 비식별 관계가 선호된다.
@GeneratedValue
와 같이 편리하게 대리 키를 생성하기 위한 방법을 제공해주기 때문에 비식별 관계로 생성하기가 보다 쉽다.반면 식별 관계가 가지는 장점은 기본 키 인덱스를 활용하기 좋고, 상위 테이블의 기본 키 컬럼을 자식 테이블들이 가지고 있기 때문에 특정 상황에서 조인 없이 하위 테이블만으로 검색을 완료할 수 있다는 점이 있다.