스터디/JPA 스터디

[JPA] 고급 매핑

제이온 (Jayon) 2021. 12. 15.

jpa-study에서 스터디를 진행하고 있습니다.

상속 관계 매핑

조인 전략

Untitled

 

조인 전략은 위 그림과 같이 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다. 이 전략을 사용할 때 주의할 점은 객체는 타입으로 구분할 수 있지만, 테이블은 타입의 개념이 없으므로 DTYPE 컬럼과 같이 타입을 구분하는 컬럼이 꼭 있어야 한다.

 

@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;

}

@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID")
public class Book extends Item {

    private String author;
    private String isbn;

}

 

  •  @inheritance
    • 상속 매핑은 부모 클래스에 @Inheritance를 사용해야 한다. 그리고 매핑 전략을 지정하기 위해 InheritanceType.JOINED를 사용하였다.
  • @DiscriminatorColumn
    • 부모 클래스에 구분 컬럼을 지정한다. 이 컬럼으로 저장된 자식 테이블을 구분할 수 있으며, 기본 값은 "DTYPE"이다.
  • @DiscriminatorValue
    • 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다. 기본값은 엔티티의 이름이다.
  • @PrimaryKeyJoinColumn
    • name 속성을 통해 자식 테이블의 기본 키 컬럼명을 지정한다. 기본값은 부모 테이블의 ID 컬럼명이다.

 

장점

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

단점

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

 

단일 테이블 전략

Untitled

 

단일 테이블 전략은 위와 같이 테이블 하나에 데이터를 다 집어 넣는 전략이다. 구분 컬럼을 통해서 어떤 자식 데이터가 저장되었는지 구분하며, 조회할 때 조인을 사용하지 않는다. 이 전략을 사용할 때 주의할 점은 자식 엔티티가 매핑한 컬럼은 모두 NULL을 허용해야 한다.

 

@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 {

    private String artist;

}

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {

    private String director;
    private String actor;

}

@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID")
public class Book extends Item {

    private String author;
    private String isbn;

}

 

장점

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

단점

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

 

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

Untitled

 

구현 클래스마다 테이블 전략은 위 그림과 같이 자식 엔티티마다 테이블을 만들며, 자식 테이블 각각에 필요한 컬럼은 모두 있는 것을 알 수 있다.

 

@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 {

    private String artist;

}

public class Movie extends Item {

    private String director;
    private String actor;

}

@Entity
public class Book extends Item {

    private String author;
    private String isbn;

}

 

구분 컬럼을 사용하지 않는다는 특징이 있고, 일반적으로 해당 전략은 사용하지 않는다.

 

장점

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

단점

  • 여러 자식 테이블을 함께 조회할 때 성능이 느리다.
    • SQL에 UNION을 사용해야 한다.
    • 위 예제 코드에서 em.find(Item.class) 같은 것을 호출하는 순간 자식 테이블을 전부 다 조회해야 한다.
  • 자식 테이블을 통합해서 쿼리하기 어렵다.

 

@MappedSuperclass

부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공하고 싶으면 해당 어노테이션을 사용한다. @MappedSuperclass는 추상 클래스와 비슷한데, @Entity는 실제 테이블과 매핑되지만 @MappedSuperclass는 실제 테이블과 매핑되지는 않는다. 단순히 매핑 정보를 위로 뽑아서 재사용할 목적으로 사용한다.

 

 

@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의 특징

  • 테이블과 매핑되지 않고 자식 클레스에 엔티티의 매핑 정보를 상속하기 위해 사용한다.
  • 해당 어노테이션이 붙은 클래스는 엔티티가 아니다.
  • 추상 클래스로 만드는 것을 권장한다.

 

복합 키와 식별 관계 매핑

식별 관계 vs 비식별 관계

식별 관계

Untitled

 

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

 

비식별 관계

Untitled

 

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

 

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

 

최근에는 비식별 관계를 주로 사용하는 편이다.

 

복합 키 : 비식별 관계 매핑

@Entity
public class Hello {

    @Id
    private String id;

    @Id
    private string id2;

 

위와 같이 단순하게 컬럼 2개에 @Id 어노테이션을 이용하여 매핑하면 될 것 같지만 오류가 발생한다. 따라서 JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스가 필요하다.

 

@IdClass

Untitled

 

부모 테이블은 복합 키를 사용하고 있고, 자식 테이블은 부모 테이블의 복합 키 2개를 외래 키로 연결하고 있다. 부모 테이블의 복합 키를 매핑하기 위해 식별자 클래스를 다음과 같이 만들 수 있다.

 

@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;
    private String id2;

    // 생성자
    // equals, hashCode

}

 

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

 

  • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
    • 예제의 Parent.id1, Parent.id2와 ParentId.id1, ParentId.id2는 같다.
  • Serializable 인터페이스를 구현해야 한다.
  • equals, hashCode를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.

 

이제, 자식 클래스에서 외래 키로 연결해 보자.

 

@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;

}

 

참고로 예제처럼 @JoinColumn의 name 속성과 referencedColumnName 속성의 값이 같으면 후자는 생략해도 된다.

 

@EmbeddedId

@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, hashCode 구현

}

 

Parent 엔티티에 식별자 클래스를 직접 사용하고, @EmbeddedId 어노테이션을 적어주면 된다. @IdClass와 다르게 @EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본 키를 직접 매핑한다.

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

 

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

 

@IdClass vs @EmbeddedId

후자가 좀 더 객체지향적이어서 많이 쓰지만, 장단점이 있으므로 본인의 취향에 맞게 사용하면 된다.

 

주의 사항

복합 키에는 @GenerateValue를 사용할 수 없다.

 

복합 키: 식별 관계 매핑

Untitled

 

부모, 자식, 손자까지 기본 키를 전달하고 있다. 식별 관계에서 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야 한다.

 

@IdClass와 식별 관계

@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;

}

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;

}

public class GrandChildId implements Serializable {

    private ChildId child; // GrandChild.child 매핑
    private String id; // GrandChild.id 매핑

    // 생성자
    // equals, hashCode

}

 

@EmbeddedId와 식별 관계

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

 

@Entity
public class Parent {

    @Id
    @Column(name = "PARENT_ID")
    private String id;

    private String name;

}

@Entity
public class Child {

    @EmbeddedId
    private ChildId id;

    @MapsId("parentId") // ChildId.parentId 매핑
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;

    private String name;

}

@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;

}

@Embeddable
public class GrandChildId implements Serializable {

    private ChildId childid; // @MapsId("childId")로 매핑

    @Column(name = "GRANDCHILD_ID")
    private String id;

    // 생성자
    // equals, hashCode

}

 

아래와 같이 Child 엔티티의 parent 필드는 @Id 대신에 @MapsId를 사용하였다. @MapsId는 외래 키와 매핑한 연관 관게를 기본 키에도 매핑하겠다는 뜻이다. @MapsId의 속성 값은 @EmbeddedId를 사용한 식별자 클래스의 기본 키 필드를 지정하면 된다.

 

    @MapsId("parentId") // ChildId.parentId 매핑
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;

 

비식별 관계로 구현

위에서 구현한 식별 관계 테이블을 아래와 같이 비식별 관계로 변경하자.

 

 

@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;

}

 

복합 키가 존재하지 않으므로 복합 키 클래스를 만들 필요도 없고, 코드 자체가 간결하다.

 

일대일 식별 관계

Untitled

 

일대일 식별 관계는 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용한다. 그래서 부모 테이블의 기본 키가 복합 키가 아니면 자식 테이블의 기본 키는 복합 키로 구성하지 않아도 된다.

 

@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;

}

 

BoardDetail처럼 식별자가 단순히 컬럼 하나면 @MapsId를 사용하고 속성 값은 비워두면 된다.

 

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

식별 관계

  • 장점
    • 기본 키 인덱스를 활용하기 좋다.
      • 상위 테이블의 기본 키 컬럼을 자식, 손자 테이블이 가지고 있으므로 특정 상황에 조인 없이 검색이 가능하다.
  • 단점
    • 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어난다.
      • 부모 테이블은 1개, 자식 테이블은 2개, 손자 테이블은 3개, ....과 같은 상황에서 조인할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
    • 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 사용한다.
      • 비즈니스 요구가 바뀌면 엄청난 코드 수정이 필요하다.
    • 테이블 구조가 유연하지 않다.
    • 일대일 관계를 제외하고 별도의 복합 키 클래스를 만들어야 한다.

 

비식별 관계는 위의 장단점을 바꾸어서 생각하면 된다. 결국 ORM 신규 프로젝트 진행 시 될 수 있으면 필수적 비식별 관계를 사용하고 기본 키는 Long 타입의 대리 키를 사용하는 것이 좋다.

 

조인 테이블

데이터베이스 테이블의 연관 관계를 설계하는 방법은 조인 컬럼 (외래 키)을 사용하거나 조인 테이블을 사용하는 방법이 있다.

 

조인 컬럼 사용

Untitled

 

위와 같이 회원과 사물함 사이의 선택적 비식별 관계로 만들면 외래 키에 NULL을 허용하므로 회원과 사물함을 조인할 때 외부 조인을 사용해야 한다. 실수로 내부 조인을 사용하면 사물함과 관계가 없는 멤버는 조회가 되지 않는다.

 

조인 테이블 사용

Untitled

 

연관 관계를 관리하는 조인 테이블을 추가하고 이 안에서 두 테이블의 외래 키를 활용한다. 조인 테이블은 그 자체로 테이블 하나를 추가해야 한다는 큰 단점이 있다. 따라서 기본은 조인 컬럼을 사용하고 필요하면 조인 테이블을 고민한다.

 

일대일 조인 테이블

일대일 관계를 만들려면 조인 테이블의 외래 키 컬럼 2개에 반드시 유니크 제약 조건을 걸어야 한다.

 

 

@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;

}

 

일대다 조인 테이블

일대다 관계를 만들려면 조인 테이블의 컬럼중 다(N)와 관련된 컬럼에 대해 유니크 제약 조건을 걸어야 한다.

 

 

@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> children = new ArrayList<>();

}

@Entity
public class Child {

    @Id
    @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;

    private String name;

}

 

다대일 조인 테이블

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

 

@Entity
public class Parent {

    @Id
    @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent")
    private List<Child> children = new ArrayList<>();

}

@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;

}

 

다대다 조인 테이블

다대다 관계를 만들려면 조인 테이블의 두 컬럼을 합해서 하나의 복합 유니크 제약 조건을 걸어야 한다. 사실 보통 두 테이블의 키가 기본 키이므로 유니크 제약 조건이 걸려 있긴하다. 위 예시들도 모두 마찬가지.

 

 

@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> children = new ArrayList<>();

}

@Entity
public class Child {

    @Id
    @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;

    private String name;

}

 

참고로 조인 테이블에 컬럼을 추가하고 싶다고 하면 @JoinTable 전략을 사용할 수 없다.

 

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

@SecondaryTable을 사용하면 한 엔티티에 여러 테이블을 매핑할 수 있다.

 

 

@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;

}

 

Board 엔티티는 @Table을 사용해서 BOARD 테이블과 매핑하고, @SecondaryTable을 사용해서 BOARD_DETAIL 테이블을 매핑할 수 있다.

 

  • @SecondaryTable.name
    • 매핑할 다른 테이블의 이름
  • @SecondaryTable.pkJoinColumns
    • 매핑할 다른 테이블의 기본 키 컬럼 속성

 

출처

김영한 - 자바 ORM 표준 JPA 프로그래밍

'스터디 > JPA 스터디' 카테고리의 다른 글

[JPA] 값 타입  (0) 2022.01.01
[JPA] 프록시와 연관 관계 관리  (0) 2021.12.25
[JPA] 다양한 연관 관계 매핑  (0) 2021.12.11
[JPA] 연관 관계 매핑 기초  (0) 2021.12.09
[JPA] 엔티티 매핑  (0) 2021.12.04

댓글

추천 글