이 포스트는 김영한님의 "자바 ORM 표준 JPA 프로그래밍"을 참조하였음을 알립니다.
📕 목차
1. 상속 관계 매핑
2. @MappedSuperclass
3. 복합 키와 식별 관계 매핑 : 조인 전략
4. 조인 테이블 : 단일 테이블 전략
5. 엔티티 하나에 여러 테이블 매핑 : 구현 클래스마다 테이블 전략
6. 정리
1. 상속 관계 매핑
• As-is
• 조인 전략(Joined Strategy)
• 단일 테이블 전략(Single-Table Strategy)
• 구현 클래스마다 테이블 전략(Table-per-Concrete-Class Strategy)
🤔 As-is
- 관계형 데이터베이스에는 상속이라는 개념이 없다.
- 대신 비슷한 개념으로 Super-Type Sub-Type Relationship 모델링 기법이 존재한다.
- 각각의 테이블로 변환 <조인 전략>
- 통합 테이블로 변환 <단일 테이블 전략>
- 서브타입 테이블로 변환 <테이블 전략>
📌 조인 전략(Joined Strategy)
- Entity를 각각의 Table로 만든다.
- 자식 Table이 부모 Table의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다.
- Object와 달리 Table은 타입 개념이 없으므로, 타입 구분용 컬럼(dtype)을 추가해야 한다.
1️⃣ 장점
- 테이블 정규화
- 외래 키 참조 무결성 제약조건 활용 가능
- 효율적인 저장공간
2️⃣ 단점
- 조회할 때 조인 횟수 증가로 성능 저하 우려
- 조회 쿼리 복잡도 증가
- 데이터 등록할 INSERT SQL 두 번 실행 요구
3️⃣ 특징
- JPA 표준 명세는 구분 컬럼(@DiscriminatorColumn) 사용 권장
- 하이버네이트를 포함한 몇몇 구현체는 구분 컬럼 없이도 동작
4️⃣ 관련 어노테이션
- @PrimaryKeyJoinColumn
- @DiscriminatorColumn
- @DiscriminatorValue
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE") // 기본값 DTYPE (생략 가능)
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
}
- @Inheritance(strategy = InheritanceType.JOINED)
- 상속 매핑 시, 부모 클래스에 사용해야 한다.
- 매핑 전략 중 조인 전략(InheritanceType.JOINED)을 사용
- @DiscriminatorColumn(name = "DTYPE")
- 부모 클래스에 구분 컬럼(DiscriminatorColumn) 지정
- 해당 컬럼으로 자식 Table 구분
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
private String artist;
}
- @DiscriminatorValue("A")
- Entity 저장할 때 구분 컬럼에 입력할 값 지정
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
private String director;
private String actor;
}
@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID") // ID 재정의
public class Book extends Item {
private String author;
private String isbn;
}
- @PrimaryKeyJoinColumn(name = "BOOK_ID")
- 자식 테이블은 기본 값으로 부모 테이블 @Id 컬럼명 사용
- 기본 키 이름을 바꾸고 싶을 때 사용
📌 단일 테이블 전략(Single-Table Strategy)
- 구분 컬럼(DTYPE)으로 어떤 자식 데이터가 저장되었는지 구분한다. (인수인계 빡셀 듯)
- 조인을 사용하지 않으므로 가장 빠르다. (하지만 빨랐죠?)
무슨 예능 전략 이런 건가? 실무에서 진짜 쓰는 경우가 있을 지 의문이었는데, 그래도 이 전략은 나름대로 쓰임이 있는 것 같다.
일반적으로 테이블이 많으면 관리가 힘들어서 그런 듯 한데, 역시 이론과 실전은 다르다. ㅠ
1️⃣ 장점
- 조인이 필요 없으므로, 일반적으로 조회 성능 빠름
- 조회 쿼리 단순
2️⃣ 단점
- 자식 Entity가 매핑한 컬럼은 모두 null 허용
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 상황에 따라 조회 성능 저하 우려
3️⃣ 특징
- 구분 컬럼 필수
- @DiscriinatorValue를 지정하지 않으면 기본으로 Entity 이름을 사용
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
}
이전 코드에서 매핑 전략만 SINGLE_TABLE로 수정하면 된다.
📌 구현 클래스마다 테이블 전략(Table-per-Concrete-Class Strategy)
- 그냥 전부 따로 만드는 전략 (이딴 게 전략...)
- DB 설계자와 ORM 전문가 둘 다 추천하지 않는다.
1️⃣ 장점
- Sub type을 구분해서 처리할 때 효과적
- not null 제약조건 사용 가능
2️⃣ 단점
- 여러 자식 테이블 함께 조회 시, 성능 저하 (SQL의 UNION 사용해야 함)
- 자식 테이블을 통합해서 쿼리하기 힘듦
3️⃣ 특징
- 구분 컬럼을 사용하지 않는다.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
}
매핑 전략을 "TABLE_PER_CLASS"로 수정해주고 구분 컬럼 어노테이션을 지워주면 된다.
2. @MappedSuperclass
- 부모 클래스는 DB Table에 매핑하지 않고, 상속받는 자식 클래스에게 매핑 정보만 알려주는 방법.
- @MappedSuperclass로 지정된 클래스는 Entity가 아니므로 em.find()나 JPQL 사용 불가
- 해당 클래스를 직접 생성할 일은 거의 없으므로 추상 클래스 생성을 권장
- 보통은 @Embedded를 더 많이 사용한다.
- 생성 시간과 수정 시간과 같은 거의 모든 Entity의 컬럼으로 들어가는 요소에 자주 사용한다.
- 상속받는 자식 테이블은 서로 관련이 없어도 상관 없다.
@MappedSuperclass
@EntityListeners(value = {AuditingEntityListener.class})
public abstract class BaseDateEntity {
@CreatedDate @Column(name="create_date", updatable = false)
private LocalDateTime created_date;
@LastModifiedDate @Column(name="last_modified_date", updatable = true)
private LocalDateTime last_modified_date;
}
@Entity
public class User extends BaseDateEntity { ... }
@Entity
public class Group extends BaseDateEntity { ... }
📌 매핑 정보 수정
- 매핑 정보 재정의 : @AttributeOverrides, @AttributeOverride
- 연관 관계 재정의 : @AssociationOverrides, @AssociationOverride
@Entity
@AttributeOverride(name="created_date", column=@Column(name="user_create_date", updatable = false))
public class User extends BaseDateEntity { ... }
@AttributeOverrides({
@AttributeOverride(
name="created_date", column=@Column(name="user_create_date", updatable = false)),
@AttributeOverride(
name="last_modified_date", column=@Column(name="user_last_modified_date", updatable = true))
})
public class User extends BaseDateEntity { ... }
💡 Entity는 Entity거나 MappedSuperclass로 지정한 클래스만 상속 가능
3. 복합 키와 식별 관계 매핑 : 조인 전략
• 식별 관계 vs 비식별 관계
• 복합 키: 비식별 관계 매핑 (@IdClass vs @EmbbededId)
• 복합 키: 식별 관계 매핑
• 비식별 관계로 구현
• 일대일 식별 관계
• 식별, 비식별 관계 장단점
💡 복합 키를 구성하는 여러 컬럼 중 단 하나에도 @GenerateValue를 사용할 수 없다.
📌 식별 관계 vs 비식별 관계
최근에는 비식별 관계를 주로 사용하고, 꼭 필요한 곳에 식별 관계를 사용하는 추세
1️⃣ 식별 관계(Identifying Relationship)
- 부모 Table의 기본 키를 내려 받아 자식 테이블의 기본 키 + 외래 키로 사용하는 관계
2️⃣ 비식별 관계(Non-Identifying Relationship)
외래 키 NULL 허용 여부에 따라 달라진다.
- 필수적 비식별 관계(Mandatory) : 외래 키는 Not NULL, 연관 관계를 필수적으로 맺어야 한다.
- 선택적 비식별 관계(Optional) : 왜래 키 NULL 허용, 연관 관계를 맺지 않아도 된다.
📌 복합 키: 비식별 관계 매핑 (@IdClass vs @EmbeddedId)
@Entity
public class Foo {
@Id private String id1;
@Id private String id2; // 실행 시점에 매핑 예외
}
- 위의 코드는 Runtime error 발생
- JPA는 Entity 식별자를 키로 사용한다.
- 식별자 구분을 위해 equals와 hashCode 메서드를 통해 동등성 비교를 수행한다.
- 식별자 필드가 하나면 자바 기본 타입, 둘 이상이면 별도의 식별자 클래스 구현
- JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스 생성 필요
- @IdClass : 관계형 데이터베이스에 가까운 방법
- @EmbeddedId : 객체 지향에 가까운 방법
1️⃣ @IdClass
- 식별자 클래스를 사용하는 클래스에서 @IdClass 적용
- 식별자 클래스 속성명과 Entity에서 사용하는 식별자의 속성명이 같아야 한다.
- Serializable 인터페이스를 구현
- equals, hashCode를 구현
- 기본 생성자 필수
- 식별자 클래스는 public
(대부분 @IdClass 특징이라기 보단 객체 지향 설계 원칙에 가깝다.)
public class ComplexKey implements Serializable {
private String id1;
private String id2;
public ComplexKey() {}
public ComplexKey(String id1, String id2) {
this.id1 = id1;
this.id2 = id2;
}
@Override public boolean equals(Object o) {
if (!(o instanceof ComplexKey)) return false;
ComplexKey that = (ComplexKey) o;
return id1.equals(that.id1) && id2.equals(that.id2);
}
@Override public int hashCode() {
return id1.hashCode() + id2.hashCode();
}
}
@Entity
@IdClass(ComplexKey.class)
public class Parent {
@Id @Column(name = "ID1")
private String id1;
@Id @Column(name = "ID2")
private String id2;
private String name;
}
@Entity
public class Child {
@Id
private String id;
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID1", referencedColumnName = "ID1"),
@JoinColumn(name = "PARENT_ID2", referencedColumnName = "ID2")
})
private Parent parent;
}
- 부모 Table의 기본 키 컬럼이 복합 키이므로 자식 Table의 외래 키도 복합 키
- 외래 키 매핑 시, 여러 컬럼을 매핑하므로 @JoinColums 사용
✒️ 사용해보기
1. 복합키 사용 엔티티 저장
Parent parent = new Parent();
parent.setId1("myId1");
parent.setId2("myId2");
parent.setName("myName");
em.persist(parent);
- em.persist() 호출 시, 영속성 컨텐스트에 Entity 등록 직전 내부에서 식별자 클래스 ComplexId를 생성한다.
2. 복합키로 조회
ComplexId complexId = new ComplexId("myId1", "myId2");
Parent parent = em.find(ComplexId.class, complexId);
- ComplexId 식별자 클래스를 사용해서 Entity 조회
2️⃣ @EmbeddedId
- 식별자 클래스에 @Embeddable 어노테이션 적용
- Serializer 인터페이스, equals, hashCode 구현
- 기본 생성자 필수
- 식별자 클래스는 public
@Embeddable
public class ComplexKey implements Serializable {
@Column(name = "ID1")
private String id1;
@Column(name = "ID2")
private String id2;
@Override public boolean equals(Object o) { ... }
@Override public int hashCode() { ... }
}
@Entity
public class Parent {
@EmbeddedId
private ComplexKey id;
private String name;
}
- @EmbeddedId를 적용한 식별자 클래스는 기본 키에 직접 매핑
✒️ 사용해보기
1. Entity 저장
Parent parent = new Parent();
ComplexId complexId = new ComplexId("myId1", "myId2");
parent.setId(complexId);
parent.setName("myName");
em.persist(parent);
2. Entity 조회
ComplexId complexId = new ComplexId("myId1", "myId2");
Parent parent = em.find(ComplexId.class, complexId);
✒️ 복합 키와 equals(), hashCode()
이건 JPA 내용이기도 하지만 자바 기본 설계 원칙과 관련된 이야기다.
• [Effective-Java] Chapter3 #10. equals는 일반 규약을 지켜 재정의하라
• [Effective-Java] Chapter3 #11. equals를 재정의하려거든 hashCode도 재정의하라
✒️ @IdClass vs @EmbeddedId
• 각각의 장단점을 찾아 취향에 맞는 것을 일관성 있게 사용하면 된다.
• @EmbeddedId가 더 객체지향적이고 중복은 적지만 특정 상황에 JPQL 더 길어질 수 있다.
.∘ em.createQuery("select p.id.id1, p.id.id2 from Parent p"); // @EmbeddedId
∘ em.createQuery("select p.id1, p.id2 from Parent p"); // @IdClass
📌 복합 키: 식별 관계 매핑
1️⃣ @IdClass와 식별 관계
@Entity
public class Parent {
@Id @Column(name = "PARENT_ID")
private String id;
private String name;
}
public class ChildId implements Serializable {
private String parent;
private String childId;
@Override public boolean equals(Object o) {...}
@Override public int hashCode() {...}
}
@Entity
@IdClass(ChildId.class)
public class Child {
@Id @ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;
@Id @Column(name = "CHILD_ID")
private String childId;
private String name;
}
public class GrandChildId implements Serializable {
private ChildId child;
private String id;
@Override public boolean equals(Object o) { ... }
@Override public int hashCode() { ... }
}
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
@Id @ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID"),
@JoinColumn(name = "CHILD_ID")
})
private Child child;
@Id @Column(name = "GRANDCHILD_ID")
private String id;
private String name;
}
- 식별 관계는 기본 키와 외래 키를 같이 매핑해야 한다.
- 따라서 식별자 매핑 @Id와 연관관계 매핑 @ManyToOne을 같이 사용한다.
2️⃣ @EmbeddedId와 식별 관계
@Entity
public class Parent {
@Id @Column(name = "PARENT_ID")
private String id;
private String name;
}
@Embeddable
public class ChildId implements Serializable {
private String parentId; // @MapsId("parentId") 로 매핑
@Column(name = "CHILD_ID")
private String id;
@Override public boolean equals(Object o) {...}
@Override public int hashCode() {...}
}
@Entity
public class Child {
@EmbeddedId
private ChildId id;
@MapsId("parentId") @ManyToOne
@JoinColumn(name="PARENT_ID")
public Parent parent;
private String name;
}
@Embeddable
public class GrandChildId implements Serializable {
private ChildId childId; // @MapsId("childId") 로 매핑
@Column(name = "GRANDCHILD_ID")
private String id;
@Override public boolean equals(Object o) {...}
@Override public int hashCode() {...}
}
@Entity
public class GrandChild {
@EmbeddedId
private GrandChildId id;
@MapsId("childId") @ManyToOne
@JoinColumns({
@JoinColumn(name="PARENT_ID"),
@JoinColumn(name="CHILD_ID"),
})
public Parent child;
private String name;
}
- @MapsId : 왜래키에 매핑한 연관관계를 기본 키에도 매핑
- @MapsId의 속성 값은 @EmbeddedId를 사용한 식별자 클래스의 기본 키 필드 지정
📌 비식별 관계로 구현
- 기존에 사용하던 방식과 동일
- 복합 키 사용 코드보다 매핑도 쉽고 코드도 단순하다.
- 복합 키 클래스를 만들 이유도 없다.
📌 일대일 식별 관계
@Entity
public class Board {
@Id @GeneratedValue
@Column(name = "BOARD_ID")
private Long id;
private String title;
@OneToOne(mappedBy = "board")
private BoardDetail boardDetail;
}
@Entity
public class BoardDetail {
@Id
private Long boardId;
@MapsId @OneToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
private String content;
}
- 일대일 식별 관계에서 자식 Table 기본 키 값은 부모 Table 키 값만 사용
- 부모 Table 기본 키가 복합 키가 아니라면 자식 Table은 복합 키로 구성할 필요가 없다.
📌 식별, 비식별 관계 장단점
💡 기본 키는 되도록 비식별 관계의 Long 타입 대리 키를 사용하라
- 식별 관계 장점
- 기본 키 인덱스 활용하기 좋다.
- 특정 상황에 조인 없이 하위 테이블만으로 검색 완료 가능
- 식별 관계 단점
- 상속이 늘어날 수록 기본 키 컬럼 증가 → SQL 복잡도 증가
- 복합 키 클래스 구현 필요
- 식별 관계의 경우 자연 키 컬럼을 조합하는 경우가 많은데, 추후 수정 힘듦
- 자식이 부모 기본 키를 참조하므로 테이블 구조 유연성 저하
- 비식별 관계 기본 키는 @GenerateValue로 쉽게 생성 가능한 반면, 식별 관계는 그렇지 않음.
4. 조인 테이블 : 단일 테이블 전략
• 연관 관계를 설계하는 방법
• 일대일 조인 테이블
• 일대다 조인 테이블
• 다대일 조인 테이블
• 다대다 조인 테이블
📌 연관 관계를 설계하는 방법
데이터베이스 테이블 연관관계 설계 방법은 크게 2가지가 있다.
- 조인 컬럼 사용 (외래 키)
- 이 경우, 외래 키에 null을 허용하여 선택적 비식별 관계를 유지해야 한다.
- 외부 조인이 아닌 내부 조인을 사용해버리면 사물함과 관계가 매핑되지 않은 회원은 조회되지 않는다.
- 회원과 사물함이 아주 가끔 관계를 매핑한다면 외래 키 값 대부분이 null로 고정된다.
- @JoinColumn
- 조인 테이블(연결 테이블, 링크 테이블) 사용
- 연관 관계를 관리하는 조인 테이블(MEMBER_LOCKER)을 추가한다.
- 조인 테이블이 두 테이블의 외래 키를 가지고 연관 관계를 관리한다.
- 관리해야 하는 테이블이 늘어난다
- 두 테이블을 조인하기 위해, MEMBER_LOCKER 테이블까지 추가로 조인해야 한다.
- 주로 다대다 관계를 풀어내기 위해 사용하지만, 그 외의 관계에서도 사용한다.
- @JoinTable
📌 일대일 조인 테이블
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private String id;
private String name;
@OneToOne
@JoinTable(name = "PARENT_CHILD",
joinColumns = @JoinColumn(name = "PARENT_ID"),
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
)
private Child child;
}
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private String childId;
private String name;
}
- @JoinTable의 속성
- name : 매핑할 조인 테이블 이름
- joinColumns : 현재 엔티티를 참조하는 외래 키
- inerseJoinColumns : 반대방향 엔티티를 참조하는 외래 키
양방향으로 매핑하고 싶다면 Child에 다음 필드를 추가해야 한다.
public class Child {
@OneToOne(mappedBy = "child")
private Parent parent;
}
📌 일대다 조인 테이블
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private String id;
...
@OneToMany
@JoinTable(name = "PARENT_CHILD",
joinColumns = @JoinColumn(name = "PARENT_ID"),
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
)
private List<Child> child = new ArrayList<>();
}
- 일대다 관계에선 조인 테이블 컬럼의 다와 관련된 child_id에 유니크 제약조건을 걸어야 한다.
📌 다대일 조인 테이블
@Entity
public class Parent {
...
@OneToMany(mappedBy = "parent")
private List<Child> child = new ArrayList<>();
}
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private String childId;
...
@ManyToOne(optional = false)
@JoinTable(name = "PARENT_CHILD",
joinColumns = @JoinColumn(name = "CHILD_ID"),
inverseJoinColumns = @JoinColumn(name = "PARENT_ID")
)
private Parent parent;
}
- 테이블 모양이 일대다와 반대로 매핑하면 된다.
📌 다대다 조인 테이블
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private String id;
...
@ManyToMany
@JoinTable(name = "PARENT_CHILD",
joinColumns = @JoinColumn(name = "PARENT_ID"),
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
)
private List<Child> child = new ArrayList<>();
}
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private String childId;
private String name;
...
}
당연히 여기서도 Child가 역참조를 할 수 있도록 하고 싶다면 필드를 다음과 같이 추가해야 한다.
@ManyToMany(mappedBy = "child")
private List<Parent> parents = new ArrayList<>();
5. 엔티티 하나에 여러 테이블 매핑 : 구현 클래스마다 테이블 전략
@Entity
@Table(name = "BOARD")
@SecondaryTable(name = "BOARD_DETAIL",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "BOARD_DETAIL_ID"))
public class Board {
@Id @GeneratedValue
@Column(name = "BOARD_ID")
private Long id;
private String title;
@Column(table = "BOARD_DETAIL")
private String content;
}
- 잘 사용하지는 않지만 @SecondaryTables, @SecondaryTable을 이용해서 한 엔티티에 여러 테이블을 매핑할 수 있다.
- @SecondaryTable 속성
- name : 매핑할 다른 테이블 이름
- pkJoinColumns : 매핑할 다른 테이블의 기본 키 컬럼 속성