• 상속 관계 매핑: 객체의 상속 관계를 db에 어떻게 매핑하는지 다룬다.
  • @MappedSuperclass: 등록일, 수정일 같이 여러 엔티티에서 공통으로 사용하는 매핑 정보만 상속받고 싶으면 이 기능을 사용.
  • 복합 키와 식별 관계 매핑: db의 식별자가 하나 이상일 때 매핑하는 방법. 식별관계와 비식별 관계에 대해서다룸.
  • 조인 테이블: 테이블은 외래 키 하나로 연관관계 맺을 수 있지만 연관관계를 관리하는 연결 테이블을 두는 방법도 있다. 여기서는 이 연결 테이블을 매핑하는 방법을 다룬다.
  • 엔티티 하나에 여러 테이블 매핑하기: 보통 엔티티 하나에 테이블 하나를 매핑하지만 엔티티 하나에 여러 테이블을 매핑하는 방법도 있다. 여기서는 이 매핑 방법을 다룬다.

7.1 상속 관계 매핑

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

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

7.1.1 조인 전략

조인 전략은 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본키+외래 키로 사용하는 전략이다. 따라서 조회할 때 조인을 자주 사용한다.

이 전략을 사용할 떄 주의점은 객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없다. 따라서 타입을 구분하는 칼러 DTYPE을 추가해야 함.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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("A")
public class Album extends Item {
    private String artist;
}
 
@Entity
@DiscriminatorValue("M")
public class Movie extends Item{
    private String director;
    private String actor;
}
cs

 

 

 

 

 

 

 

 

 

 

 

1. @Inheritance(strategy = InheritanceType.JOINED): 상속 매핑은 부모 클래스인 @Inheritance를 사용해야 한다. 그리고 매핑 전략을 지정해야 하는데 조인전략을 사용하므로 InhertanceType.JOINED를 사용.

 

2.@DiscriminatorColumn(name="DTYPE"): 부모 클래스에 구분 칼럼을 지정한다. 이 컬럼으로 저장된 자식 테이블을 구분할 수 있다. 기본값이 DTYPE이므로 @DiscriminatorColumn으로 줄여 사용해도 된다.

 

3.@DiscriminatorValue("M"):엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다. 만약 영화엔티티를 저장하면 구분 컬럼인 DTYPE에 값 M이 저장된다.

 

기본값으로 자식 테이블은 부모 테이블의 ID컬럼명을 그대로 사용하는데 변경하고 싶으면

@PrimaryKeyJoinColumn(name="BOOK_ID")이렇게 재정의하면 된다.

 

장점

  • 테이블이 정규화 된다.
  • 외래 키 참조 무결성 제약조건을 활용할 수 있다.
  • 저장공간을 효율적으로 사용.

단점

  • 조회할 때 조인이 많이 사용되므로 성능이 저하
  • 조회 쿼리가 복잡하다.
  • 데이터를 등록할 INSERT SQL을 두 번 실행한다.

7.1.2 단일 테이블 전략

단일 테이블 전략은 이름 그대로 테이블을 하나만 사용한다. 그리고 구분 컬럼으로 어떤 자식 데이터가 저장되었는지 구분한다. 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다.

 

이 전략을 사용할 때 주의점은 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다는 점이다.

Book엔티티를 저장하면 ITEM ㅔㅌ이블의 AUTHOR,ISBN컬럼만 사용하고 다른 엔티티와 매핑된 ARTIST,DIRECTOR,ACTOR  컬럼은 사용하지 않으므로 null이 입력되기 때문.

 

1
2
3
4
5
6
7
8
9
10
11
12
@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;
    ...
}
 
cs

 

장점

  • 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다.
  • 조회 쿼리가 단순하다.

단점

  • 자식 엔티티가 매핑한 칼럼은 모두 null을 허용해야한다.
  • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 그러므로 상황에 따라서 조회 성능이 오히려 느릴수있다.

특징

  • 구분 컬럼을 꼭 사용해야 한다. 따라서 @DiscriminatorColumn을 꼭 설정해야 한다.
  • @DiscriminatorValue를 지정하지 않으면 기본으로 엔티티 이름을 사용한다. (ex Movie,Album,Book)

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

구현 클래스마다 테이블 전략은 자식 엔티티마다 테이블을 만든다. 그리고 자식 테이블 각각에 필요한 컬럼이 모두 있다.

 

 

1
2
3
4
5
6
7
8
9
10
11
@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;
    ...
}
 
cs

일반적으로 추천하지 않는 전략.

 

장점

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

단점

  • 여러 자식 테이블을 함께 조회할 때 성능이 느리다. (SQL에 UNION을 사용해야 한다)
  • 자식 테이블을 통합해서 쿼리하기 어렵다.

특징

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

 

이 전략은 db설계자와 orm 전문가 둘 다 추천하지 않는 전략이다. 조인이나 단일 테이블 전략을 고려하자.

 

7.2 @MappedSuperclass

지금까지 학습한 상속 관계 매핑은 부모 클래스와 자식 클래스를 모두 db테이블과 매핑했다. 부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고 싶으면 

@MappedSuperclass를 사용하면 된다.

 

@MappedSuperclass는 비유 하자면 추상 클래스와 비슷한데 @Entity는 실제 테이블과 매핑되지만 @MappedSuperclass는 실제 테이블과 매핑되지 안흔다.

이것은 단순히 매핑 정보를 상속할 목적으로만 사용된다. 

예제를 통해 @MappedSuperclass를 알아보자. 

 

 

 

 

 

 

 

 

 

 

 

 

회원(Member)과 판매자(Seller)는 서로 관계가 없는 테이블과 엔티티다. 테이블은 그대로 두고 객체 모델의 id,name 두 공통 속성을 부모 클래스로 모으고 객체 상속관계로 만들어보자.

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

 

BaseEntity에는 객체들이 주로 사용하는 공통 매핑 정보를 정의했다. 

그리고 자식 엔티티들은 상속을 통해 BaseEntity의 매핑 정보를 물려받았다.

여기서 BaseEntity는 테이블과 매핑할 필요가 없고 자식 엔티티에게 공통으로 사용되는

매핑 정보만 제공하면 된다. 따라서 @MappedSuperclass를 사용했다.

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

 

  • 테이블과 매핑되지 않고 자식 클래스에 엔티티 매핑 정보 상속하기위해 사용.
  • @MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용X
  • 이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장.

정리하자면 @MappedSuperclass는 테이블과는 관계가 없고 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모아주는 역할을 할 뿐이다. ORM에서 이야기하는 진정한 상속 매핑은 이전에 학습한 객체 상속을 db의 슈퍼타입 서브타입과 매핑하는 것이다. 

 

@MappedSuperclass를 사용하면 등록일자,수정일자,등록자,수정자 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있다. 

 

7.3 복합 키와 식별 관계 매핑

7.3.1 식별관계 vs 비식별 관계

db 테이블 사이에 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별 관계와 비식별 관계로 구분한다. 

 

식별 관계

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

 

 

 

 

 

 

비식별 관계

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

 

 



 

 

 

 

 

 

 

 

 

그림 7.9를 보면 PARENT 테이블 기본 키 PARENT_ID를 받아서 CHILD 테이블의 외래 키로만 사용한다.

 비식별 관계는 외래 키에 NULL을 허용하는지에 따라 필수적 비식별 관계와 선택적 비식별 관계로 나눈다.

 

  • 필수적 비식별 관계 : 외래 키에 NULL을 허용하지 않는다. 연관관계를 필수적으로 맺어야 한다.
  • 선택적 비식별 관계 : 외래 키에 NULL을 허용한다. 연관관계를 맺을지 말지 선택할 수 있다.

db 테이블을 설계할 때 식별 관계나 비식별 관계 중 하나를 선택해야 한다. 최근에는 비식별 관계를 주로 사용함.

JPA는 식별 비식별 모두 지원.

 

7.3.2 복합 키: 비식별 관계 매핑

기본 키를 구성하는 컬럼이 하나면 다음처럼 단순하게 매핑한다. 

@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는 관계형 db에 가까운 방법.

@EmbeddedId는 객체지향에 가까운 방법.

 

@IdClass

복합 키 테이블은 비식별 관계고 PARENT는 복합 기본 키를 사용한다. 참고로 여기서 이야기하는 부모와 자식은 객체의 상속과는 무관하다. 단지 테이블의 키를 내려받은 것을 강조하려고 이름을 이렇게 지었다. 

 

 

 

 

 

 

 

 

 

 

PARENT 테이블을 보면 기본 키를 PARENT_ID1,PARENT_ID2로 묶은 복합 키로 구성했다. 

따라서 복합 키  매핑위한 식별자 클래스를 만들어야 한다. 

 

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
 
@Entity
@IdClass(ParentId.class)
public class Parent {
    @Id
    @Column(name = "PARENT_ID1")
    private String id1;
 
    @Id
    @Column(name = "PARENT_ID2")
    private String id2;
 
    private String name;
    ...
}
 
public class ParentId implements Serializable {
    private String id1;
    private String id2;
 
    public ParentId(){
 
    }
 
    public ParentId(String id1,String id2){
        this.id1=id1;
        this.id2=id2;
    }
    @Override
    public boolean equals(Object o){...}
    
    @Override
    public int hashCode(){...}
    
}
cs

 

PARENT 테이블을 매핑한 Parent 클래스 :기본 키 컬럼을 @Id로 매핑하고 @IdClass를 사용해서 ParentId 클래스를 식별자 클래스로 지정했다.

 

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

  • 식별자 클래스 속석명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다. 예제의 Parent.id1과 ParentId.id 그리고 Parent.id2와 ParentId.id2가 같다.
  • 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를 생성하고 영속성 컨텍스트의 키로 사용한다.

-복합 키로 조회하는 코드

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
15
@Entity
public class Child {
    @Id
    private String id;
 
    @ManyToOne
    @JoinColumns({
            @JoinColumn(name = "PARENT_ID1",
                    referencedColumnName = "PARENT_ID1"),
            @JoinColumn(name = "PARENT_ID2",
                    referencedColumnName = "PARENT_ID2")
    })
    private Parent parent;

...
}
 
cs

 

부모 테이블의 기본 키 컬럼이 복합 키이므로 자식 테이블의 외래 키도 복합 키다. 따라서 외래 키 매핑 시 여러 컬럼을 매핑해야 하므로 @JoinColumns어노테이션을 사용하고 각각의 외래 키 컬럼을 @JoinColumn으로 매핑한다.

 @JoinColumn의 name속성과 referencedColumnName속성값이 같으면 referencedColumnName은 생략해도 된다.

 

@EmbeddedId

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Entity
@IdClass(ParentId.class)
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 구현
}
cs

@IdClass와는 다르게 @EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본 키를 직접 매핑한다.

 

@EmbeddedId를 적용한 식별자 클래스는 다음 조건을 만족해야 함.

  • @Embeddable 어노테이션을 붙여야함.
  • Serializable 인터페이스 구현해야함
  • equals,hashCode 구현해야함
  • 기본 생성자 있어야 함
  • 식별자 클래스는 public이어야 함.

@EmbeddedId를 사용하는 코드를 보자.

 

-엔티티 저장 코드

Parent parent= new Parent();

ParentId parentId= new ParentId("myId1","myId2");

parent.setId(parentId);

parent.setName("parentName");

em.persist(parent);

 

저장하는 코드를 보면 식별자 클래스 parentId를 직접 생성해서 사용한다.

-엔티티를 조회 코드

ParentId parentId= new ParentId("myId1","myId2");

Parent parent= em.find(Parent.class, parentId);

 

조회코드, 저장 코드 둘다 식별자 클래스 parentId를 직접 사용한다. 

 

복합 키와 equals(),hasCode()

복합 키는 equals() 와 hashCode()를 필수로 구현해야 한다.

 

1
2
3
4
5
6
7
8
9
ParentId id1=new parentId();
id1.setId("myId1");
id1.setId("myId2");
 
ParentId id2=new parentId();
id2.setId("myId1");
id2.setId("myId2");
 
id1.equals(id2) -> ?
cs

이것은 순수한 자바 코드다. id1과 id2 인스턴스 둘다 myId1,myId2라는 같은 값을 가지고 있지만 인스턴스는 다르다. 

마지막 id1.equals(id2)는 참일까 거짓일까?

 

equals()를 적절히 오버라이딩했다면 참이겠지만 equals()를 적절히 오버라이딩하지 않았다면 결과는 거짓이다. 

왜냐하면 자바의 모든 클래스는 기본으로 Object 클래스를 상속받는데 이 클래스가 제공하는 기본 equals()는 인스턴스 참조 값 비교인 == 비교를 하기 때문이다. 

 영속성 컨텍스트는 엔티티의 식별자를 키로 샤용해서 엔티티를 관리한다. 그리고 식별자를 비교할때 equals()와 hasCode()를 사용한다. 따라서 식별자 객체의 동등성(equals 비교)이 지켜지지 않으면 예상과 다른 엔티티가 조회되거나 엔티티를 찾을 수 없는 등 영속성 컨텍스트가 엔티티를 관리하는데 심각한 문제가 발생한다.

따라서 복합 키는 equals()와 hasCode()를 필수로 구현해야 한다.

식별자 클래스는 보통 equals()와 hasCode()를 구현할 때 모든 필드를 사용한다.

 

@IdClass vs @EmbeddedId

각각 장단점이 있으므로 취향에 맞게 사용.

@EmbeddedId가 @IdClass와 비교해서 더 객체지향적이고 중복도 업지만,

특정 상황에 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

 

7.3.3 복합 키: 식별관계 매핑

 

 

'자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글

객체지향 쿼리언어  (0) 2021.05.05
8장 프록시와 연관관계 관리  (0) 2021.05.05
6장  (0) 2021.05.02
5장  (0) 2021.05.01
4장  (0) 2021.05.01

+ Recent posts