인프런/자바 ORM 표준 JPA 프로그래밍 - 기본편

7) 고급 매핑 [ 상속관계 매핑 -조인,단일,각각] , MappedSuperclass

backend dev 2024. 5. 29.

상속관계 매핑

 

• 관계형 데이터베이스는 상속 관계가 없다.

 

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

 

• 상속관계 매핑: 객체의 상속과 구조와         DB의 슈퍼타입 서브타입 관계를 매핑

왼쪽이 관계형 데이터베이스 논리모델

 

슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법

 

• 각각 테이블로 변환 -> 조인 전략

 

• 통합 테이블로 변환 -> 단일 테이블 전략

 

• 서브타입 테이블로 변환 -> 구현 클래스마다 테이블 전략

 

 

 

 

 

조인 전략

왼쪽은 객체 설계 모습 , 오른쪽은 테이블 설계 모습

DTYPE을 이용하여 앨범인지 영화인지 책인지 구분하겠다는것

 

장점

• 테이블 정규화

• 외래 키 참조 무결성 제약조건 활용가능

• 저장공간 효율화

 

단점

• 조회시 조인을 많이 사용, 성능 저하

• 조회 쿼리가 복잡함

• 데이터 저장시 INSERT SQL 2번 호출

 

 

 

단일 테이블 전략

왼쪽은 객체 설계 모습 , 오른쪽은 테이블 설계 모습

하나의 테이블에 모든 컬럼들을 넣는다.
,DTYPE을 이용하여 앨범인지 영화인지 책인지 구분하겠다는것

 

 

장점

• 조인이 필요 없으므로 일반적으로 조회 성능이 빠름

• 조회 쿼리가 단순함

 

단점

• 자식 엔티티가 매핑한 컬럼은 모두 null 허용

• 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.

  상황에 따라서 조회 성능이 오히려 느려질 수 있다.

 

 

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

왼쪽은 객체 설계 모습 , 오른쪽은 테이블 설계 모습

 

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

 

장점

• 서브 타입을 명확하게 구분해서 처리할 때 효과적

• not null 제약조건 사용 가능

 

단점

• 여러 자식 테이블을 함께 조회할 때 성능이 느림(UNION SQL 필요)

• 자식 테이블을 통합해서 쿼리하기 어려움

 

 

 

jpa에서는 위의 3가지 방법중 어떤것을 골라도 매핑 가능하다.

 


테스트

Item

@Entity
public abstract class Item {

    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private int price;
}

abstract로 만들지않으면 항상 테이블로 생성된다. ( 아이템 엔티티 하나도 독립적으로 사용된다는 의미이니까) [ 그러므로 abstract로 만들어서 상속전용 으로 만든다. ] 

 

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

이렇게 객체설계는 상속을 이용하도록 구성하고 실행해보면

jpa의 기본전략은 단일테이블 전략임을 확인할 수 있다.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Inheritance {
    InheritanceType strategy() default InheritanceType.SINGLE_TABLE;
}

 

 

join전략으로 바꿔보자.

@Inheritance(strategy = InheritanceType.JOINED)
@Entity
public abstract class Item {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int price;
}

 

@Inheritance(strategy = InheritanceType.JOINED)

부모클래스의 클래스레벨에 
@Inheritance(strategy = InheritanceType.JOINED)를 사용하여 조인전략으로 바꾼다.

각 클래스별 가지고있는 필드를 컬럼으로 가지고 id는 item의 기본키이다. 즉 외래키이다.

 

item의 기본키를 외래키로 가진다. 그리고 그것을 기본키로 삼는다. -> 식별관계
join전략 그 구조 그대로 생성된다. DTYPE은 ITEM에 넣지않아서 안생긴것 -> 밑에서 어노테이션으로 추가한다.

 

 

public static void main(String[] args) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    tx.begin();
    try {
        Movie movie = new Movie();
        movie.setDirector("aa");
        movie.setActor("bb");
        movie.setName("바람");
        movie.setPrice(10000);
        em.persist(movie);

        tx.commit();
    }catch (Exception e) {
        tx.rollback();
    }finally {
        em.close();
    }
    emf.close();
}

조인전략은 insert쿼리가 2번 실행되는것을 확인할 수 있다.

두 테이블다 데이터가 들어가있는것을 확인 가능하다.

조인전략 - 조회테스트

public static void main(String[] args) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    tx.begin();
    try {
        Movie movie = new Movie();
        movie.setDirector("aa");
        movie.setActor("bb");
        movie.setName("바람");
        movie.setPrice(10000);
        em.persist(movie);

        em.flush(); // 생성,수정,삭제등 변경사항 처리
        em.clear(); // 영속성 컨테이너 비우기

        em.find(Movie.class,movie.getId());
        tx.commit();
    }catch (Exception e) {
        tx.rollback();
    }finally {
        em.close();
    }
    emf.close();
}

 

join전략은 엔티티 데이터를 가져올때 join해서 가져오는것을 확인할 수 있다.

 

 

Item에 DTYPE이라는 식별자를 추가하기 위해 @DiscriminatorColumn 추가

@Getter
@Setter
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
@Entity
public abstract class Item {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int price;
}

@DiscriminatorColumn를 붙이면

해당 클래스의 필드에 DTYPE이 추가된다.

jpa가 insert 쿼리를 만들면서 알아서 값을 넣어준다.

 

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DiscriminatorColumn {
    String name() default "DTYPE";

    DiscriminatorType discriminatorType() default DiscriminatorType.STRING;

    String columnDefinition() default "";

    int length() default 31;
}

@DiscriminatorColumn는 기본값이 DTYPE이다.

 

DB 관점에서 볼때 ITEM이 추가됬다면 ITEM만 봐서는 Album,Movie,Book중에 어떤것때문에 추가된것인지 모르기때문에

식별자 컬럼을 추가해주는것이 좋다.

@DiscriminatorColumn(name = "DIS_TYPE")

name 속성을 이용하여 식별자의 컬럼명을 수정가능하다.

 

 

또한 해당 컬럼에 들어갈 데이터도 정의가 가능하다.

ex) Album에는 식별자로 A가 들어가야하고 ,Movie에는 식별자로 B가 들어가야하고 ...

@DiscriminatorValue(value = "A")
@Entity
public class Album extends Item{
    private String artist;
}

 

@DiscriminatorValue

를 이용하여 지정가능하다.

기본값은 엔티티명이다.

엔티티명은 기본으로 클래스명과 동일하다. ( 기본값 )

 

 

주요 어노테이션

@Inheritance(strategy=InheritanceType.XXX)

• JOINED: 조인 전략

• SINGLE_TABLE: 단일 테이블 전략

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

@DiscriminatorColumn(name=“DTYPE”) // DTYPE은 기본값.
@DiscriminatorValue(“XXX”)

 

 

 

단일테이블전략으로 수정하고 실행해본 결과

또한 단일테이블 전략은 

@DiscriminatorColumn(name=“DTYPE”) // DTYPE은 기본값.

이 어노테이션 없어도 알아서 식별자 컬럼이 생성된다.

@Getter
@Setter
@Inheritance // 기본 전략은 단일테이블
@Entity
public abstract class Item {

단일테이블은 이 데이터가 앨범인지 책인지 영화인지 알수가없으므로 식별자 컬럼이 필수이므로

따로 어노테이션으로 설정하지 않아도 기본으로 들어간다.

또한 단일테이블전략은 insert, select 쿼리가 하나이고 깔끔하다.

 

결론적으로 운영상 조인전략에도 DTYPE은 들어가있는게 좋다.

 

 

 

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

아이템 테이블을 없애고 공통된 속성을 가지도록 설계한 테이블 전략

 

@DiscriminatorColumn(name=“DTYPE”) // DTYPE은 기본값.

를 item클래스에 붙여도 생성되지않는다. 어차피 item엔티티는 테이블로 생성되지않기 때문.

 

조회를 할때 부모타입으로도 조회할 수 있어야한다.

Item item = em.find(Item.class, movie.getId());
System.out.println("item = " + item);

무비객체가 잘나온다.

구현 클래스마다 테이블 전략을 사용할때 부모타입으로 조회하게 된다면

    select
        i1_0.id,
        i1_0.clazz_,
        i1_0.name,
        i1_0.price,
        i1_0.artist,
        i1_0.author,
        i1_0.isbn,
        i1_0.actor,
        i1_0.director 
    from
        (select
            price,
            id,
            artist,
            name,
            null as author,
            null as isbn,
            null as actor,
            null as director,
            1 as clazz_ 
        from
            Album 
        union
        all select
            price,
            id,
            null as artist,
            name,
            author,
            isbn,
            null as actor,
            null as director,
            2 as clazz_ 
        from
            Book 
        union
        all select
            price,
            id,
            null as artist,
            name,
            null as author,
            null as isbn,
            actor,
            director,
            3 as clazz_ 
        from
            Movie
    ) i1_0 
where
    i1_0.id=?

 

이렇게 엄청 긴 union 쿼리를 사용하여 조회해오게 된다. [앨범,영화,책 모두 union 해서 가져옴 ]

 

 

join 전략의 장단점과 단일테이블 전략의 장단점을 고려해서 둘중 하나를 선택하면된다.

조인전략을 기본적으로 선택하고, 단순한 프로젝트라면 단일 테이블 전략을 선택한다.

단순한 프로젝트란 데이터도 얼마안되고 확장성도 없을거같은 프로젝트를 의미 

비즈니스적으로 복잡하다 싶으면 조인전략

 


@MappedSuperclass

• 공통 매핑 정보가 필요할 때 사용(id, name)

 

DB에서는 각각 컬럼으로 있지만

객체에서 공통된 필드를 상속받아서 사용하고 싶을때

 

예시)

private String createdBy;
private LocalDateTime createdDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;

다음과 같은 필수 공통 필드가 있다고 가정해보자.  [ 테이블의 필수 공통 컬럼 ]

 

이럴때 속성만 상속받아서 쓰고 싶을때 @MappedSuperclass를 사용한다.

@Getter
@Setter
@MappedSuperclass
public abstract class BaseEntity {
    private String createdBy;
    private LocalDateTime createdDate;
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;
}

이렇게 공통속성을 가지는 클래스를 구성하고 @MappedSuperclass를 붙인다.

@Entity
public class Member extends BaseEntity {

사용은 다음과같이 상속받으면 된다.

공통속성 적용된것을 확인가능

 

@Getter
@Setter
@MappedSuperclass
public abstract class BaseEntity {
    @Column(name = "INSERT_MEMBER")
    private String createdBy;
    private LocalDateTime createdDate;
    @Column(name = "UPDATE_MEMBER")
    private String lastModifiedBy;
    private LocalDateTime lastModifiedDate;
}

이런식으로 column어노테이션을 이용하여 컬럼이름도 수정가능.

 

 

@MappedSuperclass

상속관계 매핑X [ 공통된 속성을 매핑해주는것일 뿐이다. ]

 

엔티티X, 테이블과 매핑X [ 위에서 만든 BaseEntity는 테이블로 생성 안된다 속성만 내려주는 클래스이고 엔티티가 아님 ]

 

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

 

조회, 검색 불가(em.find(BaseEntity) 불가)

 

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

 

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

 

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

 

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

-> 

 

@Entity
public abstract class Item {
@Entity
public class Album extends Item{

이렇게 Album이라는 Entity클래스는 @Entity가 붙은 Item을 상속받는 상속관계일 경우 extends를 사용할 수 있고

또는 

@MappedSuperclass
public class BaseEntity {
@Entity
public class Member extends BaseEntity {

@MappedSuperclass 가 붙은 클래스만 extends로 상속받을 수 있다 ( 공통 속성 매핑을 위해 )

 

즉 @Entity가 붙은 클래스는 @Entity가 붙은 클래스 또는 @MappedSuperclass 가 붙은 클래스만 extends로 상속받을 수 있다

 

 

 


실전예제

 

요구사항 추가

 

• 상품의 종류는 음반, 도서, 영화가 있고 이후 더 확장될 수 있다.

 

• 모든 데이터는 등록일과 수정일이 필수다.

 

 

테이블은 단일 테이블 전략

 

 

 

@Getter
@Setter
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
@Entity
public abstract class Item {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int price;
}

item만 단독으로 저장할 일이 없으므로 abstract로 설정한다.

[앨범,영화,책 만 저장할거니까 ] 하지만 단일테이블전략에서는 ITEM이라는 부모엔티티의 이름을 가진 단일 테이블이 생성되어야한다. 조인전략에서는 조인을위한 부모테이블이 존재해야하므로 ITEM 테이블이 생성된다.

구현 클래스마다 테이블 전략에서는 부모테이블이 없어야하므로 abstract를 붙여야하고, 붙여야 item 테이블이 생성되지않는다.

 

@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 기본 전략은 단일테이블이므로 이렇게 안적어도되긴함.

 

단일 테이블전략은 기본적으로 식별자 컬럼을 가지고 그 식별자 컬럼 이름은 DTYPE이므로

@DiscriminatorColumn 또한 따로 안붙여도된다.

Book book = new Book();
book.setName("책");
book.setAuthor("rlf");

em.persist(book);

단일테이블에 데이터가 잘 들어가있다.

 

여기까지가 상속관계 고급매핑 테스트

 

공통속성 매핑 테스트는 위에서 이미 진행해봤다.

댓글