본문 바로가기

JPA

[JPA] 고급 매핑 (상속 관계 매핑, @MappedSuperclass)

상속 관계 매핑

 

위 사진과 같이 id, 이름(name), 가격(price)이라는 동일한 컬럼을 사용하는 엔티티가 여러 개있는 경우 상속 구조를 이용해 공통되는 컬럼을 추출하여 구현할 수 있다.

 

구현하는 방법으로는 총 3가지가 있으며 부모 클래스까지 직접 테이블과 매핑되는 3가지 방법 외에 마지막으로 부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하는 @MappedSuperclass 까지 알아보자.

조인 전략 (Joined Strategy)

 

조인 전략은 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다. 조회할 때 조인을 자주 사용하며 조인전략을 사용하기 위해서는 타입을 구분하는 컬럼을 추가해야 한다.

 

장점

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

단점

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

예제 코드

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

    @Id @GeneratedValue
    @Cloumn(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

상속 매핑을 할때 매핑 전략을 지정하는 어노테이션이다. 여기서는 조인 전략을 사용하므로 InheritanceType.JOINED를 사용했다.

 

@DiscriminatorColumn

부모 클래스에 구분 컬럼을 지정한다. 기본값이 DTYPE이므로 @DiscriminatorColumn으로 줄여서 사용해도 된다.

 

@DiscriminatorValue

엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다.

 

@PrimaryKeyJoinColumn

자식 테이블의 ID 컬럼명을 변경하고 싶을때 사용한다. 사용하지 않으면 부모 테이블의 ID 컬럼명을 그대로 사용한다. 

단일 테이블 전략 (Single-Table Strategy)

 

단일 테이블 전략은 이름 그대로 테이블을 하나만 사용한다. 조회할 때 조인을 사용하지 않으므로 조회속도가 가장 빠르고 사용하지 않는 컬럼들은 모두 null로 입력되야 하기 때문에 자식 엔티티가 매핑된 컬럼은 모두 null을 허용해야 한다.

 

장점

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

단점

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

예제 코드

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

    @Id @GeneratedValue
    @Cloumn(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")
public class Book extends Item {

    private String author;
    private STring isbn;
}

구현 클래스마다 테이블 전략 (Table-per-Concrete-Class Strategy)

 

구현 클래스마다 테이블 전략은 자식 엔티티마다 테이블을 만들고 자식 테이블 각각에 필요한 컬럼이 모두 있다. 일반적으로 추천하지 않는 전략이다.

 

장점

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

단점

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

예제 코드

구분 컬럼을 사용하지 않기 때문에 @DiscriminatorColumn과 @DiscriminatorValue 어노테이션을 사용하지 않는다.

@Entity
@Ingeritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Itam {

    @Id @GeneratedValue
    @Cloumn(name = "ITEM_ID")
    private Long id;
    
    private String name;
    private int price;
}


@Entity
public class Album extends Item {

    private String artist;
}


@Entity
public class Movie extends Item {

    private String director;
    private String actor;
}


@Entity
public class Book extends Item {

    private String author;
    private STring isbn;
}

@MappedSuperclass

@MappedSuperclass는 부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공하고 싶을 때 사용한다.

 

@MappedSuperclass는 추상 클래스와 비슷하다. @Entity는 실제 테이블과 매핑되지만 @MappedSuperclass는 실제 테이블과는 매핑되지 않는다.

@MappedSuperclass
public abstract class BaseEntity {

    @Id @GeneratedValue
    private Long id;
    private String name;
}

@Entity
@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID"))
public class Member extends BaseEntity {

    private String email;
}

@Entity
@AttributeOverrides({
    @AttributeOverride(name = "id", column = @Column(name = "SELLER_ID")),
    @AttributeOverride(name = "name", column = @Column(name = "SELLER_NAME"))
})
public class Seller extends BaseEntity {

    private String shopName;
}

BaseEntity에는 객체들이 주로 사용하는 공통 매핑 정보를 정의해놓고 자식 엔티티들이 상속을 통해 공통 매핑 정보를 상속받아 사용한다.

 

@AttributeOverrides

여러 매핑 정보를 재정의할 때 사용한다.

 

@AttributeOverride

매핑 정보를 재정의할 때 사용한다.

 

@AssociationOverride

여러 연관관계를 재정의할 때 사용한다.

 

@AssociationOverrides

연관관계를 재정의할 때 사용한다.

사용 예시

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

 

BaseEntity

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

    @CreatedDate // 최초 생성 시점
    private Timestamp createdAt;

    @LastModifiedDate // 마지막 변경 시점
    private Timestamp modifiedAt;
}

 

User

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
@Table(name = "Users")
public class User extends BaseEntity {

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

    @Column(nullable = false, unique = true)
    private String username; //이메일아이디

    @Column(nullable = false)
    private String password; //비밀번호
}