본문 바로가기
Programming/JPA 프로그래밍

07. 고급 매핑

by JKROH 2023. 11. 13.
반응형
  • 이번 매핑 전략에서는 책의 예시를 그대로 사용한다. 내 프로젝트에 적용해보려 했는데, 적절히 적용할만한 부분을 찾지 못했다.
    • 2023.11.27 프로젝트에 적용했다! 링크에서 확인할 수 있다.
  • 모든 코드는 링크에서 확인할 수 있다.

슈퍼타입 서브타입 논리 모델과 객체 상속 모델

상속 관계 매핑

- 조인 전략

  • 조인 전략은 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 복합 키로 사용하는 전략이다.
    • 위의 예시에서는 ITEM, ALBUM, MOVIE, BOOK 테이블을 모두 만든다.
    • 부모 키를 받아 기본 키로 사용하기 때문에 조회 시 조인 방식을 사용한다.
  • 객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없다. 따라서 타입을 구분하는 칼럼을 추가해야 한다.
  • 부모 클래스에 @Inheritance(strategy= "") 애너테이션을 붙여 상속 매핑할 것임을 명시해야 한다. strategy에는 매핑 전략을 지정한다. 조인 전략을 사용할 때는 strategy = "InheritanceType.JOINED 를 사용한다.
  • 부모 클래스에 @DiscriminatorColumn(name = "") 애너테이션을 붙여 타입 구분 칼럼을 지정한다. 이 때, name의 기본 값은 DTYPE이다. 칼럼 명을 DTYPE으로 그대로 사용한다면 굳이 name = ""을 통해 이름을 지정해주지 않아도 된다.
  • 자식 클래스에 @DiscriminatorValue("")을 붙여 엔티티를 저장할 때 구분 칼럼에 입력할 값을 지정한다.
  • 자식 테이블은 기본적으로 부모 테이블의 id 칼럼 명을 그대로 가져와 사용한다. 만일, 기본 키 칼럼 명을 변경하고 싶다면 @PrimaryKeyJoinColumn(name = "")을 자식 클래스에 붙여 id 칼럼 명을 재정의할 수 있다.
  • 조인 전략의 가장 큰 장점은 테이블 정규화가 가능하다는 점이며 가장 큰 단점은 조회 시 조인이 많이 사용되고 등록시 INSERT SQL이 두 번 실행되는 등 성능 저하가 있을 수 있다는 점이다.

- 단일 테이블 전략

  • 부모 테이블 하나만 두고 구분 칼럼으로 어떤 자식 데이터가 저장되었는지 구분한다.
  • 이 때, 자식 엔티티가 매핑한 정보가 담길 칼럼은 모두 null을 허용해야 한다.
    • 예를 들어, 앨범 데이터를 저장하면 감독이나 배우, 작가의 정보는 없다. 따라서 해당 칼럼들은 null처리해야한다.
  • 부모 클래스에 @Inheritance(strategy= "InheritanceType.SINGLE_TABLE") 애너테이션을 붙여야 한다.
  • 단일 테이블 전략의 가장 큰 장점은 조인이 필요가 없어 조회 성능이 빠르다는 것이다. 그러나 null을 허용해야 하며 테이블 하나가 비대해질 수 있다.
  • 구분 칼럼을 반드시 사용해야한다. @DiscriminatorColumn(name ="") 애너테이션을 반드시 부모 클래스에 붙여야한다.
  • 자식 클래스에 @DiscriminatorValue("")을 붙이지 않으면 엔티티 이름을 기본으로 사용한다.

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

  • 각 자식 테이블에 필요한 칼럼을 모두 정의한다. 공통적으로 필요한 칼럼들이 각 테이블에 모두 정의되어있는 형태다.
    • 위의 두 전략은 자식 테이블에 이름과 가격 칼럼이 없지만, 해당 전략에는 자식 테이블이 각자 이름과 가격 칼럼을 갖는다.
  • 부모 클래스에 @Inheritance(strategy= "InheritanceType.TABLE_PER_CLASS") 애너테이션을 붙여야 한다.
  • 서브 타입을 구분해서 처리할 때 효과적이며 not null 제약조건을 사용할 수 있다는 장점이 있다. 그러나 여러 자식 테이블을 함께 조회할 때 성능이 느리며 자식 테이블을 통합해서 쿼리하기 어렵다.
  • 필요한 정보가 각 자식 테이블의 칼럼에 모두 정의되어 있으므로 구분 칼럼을 사용하지 않아도 된다.
  • 일반적으로 추천하지 않는 전략이다.

@MappedSuperClass

  • 상속 관계 매핑 전략은 부모 클래스 역시 하나의 테이블에 매핑한다. 그러나 부모 클래스는 테이블에 매핑하지 않으면서 자식 클래스에는 매핑 정보를 제공하고 싶다면 @MappedSuperClass를 사용한다.
    • 부모 클래스는 테이블을 만들고 싶지 않고, 단순히 매핑 정보를 상속하려는 목적으로만 사용한다.
  • 부모 클래스에는 @MappedSuperClass 애너테이션을 붙여야 한다.
  • 자식 클래스에는 별도의 애너테이션을 붙일 필요는 없다. 다만, 만일 자식 클래스에서 부모 클래스에서 제공하는 매핑 정보를 재정의하려면 @AttributeOverride 또는 @AttributeOverrides 를 사용한다. 연관관계를 재정의 하려면 @AssociationOverried 또는 @AssociationOverrieds 를 사용한다.
    • 예를 들어, 부모 테이블의 id를 자식 테이블에서 재정의하고 싶다면 자식 클래스에 @AttributeOverride(name = "id", column = @Column(name = "CHILD_ID") 애너테이션을 붙인다.
      • 여러 매핑 정보를 재정의 하려면 @AttributeOverries(@AttributeOverride( ...), @AttributeOverride(...))의 형태로 사용한다.
    • 예를 들어, 부모 테이블이 조부모 테이블 grandParent와 다대일 연관관계일 때 이를 재정의하고 싶다면 자식 클래스에 @AssociationOverride(name="grandParent", joinColumns=@JoinColumn(name="재정의할 FK 컬럼명")) 애너테이션을 붙인다.
      • 여러 연관관계 정보의 재정의는 매핑 정보 재정의와 같다.
      • 그런데 사실 연관관계 정보를 재정의할 일이 많을까? 그렇게 되면 애초에 부모 테이블을 잘못 추상화한 것은 아닐까? 고민해 볼 문제다.
  • 부모 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것이 권장된다.

복합 키와 식별 관계 매핑

- 식별 관계 vs 비식별 관계

  • 식별 관계와 비식별 관계의 차이는 부모 테이블의 키본 키를 자식 테이블의 외래 키뿐만 아니라 기본 키로도 사용할 것이냐(식별) 외래 키로만 사용할 것이냐(비식별)이다.
  • 비식별 관계는 외래 키에 null을 허용하는지에 따라 필수적 비식별 관계와 선택적 비식별 관계로 나뉜다.
    • 필수적 비식별 관계 : 외래 키에 null을 허용하지 않는다. 반드시 연관관계를 맺어야한다.
    • 선택적 비식별 관계 : 외래 키에 null을 허용한다. 연관관계를 선택적으로 맺는다.
  • 최근에는 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용한다.

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

  • JPA에서 복합 키를 사용하기 위해서는 @IdClass 또는 @EmbeddedId 두 가지 방법을 사용할 수 있다.
  • @IdClass
    • @IdClass는 관계형 데이터베이스에 가까운 방법이다. 
    • 이전 글에서 @IdClass의 특징을 다뤘다.
    • @IdClass를 사용해서 키를 여러 개 만든다고 해서 식별자 클래스의 객체 인스턴스를 만들거나, persist하지 않아도 된다.
    • 아래와 같은 형태로 사용하기만 해도 영속성 컨텍스트에 엔티티를 등록하기 직전에 내부에서 id값을 확인해 식별자 클래스를 생성하고 영속성 컨텍스트의 키로 사용한다.
    • 자식 클래스에서는 외래 키가 복합 키로 적용된다. 따라서 @JoinColumns 애너테이션과 @JoinColumn 애너테이션을 사용해 각각의 외래 키 칼럼을 매핑해야 한다.
      • @JoinColumns(@JoinColumn(name = "FATHER_ID", referencedColumnName = "PARENT_ID1", @JoinColumn(...))
      • 만일 @JoinColumn의 name속성과 referencedColumnName 속성의 값이 같으면 referenced는 생략해도 된다.
Parent parent = new Parent();
parent.setId1(1L);
parent.setId2(2L);
em.persist(parent);

/////////////////////////

ParentId parentId = new ParentId(1L, 2L);
Parent parent = em.find(Parent.class, parentId);
  • @EmbeddedId
    • @EmbeddedId는 객체지향에 가까운 방법이다.
    • 식별자 클래스에는 @EmbeddedId 애너테이션을 붙여야 한다.
    • Serializable 인터페이스를 구현해야 한다.
    • equals(), hashCode()를 구현해야 한다.
    • 기본 생성자가 있어야 한다.
    • 식별자 클래스는 public이어야 한다.
    • @Embeddable을 활용해 엔티티의 필드 값으로 지정되기 때문에, 엔티티를 만드는 과정에서 식별자 클래스의 객체 인스턴스도 함께 만들어주어야 한다. 
Parent parent = new Parent();
ParentId parentId = new ParentId(1L, 2L);
parent.setId(parentId);
em.persist(parent);

/////////////////////////

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

- 복합 키와 equals(), hashCode()

  • 복합 키는 equals()와 hashCode()를 필수로 구현해야 한다.
ParendId id1 = new ParentId(1L, 2L);
ParendId id2 = new ParentId(1L, 2L);

id1.equals(id2) -> ?
  • 위의 결과에서 id1.equals(id2)는 true일까 false일까? equals()와 hashCode()를 적절히 구현했다면 true지만, 그렇지 않다면 아닐 것이다.
  • 식별자 객체의 동등성이 지켜지지 않으면, 예상과 다른 엔티티가 조회되거나 엔티티를 찾을 수 없는 등의 문제가 발생할 수 있다.
  • 따라서 복합 키는 equals()hashCode()를 필수로 구현해야 한다.
    • 두 메서드를 구현할 때는 보통 모든 필드를 사용한다.

- @IdClass vs @EmbeddedId

  • 둘 모두 장단이 있기 때문에 취향에 맞게 하나를 골라 일관성 있게 사용하면 된다.
  • @EmbeddedId가 더 객체지향적이고 중복도 없어 좋아보이지만 특정 상황에서 JPQL이 조금 더 길어질 수 있다.

- 복합 키 : 식별 관계 매핑

  • 식별 관계는 기본 키와 외래 키를 같이 매핑해야 한다. 따라서 식별자 매핑인 @Id와 연관관계 매핑인 @ManyToOne을 함께 사용한다.
@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
    private Long id;
}

/////////////////////////

public class ChildId implements Serializable {

    private Long parent;
    private Long child;
    
    // equals(), hashCode()
    ...
}

- @EmbeddedId와 식별 관계

  • 식별 관계로 사용할 연관관계의 속성에 @MapsId 애너테이션을 사용해야한다.
  • @MapsId는 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻이다.
  • @MapsId의 속성 값은 @EmbeddedId를 사용한 식별자 클래스의 기본 키 필드를 지정하면 된다.
@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;
}

/////////////////////////

@Embeddable
public class ChildId implements Serializable {

    private Long parentId;
    
    @Column(name = "CHILD_ID")
    private Long id;
    
    // equals(), hashCode()
    ...
}

- 비식별 관계로 구현

@Entity
public class Parent {

    @Id
    private Long id;
}

/////////////////////////

@Entity
public class Child{

    @Id
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    private Parent parent;
}
  • 훨씬 단순하지 않은가? 매핑도 훨씬 쉽다. 복합 키가 없으니 복합 키 클래스를 만들지 않아도 된다.

- 일대일 식별 관계

@Entity
public class User {
    @Id
    @Column(name = "USER_ID")
    private Long id;
    
    @OneToOne(mappedBy = "user")
    private Wallet wallet;
}

/////////////////////////

@Entity
public class Wallet {

    @Id
    @Column(name = "WALLET_ID")
    private long userId;

    private long balance;

    @MapsId // Wallet.userId 매핑
    @OneToOne
    @JoinColumn(name = "USER_ID")
    private User user;
}
  • 일대일 식별 관계는 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용한다. 따라서 부모 테이블의 기본 키가 복합 키가 아니면 자식 테이블의 기본 키는 복합 키로 구성하지 않아도 된다.
  • Wallet처럼 식별자가 단순히 칼럼 하나면 @MapsId를 사용하고 속성 값은 비워두면 된다. @MapsId는 @Id를 사용해서 식별자로 지정한 userId와 매핑된다.
  • 자식 엔티티를 저장할 때 부모 엔티티를 설정주고 저장하기만 하면 된다.
    • 위의 예시에선 wallet.setUser(user); 를 진행하고 persist해주면 된다.

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

  • 데이터베이스 설계 관점이던, 객체 관계 매핑 관점이던 식별 관계보단 비식별 관계를 더 선호한다.
  • 데이터베이스 설계 관점
    • 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 칼럼이 점점 늘어난다. 결국 조인할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
    • 식별 관계는 2개 이상의 칼럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다.
    • 식별 관계를 사용할 때는 기본 키로 비즈니스 의미가 있는 자연 키 칼럼을 조합하는 경우가 많다. 문제는 비즈니스 요구사항은 시간이 지남에 따라 언젠가는 변한다는 점이다. 반면 비식별 관계의 기본 키는 비즈니스와 전혀 관계없는 대리 키를 주로 사용한다.
    • 식별 관계는 부모 테이블의 키본 키를 자식 테이블의 기본 키로 사용하므로 테이블 구조가 유연하지 못하다.
  • 객체 관계 매핑의 관점
    • 일대일 관계를 제외하면 식별 관계는 복합 키를 사용한다. 이를 위해 식별자 클래스를 만들어야 하는 등 칼럼이 하나인 키본 키를 매핑하는 것보다 많은 노력이 필요하다.
    • 비식별 관계는 기본 키로 주로 대리 키를 사용하는데, JPA는 @GenerateValue처럼 대리 키를 생성하기 위한 편리한 방법을 제공한다.
  • 식별 관계 역시 특정 상황의 조회 시 조인 테이블을 생성하지 않아도 된다는 장점이 있기는 하다. 따라서 기본적으로 비식별 관계를 사양하되, 적절히 식별 관계도 사용해보자.
  • 선택적 비식별 관계보다는 필수적 비식별 관계를 사용하는 것이 좋다. 필수적 관계는 not null로 항상 관계가 있다는 것을 보장하므로 내부 조인만 사용할 수 있기 때문이다.

조인 테이블

  • 데이터베이스 테이블 연관관계는 조인 칼럼을 사용하는 방법과 조인 테이블을 사용하는 두 가지 방법으로 설계할 수 있다.
  • 조인 칼럼을 사용하는 방법은 앞서 설명한 방법들이다.
  • 조인 테이블을 사용하는 방법은 연관관계를 관리하는 조인 테이블을 추가하고 여기서 두 테이블의 외래 키를 가지고 연관관계를 관리한다.
    • 다대다 관계를 매핑할 때를 생각하면 이해에 도움이 된다.
    • 조인 테이블 방법의 가장 큰 단점은 테이블이 하나 추가된다는 것이다. 관리하는 테이블이 늘어날 뿐더러 연관관계에 있는 두 테이블을 조회하려면 조인 테이블까지 함께 조인해서 사용해야 한다.
    • 따라서 기본은 조인 칼럼을 사용하고 필요하면 조인 테이블을 사용하자.

- 일대일 조인 테이블

  • 일대일 관계를 만들려면 조인 테이블의 외래 키 칼럼 각각에 총 2개의 유니크 제약 조건을 걸어야한다.
  • 매핑시 @JoinColumn대신 @JoinTable 애너테이션을 사용한다.
  • @JoinTable(name = "조인 테이블 이름", joinColumns = @JoinColumn(name = "현재 엔티티를 참조하는 외래 키"), inverseJoinColumns = @JoinColumn(name = "반대방향 엔티티를 참조하는 외래 키"))
    • A클래스에서 필드의 B클래스와 매핑한다고 했을 때 JoinColumn에는 A엔티티를 참조하는 B의 외래키이기 때문에 Aid가 들어간다. 마찬가지로 inverseJoinColumns에는 방대방향이기 때문에 Bid가 들어가면 된다.

- 일대다 조인 테이블

  • 일대다 관계를 만들기 위해선 조인 테이블의 칼럼 중 다와 관련된 칼럼에 유니크 제약 조건을 걸어야 한다.
  • 매핑 방법은 일대일 조인 테이블과 같다.

- 다대일 조인 테이블

  • 일대다와 같다.

- 다대다 조인 테이블

  • 조인 테이블의 두 칼럼을 합쳐서 하나의 복합 유니크 제약조건을 걸어야 한다.
  • 매핑 방법은 이전 글에서 설명했다.

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

  • 잘 사용하지는 않지만 @SecondaryTable을 사용하면 한 엔티티에 여러 테이블을 매핑할 수 있다.
@Entity
@Table(name = "BOARD")
@SecondaryTable(name = "BOARD_DETAIL",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "BOARD_DETAIL_ID"))
public class Board {
    
    @Id
    @Column(name = "BOARD_ID")
    private Long id;
    
    private String title;
    
    @Column(table = "BOARD_DETAIL")
    private String content;
}
  • @SecondaryTable.name : 매핑할 다른 테이블의 이름을 지정한다.
  • @SecondaryTable.pkJoinCokumns : 매핑할 다른 테이블의 기본 키 칼럼 속성을 지정한다.
  • 두 번째 테이블에 매핑할 정보는 @Column(table = "두 번째 테이블 이름")을 사용하여 매핑한다.
  • 굳이 @SecondaryTable을 상요해서 두 테이블을 하나의 엔티티에 저장하기보다는 테이블당 엔티티를 하나씩 만들자.
  • 이 방법은 항상 두 테이블을 조회하기 어렵기 때문에 최적화하기 어렵다.

 

반응형

'Programming > JPA 프로그래밍' 카테고리의 다른 글

08. 프록시와 연관관계 관리  (1) 2023.12.05
06. 다양한 연관관계 매핑  (1) 2023.11.06
05. 연관관계 매핑 기초  (0) 2023.10.12
03. 영속성 관리  (0) 2023.08.18
01. JPA 소개  (0) 2023.08.15

댓글