[JPA 기본] 고급 매핑 👨‍💻

홍정완·2022년 11월 1일
0

JPA

목록 보기
34/38
post-thumbnail

상속관계 매핑


RDB에는 객체지향과 다르게 상속이라는 개념이 없다. 대신에 이와 비슷한 슈퍼 타입서브 타입 관계라는 모델링 기법을 이용한다. ORM에서의 상속 관계 매핑은 DB의 슈퍼/서브타입 관계를 매핑하는 것이다.



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



각각의 테이블로 변환

각각을 모두 테이블로 만들고 조회할 때 JOIN을 사용한다.
JPA에서는 이를 조인 전략이라고 말한다.


통합 테이블로 변환

테이블을 하나만 사용해서 통합한다.
JPA에서는 단일 테이블 전략이라고 한다.


서브 타입 테이블로 변환

서브 타입마다 하나의 테이블을 만든다.
JPA에서는 구현 클래스마다의 테이블 전략이라고 한다.



1. 조인 전략


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


단, 해당 전략을 사용할 때 주의할 점이 있는데 객체는 타입으로 구분할 수 있지만, 테이블은 타입의 개념이 없다. 따라서 타입을 구분하는 구분 컬럼을 추가해야 한다.



Item

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {

    @Id
    @GeneratedValue 
    private Long id;

    private String name;

    private int price;

	... get, set
}

@Inheritance(strategy = InheritanceType.JOINED)

상속 매핑은 부모 클래스에 @Inheritance 어노테이션을 붙여야 한다. 그리고 여러 매핑 전략이 있는데 InheritanceType.JOINED조인 전략이다.

@DiscriminatorColumn(name = "DTYPE")

부모 클래스에 구분 컬럼을 지정한다. 기본값은 DTYPE이지만, name=이름을 이용해서 컬럼 이름을 바꿀 수 없다. 이외의 나머지 속성들의 종류는 @Column의 속성들과 동일하다.



Book

@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID") // id 값 변경 
public class Book extends Item {

    private String author;
   
    private String isbn;

	... get, set
}

@DiscriminatorValue("이름")

엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다. 만약 Book 엔티티를 저장하면 구분 컬럼인 DTYPE"B"이 저장된다.

기본값으로 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용한다. 그러나 만약 자식 테이블의 기본 키 컬럼명을 변경하고 싶으면 @PrimaryKeyJoinColumn을 사용하면 된다.



장점

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

단점

  • 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.
  • 조회 쿼리가 복잡하다.
  • 데이터를 등록할 때 INSERT SQL두 번 실행한다. (Item / 자식 한 번씩)

JPA 표준 명세는 구분 컬럼을 사용하도록 권장하지만, 하이버네이트를 포함한 몇몇 구현체는 구분 컬럼 없이도 잘 동작한다. 조인 전략이 이에 해당된다.



2. 단일 테이블 전략 (default)


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

이 전략을 사용할 때 주의점은 자식 엔티티를 매핑 한 컬럼은 모두 null을 허용해야 한다. 왜냐하면 단일 테이블 전략이기에 연관된 클래스의 필드들도 컬럼이 되기 때문이다.



Item

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private int price;

	... get, set
}

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)

상속 매핑은 부모 클래스에 @Inheritance 어노테이션을 붙여야 한다. 그리고 여러 매핑 전략이 있는데 InheritanceType.SINGLE_TABLE단일 테이블 전략이다. 참고로, 해당 어노테이션을 생략해도 기본적으로 단일 테이블 전략이 수행된다.

@DiscriminatorColumn(name = "DTYPE")

부모 클래스에 구분 컬럼을 지정한다. 기본값은 DTYPE이지만, name=이름을 이용해서 컬럼 이름을 바꿀 수 없다. 이외의 나머지 속성들의 종류는 @Column의 속성들과 동일하다. 참고로, SINGLE_TABLE은 구분 컬럼이 필수적이다. 그렇기에 해당 어노테이션이 없어도 이를 자동으로 호출하여 사용한다.



Book

@Entity
@DiscriminatorValue("B")
public class Book extends Item {

    private String author;
    
    private String isbn;

	...get, set
}



장점

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

단점

  • 자식 엔티티가 매핑 한 컬럼은 모두 null 허용해야 한다.
  • 단일 테이블에 모든 것을 저장하기에 테이블이 커질 수 있고 상황에 따라서 조회 성능이 더 느려질 수 있다. (임계점을 넘을 정도의 상황은 거의 오지 않는다.)



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


이 전략은 데이터베이스 설계자와 ORM 전문가 둘 다 추천 ❌


장점

  • 서브 타입을 명확하게 구분해서 처리할 때 효과적
  • not null 제약조건 사용 가능

단점

  • 여러 자식 테이블을 함께 조회할 때 성능이 느림 (UNION SQL 필요)
  • 자식 테이블을 통합해서 쿼리 하기 어려움



@MappedSuperclass


  • 공통 매핑 정보가 필요할 때 사용 (id, name)
    • ex : 모든 테이블에 생성일, 수정일을 등록해야 하는 경우 (createdAt, updatedAt)


  • 상속관계 매핑이 아니다.
  • 엔티티도 아니고, 테이블과 매핑되지도 않는다.
  • 부모 클래스를 상속받는 자식 클래스에 매핑 정보만 제공
  • 부모 타입으로 조회, 검색 불가
  • 직접 생성해서 사용할 일이 없으므로 추상 클래스 추천

@Entity 클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속 가능



@MappedSuperclass는 단순히 하위 클래스에게 매핑 정보만 넘겨주는 어노테이션이다.
즉, 단순히 매핑 정보를 상속할 목적으로만 사용된다.


BaseEntity

@MappedSuperclass
public class BaseEntity {

    private String createdBy;
    
    private LocalDateTime createDate;
    
    private String lestModifiedBy;
    
    private LocalDateTime lastModifiedDate;

}

BaseEntity에는 주로 사용하는 공통 매핑 정보를 정의한다. @MappedSuperclass에는 따로 속성값은 없다.


Team

@Entity
public class Team extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "TEAM_ID")
    private Long id;

    @Column(name = "TEAM_NAME")
    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    ... get, set
}

매핑 정보를 물려받을 클래스는 extends를 통해 @MappedSuperclass를 정의한 클래스를 상속받는다.

만약, 부모로부터 물려받은 매핑 정보를 재정의하고 싶다면 @AttributeOverrides@AttributeOverride를 사용하면 된다.


단일, 재정의

@Entity
@AttributeOverride(name = "createdBy", column = @Column(name = "TEAM_CREATED_BY"))
public class Team extends BaseEntity { ... }

다중 재정의

@Entity
@AttributeOverrides({
    @AttributeOverride(name = "createdBy", column = @Column(name = "TEAM_CREATED_BY")),
    @AttributeOverride(name = "createDate", column = @Column(name = "TEAM_CREATE_DATE"))
})
public class Team extends BaseEntity { ... }
profile
습관이 전부다.

0개의 댓글