Java - SpringJPA

Spring JPA (7) - JPA 고급 매핑 : 상속관계 매핑, @mappedsuperclass

TerianP 2022. 10. 4.
728x90

1. 상속 관계 매핑

- 관계형 DB 는 상속 관계 X

- 슈퍼 타입, 서브 타입 관계라는 모델링 기법이 객체 상속과 유사

 

1) 슈퍼 타입, 서브타입의 논리 모델을 실제 물리 모델로 구현하는 방법 : 총 3가지

  • 각각 테이블로 변환 -> 조인 전략
  • 통합 테이블로 변환 -> 단일 테이블 전략
  • 서브타입 테이블로 변환 -> 구현 클래스마다 테이블 생성
상속 관계 매핑 : 객체의 상속 구조와 DB 의 슈퍼 타입, 서브 타입 관계를 매핑하는 것

 

2) 주요 어노테이션

@Inheritance(strategy=InheritanceType.XXX)
   - JOINED : 조인 전략
   - SINGLE_TABLE : 단일 테이블 전략 => JPA 기본값
   - TABLE_PER_CLASS : 구현 클래스마다 테이블 전략

@DiscriminatorColumn(name="DTYPE")
@DiscriminatorValue("XXX")

 

3) 예제로 사용할 코드

Item

@Entity(name="newItem")
public class Item {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private int price;
}

 

 

Album

@Entity
public class Album extends Item{

    private String artist;
}

 

Book

@Entity
public class Book extends Item {

    private String author;

    private String isbn;
}

 

Movie

@Entity
public class Movie extends Item{

    private String director;
    private String actor;

}

2. 조인 전략

조인 전략 : 상속관계의 엔티티에 해당하는 내용을 각각 테이블로 변환

 

@Inheritance : Item 코드에 @Inheritance(strategy = InheritanceType.JOINED) 를 붙인다

@Entity(name="newItem")
@Inheritance(strategy = InheritanceType.JOINED) // 조인 전략
public class Item {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private int price;
}

Movie 와 Item 의 테이블이 따로 생성된다. 단 이때 Movie 의 PK 는 Item 의 PK 를 그대로 가져간다
Item 에서의 PK 1 은 Movie 에서 PK 이자 FK 로 역할한다
find 했을 때는 inner join 을 통해 엔티티를 가져온다


@DiscriminatorColumn : 부모 엔티티에 사용

- 조인 전략에서 사용되는 테이블에 따른 엔티티를 구분하기 위한 컬럼 자동 생성 및 컬럼에 값을 넣어주는 어노테이션

// 조인 전략 사용시 생성되는 여러 테이블에 대한 엔티티를 구분하기 위한 컬럼을 자동 생성 후
// 해당 컬럼에는 엔티티명이 들어가게 됨
// 이때 NAME 파라미터에는 컬럼명이 들어감 => 기본값 DTYPE
@DiscriminatorColumn(name = "item_Type")
@Entity(name="newItem")
@Inheritance(strategy = InheritanceType.JOINED) // 조인 전략
public class Item {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private int price;
}

 

@DiscriminatorValue : 자식 엔티티에 사용

- 부모에서 DiscriminatorColumn 를 사용한다면 자식 엔티티에서는 DiscriminatorValue 사용해서 DiscriminatorColumn 에 들어값 값 value 의 이름을 바꿔 줄 수 있음 

- 기본값은 엔티티명이며, DiscriminatorValue  안에 들어오는 값으로 변경 가능

// 부모 엔티티에서 DiscriminatorColumn 를 사용한다면 자식 엔티티에서는 DiscriminatorColumn
// 를 사용해서 DiscriminatorColumn 에 들어값 값 value 의 이름을 바꿔 줄 수 있음
// 기본값은 엔티티명이 들어가며, DiscriminatorValue 안에 들어오는 값으로 변경 가능
@DiscriminatorValue("Moovie")
public class Movie extends Item{

    private String director;
    private String actor;

}

item_type 컬럼이 추가된다
moovie 라는 값이 item_type 에 들어간다

 

3. 단일 테이블 전략 : JPA 기본값

- 상속 관계에 해당하는 엔티티의 내용을 통합 테이블로 변환해서 사용하는 전략

- select 와 update 모두 한번에 됨 => 조인 전략보다 성능상 우위 

- @DiscriminatorColumn 가 따로 없어도, 자동으로 생성된다. 생각해보면 당연한 게 여러 자식 엔티티를 하나의 테이블에 다 넣는 만큼 어떤 자식 엔티티가 insert 되었는지 구분해야함으로 따로 붙지 않아도 붙은 것처럼 인식되어진다

@Entity(name="newItem")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 단일 테이블 전략
public class Item {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private int price;
}

한번에 만들어진다 && 자동으로 DTYPE 이 생성됨

 

4. 구현 클래스 마다 테이블 생성 전략

- 조인 테이블 전략과 비슷하다.

- 조인 테이블 전략과 가장 큰 다른점은 조인 테이블은 공통된 속성은 부모 엔티티가 갖고 있게 하고 자식은 이를 JOIN 해서 사용해서 사용했지만, 테이블 생성 전략은 공통된 속성이나 공통되지 않은 속성 모두 각각의 테이블의 컬럼으로 추가해서 사용하는 전략 

조인 전략은 공통된 속성은 부모 엔티티가 갖게 하지만, 
테이블 생성 전략은 모든 속성은 각각 테이블이 갖고 있게 컬럼을 중복 허용

함으로서 테이블 생성해서 사용하는 전략
따라서 테이블 생성 전략에서 부모 엔티티는 추상 클래스로 만들어서 상속 후 사용하는게 적합!
@Entity(name="newItem")
// 클래스 마다 테이블 생성 전략
// 부모 엔티티는 추상 클래스로 만들어서 사용하는게 적합
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@Getter
@Setter
public abstract class Item {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private int price;
}

다른 전략들과는 다르게 item 테이블은 생성되지 않는다

 

5. 각각의 전략들과 장단점

전략 장점 단점
조인 전략
(가장 권장됨)
- 테이블의 정규화
- 외래키 참조 무결성 제약조건 활용 가능
- 저장 공간의 효율화
- 조회 시 조인을 많이 사용 -> 성능저하
- 조회 쿼리가 복잡함
- INSERT SQL 이 2번 호출
- 테이블이 많아 관리가 복잡?
단일 전략 - 조인이 필요 없음으로 조회 성능이 빠름
- 조회 쿼리가 단순함
- 자식 엔티티가 매핑한 컬럼은 모두 NULL이 허용되어야 함
=> 어떤 자식 엔티티가 올지 모름으로
- 단일 테이블임으로 테이블이 커질 수 있고, 상황에 따라서 조회 성능이 오히려 느려질 수 있다
클래스-테이블 생성 전략
(아주 좋지 못함)
- DB 설계자와 ORM 설계자 모두 추천하지 않는 전략
- 서브 타입을 명확하게 구분해서 처리 할 때 효과적
- NOT NULL 제약 조건 사용 가능 
- 여러 테이블을 함께 조인할때 성능이 느림 -> UNION SQL 을 사용하게 됨
- 자식 테이블을 통합해서 쿼리하기 어려움
- 새로운 자식테이블이 추가되거나 테이블이 변경되는 경우 재사용성이 떨어짐

 

6. @MappedSuperclass

1) @MappedSuperClass

- 테이블은 서로 다른 테이블을 사용하지만, 공통 매핑 정보가 필요할 때 사용함

- 아래 그림에서 보자면 Member 과 Seller 은 서로 다른 테이블이지만 id 와 name 이라는 공통 속성을 사용하고, 이렇게 공통된 속성만을 매핑해서 사용하고 싶을 때 쓰는 어노테이션 

 

- 사용하려면 공통 속성을 갖는 superclass 가 필요하다 => 이때 superclass 는 추상 클래스로 사용이 권장됨

- 상속 관계 매핑 X => 엔티티 X , 테이블과 매핑 X

- 부모 클래스를 상속받는 자식 클래스에 매핑 정보만 제공

- 조회, 검색 em.find 불가

- 직접 생성해서 사용할 일이 없음으로 추상 클래스로 사용 권장

- 테이블과 관계가 없고, 단순히 엔티티가 공통으로 사용한느 매핑 정보를 모으는 역할

- 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용함

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

/*
- 테이블은 서로 다른 테이블을 사용하지만, 공통 매핑 정보가 필요할 때 사용함
- 아래 그림에서 보자면 Member 과 Seller 은 서로 다른 테이블이지만 id 와 name 이라는 공통 속성을 사용하고,
    이렇게 공통된 속성만을 매핑해서 사용하고 싶을 때 쓰는 어노테이션
- 사용하려면 공통 속성을 갖는 superclass 가 필요 => 추상 클래스로 만들어 사용하는게 권장됨
*/
@MappedSuperclass
@Getter @Setter
public abstract class BaseEntity {
    private String createdBy;
    private LocalDateTime createdDate;
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;
}

 

7. 실전 예제 코드로 공부하기

요구 사항
- 상품 종류는 음반, 도서, 영화가 있고 이후 더 확장될 수 있다

- 모든 데이터는 등록일과 수정일이 필수!

요구사항에 따른 사용 전략
- 조인 전략 사용
- BaseEntity 사용

1) Album

@Entity(name = "domain_album")
@Setter @Getter
public class Album extends Item{

    private String artist;
    private String etc;
}

 

2) Book

@Entity(name = "domain_book")
@Setter @Getter
public class Book extends Item{
    private String author;
    private String isbn;
}

 

3) Movie

@Entity(name = "domain_movie")
@Getter @Setter
public class Movie extends Item{
    private String director;
    private String actor;
}

 

4) Item

@Entity
@Table
@Getter
@Setter
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "item_type")
public abstract class Item extends BaseEntity{

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

    private String name;

    private int price;

    private int stockQuantity;

    // 다대다 매핑
    // joinTable 을 통해서 임의로 테이블 생성 후 join 하게 됨
    // 단 이때 연관관계의 주인은 Category 에서 가져갔음으로 여기는 mappedBy 를 사용한다
    @ManyToMany(mappedBy = "items")
    private List<Category> categories = new ArrayList<>();

}

 

5) BaseEntity

/*
* 공통된 속성을 매핑하기 위한 mappedSuperclass
* 이 클래스는 모든 클래스에 넣어준다
* */
@MappedSuperclass
@Getter
@Setter
public abstract class BaseEntity {
    private String createdBy;
    private LocalDateTime createdDate;

    private String modifiedBy;
    private LocalDateTime modifiedDate;
}

조인 전략을 사용했기에 Item 과 domain_Book 모두에 insert 되었다

 

댓글