Home 자바 ORM 표준 JPA 프로그래밍 - 고급 매핑
Post
Cancel

자바 ORM 표준 JPA 프로그래밍 - 고급 매핑

Table of Contents

  1. 상속 관계 매핑
    1. 조인 전략
      1. 장점
      2. 단점
      3. 특징
    2. 단일 테이블 전략
      1. 장점
      2. 단점
      3. 특징
    3. 구현 클래스마다 테이블 전략
      1. 장점
      2. 단점
      3. 특징
  2. @MappedSuperclass
  3. 복합 키와 식별 관계 매핑
    1. 식별 관계 vs 비식별 관계
      1. 식별 관계
      2. 비식별 관계
    2. 복합 키: 비식별 관계 매핑
      1. @IdClass
      2. @EmbeddedId
      3. @IdClass vs @EmbeddabledId
    3. 복합 키: 식별 관계 매핑
      1. @IdClass와 식별관계
      2. @EmbeddabledId와 식별 관계
    4. 비식별 관계로 구현
    5. 일대일 식별 관계
    6. 식별, 비식별 관계의 장단점
  4. 조인 테이블
    1. 일대일 조인 테이블
    2. 일대다 조인 테이블
    3. 다대일 조인 테이블
    4. 다대다 조인 테이블
  5. 엔티티 하나에 여러 테이블 매핑

상속 관계 매핑

관계형 데이터베이스에는 객체지향 언어에서 다루는 상속이라는 개념이 없다. 대신 슈퍼타입 서브타입 관계라는 모델링 기법이 객체의 상속 개념과 가장 유사하다.
ORM에서 이야기하는 상속 관계 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것.

슈퍼타입 서브타입 논리 모델을 실제 물리 모델인 테이블로 구현할 때는 3가지 방법을 선택

  • 각각의 테이블로 변환 : 모두 테이블로 만들고 조회할 때 조인을 사용. JPA에서는 조인 전략 이라 한다.
  • 통합 테이블로 변환 : 테이블을 하나만 사용해서 통합한다. JPA에서는 단일 테이블 전략 이라 한다.
  • 서브타입 테이블로 변환 : 서브 타입마다 하나의 테이블을 만든다. JPA에서는 구현 클래스마다 테이블 전략 이라 한다.

조인 전략

엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략
객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없다. 따라서 타입을 구분하는 컬럼을 추가 해야 한다.

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    @Entity
    @Inheritance(strategy = InheritanceType.JOINED) 
    @DiscriminatorColumn(name = "DTYPE") // 구분 컬럼 지정
    public abstract class Item {
    
        @Id @GeneratedValue
        @Column(name = "ITEM_ID")
        private Long id;
    
        private String name;        //이름
        private int price;          //가격
        ...
    }

    @Entity
    @DiscriminatorValue("B") // 구분 컬럼에 입력할 값을 지정
    @PrimaryKeyJoinColumn(name = "BOOK_ID") // ID 재정의
    public class Book extends Item {
    
        private String author;
        private String isbn;
        ...
    }

장점

  • 테이블이 정규화
  • 외래 키 참조 무결성 제약조건을 활용
  • 저장공간을 효율적 사용

단점

  • 조인을 많이 사용, 성능 저하
  • 쿼리가 복잡
  • 데이터 등록 시, INSERT SQL을 두 번 실행

특징

  • 몇몇 구현체는 구분 컬럼 없이도 동작한다.

단일 테이블 전략

테이블을 하나만 사용
구분 컬럼(DTYPE)으로 어떤 자식 데이터가 저장되었는지 구분
조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다.

img

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
    @Entity
    @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
    @DiscriminatorColumn(name = "DTYPE")
    public abstract class Item {
    
        @Id @GeneratedValue
        @Column(name = "ITEM_ID")
        private Long id;
    
        private String name;        //이름
        private int price;          //가격
        ...
    }

    @Entity
    @DiscriminatorValue("A")
    public class Album extends Item { ... }
    
    @Entity
    @DiscriminatorValue("B")
    public class Book extends Item { ... }
    
    @Entity
    @DiscriminatorValue("M")
    public class Movie extends Item { ... }

장점

  • 조회 성능이 빠르다
  • 쿼리가 단순

단점

  • 자식 엔티티가 매핑한 컬럼은 모두 null을 허용
  • 테이블이 커질 수 있어서 상황에 따라서는 조회 성능이 오히려 느려질 수 있다.

특징

  • 구분 컬럼을 꼭 사용 @DiscriminatorColumn을 꼭 설정
  • @DiscriminatorValue을 지정하지 않으면 기본으로 엔티티 이름을 사용

구현 클래스마다 테이블 전략

자식 엔티티마다 테이블을 만든다. 그리고 자식 테이블 각각에 필요한 컬럼이 모두 있다.
데이터베이스 설계자와 ORM 전문가 둘 다 추천하지 않는 전략

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    @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;          //가격
        ...
    }

    @Entity
    public class Album extends Item { ... }
    
    @Entity
    public class Movie extends Item { ... }
    
    @Entity
    public class Book extends Item { ... }

장점

  • 서브 타입을 구분해서 처리할 때 효과적
  • not null 제약조건을 사용할 수 있다.

단점

  • 여러 자식 테이블을 함께 조회할 때 성능이 느리다.
  • 자식 테이블을 통합해서 쿼리하기 어렵다.

특징

  • 구분 컬럼을 사용하지 않는다.

@MappedSuperclass

지금까지 상속 관계 매핑은 부모 클래스와 자식 클래스 모두 데이터베이스 테이블과 매핑
부모 클래스는 테이블과 매핑하지 않고
부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공하고 싶으면 @MappedSuperclass를 사용하면 된다.

실제 테이블과는 매핑되지 않지만, 매핑 정보를 상속할 목적으로만 사용
즉, 테이블과는 관계가 없고 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모아주는 역활을 할 뿐이다.

부모로부터 물려받은 매핑 정보를 재정의 하려면 @AttributeOverrides나 @AttributeOverride를 사용
연관관계를 재정의 하려면 @AssociationOverrides나 @AssociationOverride를 사용

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    @MappedSuperclass
    public abstract class BaseEntity {
       @Id @GeneratedValue
       private Long id;
       private String name;
       ...
    }
    
    @Entity
    public class Member extends BaseEntity {
       //ID 상속
       //NAME 상속
       private String email;
       ...
    }
    
    @Entity
    public class Seller extends BaseEntity {
       //ID 상속
       //NAME 상속
       private String shopName;
       ...
    }
  • 매핑 정보를 상속하기 위해 사용
  • @MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용할 수 없다.
  • BaseEntity는 직접 생성해서 사용할 일이 거의 없으므로 추상 클래스로 만드는 것을 권장

복합 키와 식별 관계 매핑

식별 관계 vs 비식별 관계

데이터베이스 테이블 사이에 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별 관계와 비식별 관계로 구분

식별 관계

부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용 하는 관계

img

비식별 관계

부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계

필수적 비식별 관계

img

선택적 비실별 관계

img

  • 필수적 비식별 관계 : 외래 키에 NULL을 허용하지 않는다.
  • 선택적 비식별 관계 : 외래 키에 NULL을 허용한다.

최근에는 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 추세

복합 키: 비식별 관계 매핑

식벽자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 그곳에 equals와 hashCode를 구현해야 한다.
JPA는 복합 키를 지원하기 위해 @IdClass와 @EmbeddedId 2가지 방법을 제공
@IdClass는 관계형 데이터베이스에 가까운 방법
@EmbeddedId는 좀 더 객체지향에 가까운 방법

@IdClass

데이터베이스에 가까운 방법

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    @Entity
    @IdClass (ParentId.class)
    public class Parent {
       @Id
       @Column(name = "PARENT_ID1")
       private String id1; // ParentId.id1과 연결
    
       @Id
       @Column(name = "PARENT_ID2")
       private String id2; // ParentId.id2와 연결
    
       private String name;
       ...
    }
    
    public class ParentId implements Serializable {
       private String id1; // Parent.id1 매핑
       private String id2; // Parent.id2 매핑
       ...
    }

@IdClass를 사용할 때 식별자 클래스는 다음 조건을 만족해야 한다.

  • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
  • Serializable 인터페이스를 구현
  • equals, hashCode를 구현
  • 식별자 클래스는 public이어야 한다.

    Parent parent = new Parent(); parent.setId1(“myId1”); parent.setId2(“myId2”); parent.setName(“parentName”); em.persist(parent);

위의 코드를 보면 식별자 클래스 ParentId가 보이지 않는다.
em.persist를 호출하면 영속성 컨텍스트에 엔티티를 등록하기 직전에 내부에서 Parent.id1, Parent.id2 값을 사용해서 식별자 클래스인 ParentId를 생성하고 영속성 컨텍스트의 키로 사용

1
2
    ParentId parentId = new ParentId("myId1", "myId2");
    Parent parent = em.find(Parent.class, parentId);

조회 코드를 보면 식별자 클래스인 ParentId를 사용해서 엔티티를 조회한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    @Entity
    public class Child {
       @Id
       private String id;
    
       @ManyToOne
       @JoinColumns({
          @JoinColumn(name = "PARENT_ID1",
             referenceColumnName = "PARENT_ID1"),
          @JoinColumn(name = "PARENT_ID2",
             referenceColumnName = "PARENT_ID2")
       })
       private Parent parent;
    }

@EmbeddedId

좀 더 객체지향적인 방법

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    @Entity
    public class Parent {
       @EmbeddedId
       private ParentId id;
    
       private String name;
       ...
    }
    
    @Embeddable
    public class ParentId implements Serializable {
       @Column(name = "PARENT_ID1")
       private String id1;
       @Column(name = "PARENT_ID2")
       private String id2;
    
       // equals and hashCode 구현
       ...
    }

식별자 클래스를 기본키로 직접 매핑한다.
@EmbebbedId를 적용한 식별자 클래스는 다음 조건을 만족해야 한다.

  • @Embeddable 붙여주어야 한다.
  • Serializable 인터페이스를 구현
  • equals, hashCode를 구현
  • 식별자 클래스는 public이어야 한다.
1
2
3
4
5
6
7
8
9
10
    // 저장
    Parent parent = new Parent();
    ParentId parentId = new ParentId("myId1", "myId2");
    parent.setId(parentId);
    parent.setName("parentName");
    em.persist(parent);
    
    // 조회
    ParentId parentId = new ParentId("myId1", "myId2");
    Parent parent = em.find(Parent.class, parentId);

@IdClass vs @EmbeddabledId

각각 장단점이 있으므로 본인의 취향에 맞는 것을 일관성 있게 사용하면 된다.
@EmbeddabledId가 더 객체지향적이고 중복도 없어서 좋아보이긴 하지만 특정 상황에 JPQL이 조금 더 길어질 수 있다.

복합 키: 식별 관계 매핑

img

@IdClass와 식별관계

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
    // 부모
    @Entity
    public class Parent {
       @Id @Column(name = "PARENT_ID")
       private String id;
       private String name;
       ...
    }
    
    // 자식
    @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;
       ...
    }
    
    // 자식 ID
    public class ChildId implements Serializable {
       private String parent; // Child.parent 매핑
       private String childId; // Child.childId 매핑
    
       // equals, 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
    public class GrandChild implements Serializable {
       private ChildId child; // GrandChild.child 매핑
       private String id; // GrandChild.id 매핑
    
       // equals, hashCode
       ...
    }

@EmbeddabledId와 식별 관계

식별 관계를 구성할 때는 @MapsId를 사용해야 한다.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
    // 부모
    @Entity
    public class Parent {
       @Id @Column(name = "PARENT_ID")
       private String id;
       private String name;
       ...
    }
    
    @Entity
    public class Child {
       @EmbeddedId
       public ChildId id;
    
       @MapsId("parentId") // ChildId.parentId 매핑
       @ManyToOne
       @JoinColumn(name = "PARENT_ID")
       public Parent parent;
    
       private String name;
       ...
    }
    
    // 자식 ID
    @Embeddable
    public class ChildId implements Serializable {
       private String parentId; // @MapsId("parentId")로 매핑
    
       @Column(name = "CHILD_ID")
       private String id;
    
       // equals, hashCode
       ...
    }
    
    // 손자
    @Entity
    public class GrandChild {
       @EmbeddedId
       private GrandChildId id;
    
       @MapsId("childId") // GrandChildId.childId 매핑
       @ManyToOne
       @JoinColumns({
          @JoinColumn(name = "PARENT_ID"),
          @JoinColumn(name = "CHILD_ID")
       })
       private Child child;
    
       private String name;
       ...
    }
    
    // 손자 ID
    @Embeddable
    public class GrandChild implements Serializable {
       private ChildId childId; // @MapsId("childId")로 매핑
    
       @Column(name = "GRANDCHILD_ID")
       private String id;
    
       // equals, hashCode
       ...
    }

@MapsId는 외래키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻이다.

비식별 관계로 구현

img

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
32
33
34
35
36
    // 부모
    @Entity
    public class Parent {
       @Id @GeneratedValue
       @Column(name = "PARENT_ID")
       private Long id;
       private String name;
       ...
    }
    
    // 자식
    @Entity
    public class Child {
       @Id @GeneratedValue
       @Column(name = "CHILD_ID")
       private Long id;
       private String name;
    
       @ManyToOne
       @JoinColumn(name = "PARENT_ID")
       private Parent parent;
    }
    
    // 손자
    @Entity
    public class GrandChild {
       @Id @GeneratedValue
       @Column(name = "GRANDCHILD_ID")
       private Long id;
       private String name;
    
       @ManyToOne
       @JoinColumn(name = "CHILD_ID")
       private Child child;
       ...
    }

일대일 식별 관계

img

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
    // 부모
    @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 //BoardDetail.boardId 매핑
       @OneToOne
       @JoinColumn(name = "BOARD_ID")
       private Board board;
    
       private String content;
       ...
    }

식별, 비식별 관계의 장단점

  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어난다.
  • 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다.
  • 식별 관계를 사용할 때 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많다.
    비식별 관계는 키본 키를 비즈니스와 전혀 관계없는 대리 키를 주로 사용
    식별 관계의 자연 키 컬럼들이 자식에 손자까지 전파되면 변경하기 힘들다.
  • 식별 관계는 비식별 관계보다 테이블 구조가 유연하지 못 하다.
    이와 같은 이유로 객체 관계 매핑 관점에서 비식별 관계를 선호
  • 일대일 관계를 제외하고 식별 관계는 2개 이상의 컬럼을 묶은 복합 기본키를 사용한다.
  • 비식별 관계는 주로 대리키를 사용하여 @GeneratedValue처럼 대리 키를 생성하기 위한 편리한 방법을 제공
  • 식별 관계의 장점으로 기본 키 인덱스를 활용하기 좋고, 특정 상황에 조인 없이 하위 테이블만으로 검색을 완료할 수 있다.

비식별관계를 사용하고 기본 키는 Long 타입의 대리 키를 사용하는 것을 추천
추가적으로 선택적 비식별 관계보다는 필수적 비식별 관계를 사용하는 것이 좋다.

조인 테이블

테이블 연관관계를 설계하는 방법은 크게 2가지다.

  • 조인 컬럼 사용
  • 조인 테이블 사용

조인 컬럼을 사용하고 필요하다고 판단되면 조인 테이블을 사용하자

일대일 조인 테이블

img

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
    // 부모
    @Entity
    public class Parent {
       @Id @GeneratedValue
       @Column(name = "PARENT_ID")
       private Long 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 Long id;
       private String name;
    
       @OneToOne(mappedBy="child")
       private Parent parent;
       ...
    }

일대다 조인 테이블

img

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
    // 부모
    @Entity
    public class Parent {
       @Id @GeneratedValue
       @Column(name = "PARENT_ID")
       private Long id;
       private String name;
    
       @OneToMany
       @JoinTable(name = "PARENT_CHILD",
                  joinColumns = @JoinColumn(name = "PARENT_ID"),
                  inverseJoinColumns = @JoinColumn(name = "CHILD_ID"))
       private List<Child> child = new ArrayList<Child>();
       ...
    }
    
    // 자식
    @Entity
    public class Child {
       @Id @GeneratedValue
       @Column(name = "CHILD_ID")
       private Long id;
       private String name;
       ...
    }

다대일 조인 테이블

다대일은 일대다에서 방향만 반대

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
    // 부모
    @Entity
    public class Parent {
       @Id @GeneratedValue
       @Column(name = "PARENT_ID")
       private Long id;
       private String name;
    
       @OneToMany(mappedBy = "parent")
       private List<Child> child = new ArrayList<Child>();
       ...
    }
    
    // 자식
    @Entity
    public class Child {
       @Id @GeneratedValue
       @Column(name = "CHILD_ID")
       private Long id;
       private String name;
    
       @ManyToOne(optional = false)
       @JoinTable(name = "PARENT_CHILD",
                  joinColumns = @JoinColumn(name = "CHILD_ID"),
                  inverseJoinColumns = @JoinColumn(name = "PARENT_ID"))
       private Parent parent;
       ...
    }

다대다 조인 테이블

img

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
    // 부모
    @Entity
    public class Parent {
       @Id @GeneratedValue
       @Column(name = "PARENT_ID")
       private Long id;
       private String name;
    
       @ManyToMany
       @JoinTable(name = "PARENT_CHILD")
                  joinColumns = @JoinColumn(name = "PARENT_ID"),
                  inverseJoinColumns = @JoinColumn(name = "CHILD_ID"))
       private List<Child> child = new ArrayList<Child>();
       ...
    }
    
    // 자식
    @Entity
    public class Child {
       @Id @GeneratedValue
       @Column(name = "CHILD_ID")
       private Long id;
       private String name;
       ...
    }

엔티티 하나에 여러 테이블 매핑

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    @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;
       ...
    }
  • @SecondaryTable.name: 매핑할 다른 테이블의 이름
  • @SecondaryTable.pkJoinColumns: 매핑할 다른 테이블의 기본 키 컬럼 속성
  • content 필드는 @Column(table = “BOARDDETAIL“)을 사용해 BOARDDETAIL 테이블의 컬럼에 매핑
  • 더 많은 테이블에 매핑하려면 @SecondaryTables를 사용
1
2
3
4
        @SecondaryTables({
           @SecondaryTable(name="BOARD_DETAIL"),
           @SecondaryTable(name="BOARD_FILE")
        })
This post is licensed under CC BY 4.0 by the author.

자바 ORM 표준 JPA 프로그래밍 - 다양한 연관관계 매핑

자바 ORM 표준 JPA 프로그래밍 - 프록시와 연관관계 관리