[JPA] 3.상속관계

재우·2025년 3월 8일

JPA

목록 보기
3/11

객체의 상속 관계를 db 테이블에서 구현하는 방법

  • db의 슈퍼타입과 서브타입이 객체의 상속관계와 유사하다.
  • 상속관계 매핑 : 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑
  • JPA는 상속관계를 아래 3가지 중에 어느 방법을 하더라도 모두 상속관계 매핑이 가능하다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
//@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
//@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@DiscriminatorColumn
public class Item {

    @Id @GeneratedValue
    private Long id;
    private String name;
    private int price;
    
    ...getter and setter
}
@Entity
public class Album extends Item{
    private String artist;
    
    ...getter and setter
}
@Entity
public class Book extends Item{
    private String author;
    private String isbn;
     
    ...getter and setter
}
@Entity
//@DiscriminatorValue("M")
public class Movie extends Item{
    private String director;
    private String actor;
    
    ...getter and setter
}

1. 조인 전략

  • @Inheritance(strategy = InheritanceType.JOINED)
Movie movie = new Movie();
movie.setDirector("aaa");
movie.setActor("bbb");
movie.setName("바람과 함께 사라지다");
movie.setPrice(10000);

em.persist(movie);

Album album = new Album();
album.setArtist("ccccc");
album.setName("하이요");
album.setPrice(20000);

em.persist(album);
  • ALBUM데이터를 추가(insert)하면, name이나 price는 ITEM테이블에 insert되고 artist는 ALBUM테이블에 insert된다. 2번의 insert 쿼리가 발생한다.
  • 이렇게 하게되면, db에는 이렇게 쌓이는데, Movie의 ID는 PK이면서 동시에 FK이다. 이 ID는 부모테이블인 Item의 ID값과 동일하다.
  • Movie데이터를 추가하게되면, Movie의 ID는 PK이지만 별도로 순차적으로 증가하지않고, 부모테이블인 Item의 ID값과 동일하게 된다.
  • 부모테이블의 PK값이 자식테이블의 PK가 된다. 부모테이블의 PK값이 자식테이블에 상속된다고 생각하면된다.
  • 자식테이블의 PK값은 순차적으로 값이 증가하지않고 1,3,7처럼 될 수도 있다. PK라는것이 단순히 테이블 내에서 중복되지만 않으면 되기 때문이다.

@DiscriminatorColumn

  • @DiscriminatorColumn를 적어주면, 부모테이블에 DTYPE컬럼이 생성된다.
  • 부모테이블(슈퍼타입)에 DTYPE과 같은 구분할 수 있는 컬럼을 넣어서, 어떤 데이터인지 명시한다.
  • DTYPE컬럼의 값은 기본값은 해당 엔티티명이다. 자식데이터(Movie)를 저장하면, 자식엔티티명이 되고, 자식데이터가 아닌 부모데이터(Item) 자체를 저장하면 부모엔티티명이 된다.
  • @DiscriminatorColumn는 적어주는것이 좋다. 부모데이터(Item)만 조회하게되면, 해당 데이터가 어떤 데이터를 insert했던것이었는지 구별할수가 없기 때문이다.
  • DTYPE컬럼명을 바꾸고 싶으면 @DiscriminatorColumn(name ="DIS_TYPE")과 같이 바꾸면된다.
  • DTYPE컬럼의 값을 엔티티명이 아닌 별도로 지정해주고 싶으면, 부모클래스나 자식클래스에서 @DiscriminatorValue("M")이런식으로 지정해주면 된다.

2. 단일테이블 전략

  • @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
  • 부모엔티티에 @Inheritance(strategy=InheritanceType.XXX)를 작성하지않으면, 기본값은 단일테이블 전략이다.
  • 객체의 상속관계를 각각의 테이블로 나누지않고, 단일 테이블로 구성하는 방법.
  • 단일 테이블에 DTYPE과 같은 구분할 수 있는 컬럼을 넣어서, 어떤 데이터인지 명시한다.
  • 단일테이블전략에서는 @DiscriminatorColumn를 적지않아도 DTYPE과 같은 컬럼명이 생성된다.

  • 단일 테이블 전략으로 하게되면 애초에 자식테이블은 생성되지않는다. 부모엔티티에 해당하는 부모테이블만 단일테이블로 생성된다. 그래서 자식데이터를 저장하더라도, 단일테이블인 ITEM테이블에 데이터가 저장된다. 그래서 1번의 insert쿼리만 발생한다. 자식데이터를 저장할떄, 해당 자식데이터와 상관없는것들은 null값이 들어간다.

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

  • @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
  • 실무에서 사용할 일이 거의 없다.
  • ITEM테이블과 같은 부모테이블(슈퍼타입)에 공통된 내용(name, price)을 따로 나누지않고, 공통된 내용(name, price)을 각각의 테이블이 모두 가지고 있는 방법.
  • 각 자식데이터를 저장할때, 각각의 테이블에 저장된다. 부모테이블에 데이터가 저장되지않는다.
  • DTYPE과 같은 구분할 수 있는것이 필요없다.
  • 주로 부모클래스를 추상클래스로 만든다.
  • 부모테이블과 각각의 자식테이블을 포함한 모든 상속관계의 테이블의 ID값이 중복되지않도록 해야한다. ID값이 중복되지않도록 하는 ID생성 전략을 사용해야함.

추상클래스

  • 참고로 abstract class Item처럼 추상클래스로 지정하면, Item item = new Item(); 처럼 객체를 생성할 수 없다.
  • 구현클래스마다테이블전략에서는 부모클래스를 추상클래스로 만들면 부모엔티티에 해당하는 부모테이블은 생성되지않는다. 하지만 조인전략과 단일테이블전략은 부모 클래스를 추상클래스여도 부모엔티티에 해당하는 부모테이블이 생성된다.


💡참고로 데이터를 조회할때,

기본적으로,
Member member = em.find(Member.class, 1); 를 하면 MEMBER테이블에서만 select하고,
Item item = em.find(Item.class, 1); 을 하면 ITEM테이블에서 select한다.


하지만 상속관계에서는 조금 다르다.

1. 조인전략

  • 부모테이블과 자식테이블을 join해서 데이터를 가져온다.
  • 부모테이블에 있는 PK와 자식테이블에 있는 PK이자 FK를 join해서 데이터를 가져온다.
  • 조인전략에서, 영속성 컨텍스트 조회 시,
    • 영속성 컨텍스트에 자식객체인 Book객체가 저장되어 있을때,
    • em.find(Item.class, id)를 하면, id가 일치하는 데이터를 조회해서 해당 객체를 그대로 반환.
    • em.find(Book.class, id)를 하면, id가 일치하는 데이터를 조회해서 해당 객체를 그대로 반환.
    • 즉, 부모타입으로 조회를 해도 해당 객체가 그대로 반환된다. 자식객체인 Book객체가 리턴된다. => 상속, 다형성의 원리
    • 영속성 컨텍스트에 부모객체인 Item객체가 저장되어 있다면,
    • em.find(Item.class, id)를 하면, id가 일치하는 데이터를 조회해서 해당 객체를 그대로 반환.
    • em.find(Book.class, id)를 통해서는 조회할 수 없다. 즉, 자식 타입으로는 조회할 수 없다.
  • 조인전략에서, db 조회 시,
    • em.find(Item.class, id)를 하면, id가 일치하는 데이터를 부모테이블을 기준으로 모든 자식테이블과 조인해서 조회하는데 이때 자식테이블 컬럼에 해당하는 데이터가 있으면 해당 자식 타입인 객체를 생성해서 반환한다. 자식 테이블 컬렘에 해당하는 데이터가 하나도 없으면 부모타입인 객체를 생성해서 반환한다. 조회되는 하나의 행에는 Item 데이터가 존재하고, Book, Movie, Album데이터는 존재할 수도있고 없을 수도 있다. 존재하지 않는 자식테이블 컬럼은 NULL이다. 조회되는 데이터가 없으면 null을 반환한다.
    • em.find(Item.class, 1); 을 하면 내부적으로 부모테이블을 기준으로 모든 자식테이블과 left join을 한다.
    • em.find(Book.class, id)를 하면, id가 일치하는 데이터를 자식테이블을 기준으로 부모테이블과 조인해서 조회하는데 이때 데이터가 있으면 자식타입인 객체를 생성해서 반환한다. 조회되는 데이터가 없으면 null을 반환한다.
    • em.find(Book.class, id)를 하면, 내부적으로 자식테이블을 기준으로 부모테이블과 inner join을 한다.

왜 left join을 하거나 inner join을 할까?

select 
    *
from 
    ITEM i
left join
    Album a
on 
    a.id = i.id
  • 예를들어 이런식으로 left join을 하게되면, ITEM테이블의 데이터 전부와 Album테이블의 id와 ITEM테이블의 id가 같은 데이터를 가져온다. 즉 ITEM테이블의 데이터 전부와 Album테이블의 데이터 전부를 on 조건에 맞게 가져온다.
  • 이렇게 데이터 전부를 가져오는데, on에 의해 id가 같은 데이터가 Album에 없을수도 있다. 이럴때는 Album테이블의 데이터 부분에 null값이 들어간다. 따라서 Item데이터를 가져오고, 자식데이터는 있으면 자식데이터도 가져오고, 없으면 null값을 가져오게 하기 위해 내부적으로 left join을 한다.
select 
	*
from 
	MOVIE m
inner join 
	ITEM i
on
	i.id = m.id
  • 예를들어 이런식으로 inner join을 하게되면, MOVIE테이블의 데이터 전부와 ITEM테이블의 id와 MOVIE테이블의 id가 같은 데이터를 가져온다. 즉 MOVIE테이블의 데이터 전부와 ITEM테이블의 데이터 전부를 on 조건에 맞게 가져온다.
  • 이렇게 데이터 전부를 가져오는데, on에 의해 id가 같은 데이터가 ITEM에 없을수도 있다. 이럴때는 MOVIE 데이터뿐만 아니라 ITEM 데이터도 조회되지않는다. 하지만 JOINED전략에서는 자식테이블에 데이터가 있으면 부모테이블에도 무조건 데이터가 있다. 그래서 join할때 on에 의해 id가 같은 데이터가 무조건 있기 때문에 내부적으로 inner join을 한다.

left join을 하면 on조건절에 해당하지않으면 일단 데이터를 붙이긴하는데 null값이 들어가는거고,
inner join을 하면 on조건절에 해당하지않으면 MOVIE데이터뿐만아니라 ITEM데이터도 조회가 안된다. 즉 inner join은 on조건절에 해당하는 데이터만 조회한다.

참고로, 어떤 전략이든 상관없이 영속성 컨텍스트 조회나 db조회 시 em.find(Item.class, id)를 했는데 Book객체가 리턴되더라도, 이때에는 Item item = em.find(Item.class, id); 처럼 해야한다. em.find()를 할때 반환타입을 Item.class라고 지정했으므로 Book book = em.find(Item.class, id)가 아니라 Item item = em.find(Item.class, id);로 해야한다.

2. 단일테이블전략

  • 부모테이블밖에 없으므로 부모테이블에서 select해서 데이터를 가져온다.
  • 즉, 단일테이블전략에서는 Item item = em.find(Item.class, 1); 를 하든 Member member = em.find(Member.class, 1); 을 하든 ITEM테이블밖에 없으므로 ITEM테이블에서 select해서 데이터를 가져온다.
  • 단일테이블 전략에서, 영속성 컨텍스트 조회 시
    • 영속성 컨텍스트에 자식 객체인 Book객체가 저장되어 있을때,
    • em.find(Item.class, id)를 하면, id가 일치하는 데이터를 조회해서 해당 객체를 그대로 반환.
    • em.find(Book.class, id)를 하면, id가 일치하는 데이터를 조회해서 해당 객체를 그대로 반환.
    • 즉, 부모 타입으로 조회를 해도 해당 객체가 그대로 반환된다. 자식 객체인 Book객체가 리턴된다. => 상속, 다형성의 원리
    • 영속성 컨텍스트에 부모객체인 Item객체가 저장되어 있을때,
    • em.find(Item.class, id)를 하면, id가 일치하는 데이터를 조회해서 해당 객체를 그대로 반환.
    • em.find(Book.class, id)를 통해서는 조회할 수 없다. 즉, 자식 타입으로는 조회할 수 없다.
  • 단일 테이블 전략에서, db 조회 시
    • em.find(Item.class, id)를 하면, id가 일치하는 데이터를 조회해서 이를 바탕으로 DTYPE을 확인해서 해당 DTYPE기반의 타입인 객체를 생성해서 반환한다. 단일테이블 전략에서는 DTYPE과 같은 컬럼이 무조건 있기 때문이다. 그래서 DTYPE이 Item이면 Item객체를 생성해서 반환하고 DTYPE이 Book이면 Book객체를 생성해서 반환한다. 조회되는 데이터가 없으면 null을 반환한다.
    • em.find(Book.class, id)를 하면, Book의 DTYPE과 id가 일치하는 데이터를 조회해서 이를 바탕으로 Book객체를 생성해서 반환한다. 조회되는 데이터가 없으면 null을 반환한다. 그래서 예를들면 Album에 대한 데이터는 db에 저장하지않았는데, Album album = em.find(Album.class, 1); 을 하게되면, DTYPE='Album'인 데이터가 없기 때문에 결과가 없어서 null을 리턴한다.

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

  • Item item = em.find(Item.class, 1); 을 하게되면, ITEM테이블이 있으면 내부적으로 ITEM테이블을 포함한 모든 자식 테이블을 UNION ALL 해서 데이터를 가져오고, ITEM테이블이 없으면 모든 자식 테이블을 UNION ALL 해서 데이터를 가져온다. 즉, 모든 테이블의 데이터를 UNION ALL로 합친다음에 WHERE 조건을 적용하여 데이터를 찾아서 가져온다.
  • ITEM테이블이 있으면,
    select
            item0_.id as id1_2_0_,
            item0_.name as name2_2_0_,
            item0_.price as price3_2_0_,
            item0_.artist as artist1_0_0_,
            item0_.author as author1_1_0_,
            item0_.isbn as isbn2_1_0_,
            item0_.actor as actor1_6_0_,
            item0_.director as director2_6_0_,
            item0_.clazz_ as clazz_0_ 
        from
            (select
                id,
                name,
                price,
                null as artist,
                null as author,
                null as isbn,
                null as actor,
                null as director,
                1 as clazz_ 
            from
                Item 
            union
            all select
                id,
                name,
                price,
                artist,
                null as author,
                null as isbn,
                null as actor,
                null as director,
                1 as clazz_ 
            from
                Album 
            union
            all select
                id,
                name,
                price,
                null as artist,
                author,
                isbn,
                null as actor,
                null as director,
                2 as clazz_ 
            from
                Book 
            union
            all select
                id,
                name,
                price,
                null as artist,
                null as author,
                null as isbn,
                actor,
                director,
                3 as clazz_ 
            from
                Movie 
        ) item0_ 
    where
        item0_.id=?
  • ITEM테이블이 없으면,
    select
            item0_.id as id1_2_0_,
            item0_.name as name2_2_0_,
            item0_.price as price3_2_0_,
            item0_.artist as artist1_0_0_,
            item0_.author as author1_1_0_,
            item0_.isbn as isbn2_1_0_,
            item0_.actor as actor1_6_0_,
            item0_.director as director2_6_0_,
            item0_.clazz_ as clazz_0_ 
        from
            ( select
                id,
                name,
                price,
                artist,
                null as author,
                null as isbn,
                null as actor,
                null as director,
                1 as clazz_ 
            from
                Album 
            union
            all select
                id,
                name,
                price,
                null as artist,
                author,
                isbn,
                null as actor,
                null as director,
                2 as clazz_ 
            from
                Book 
            union
            all select
                id,
                name,
                price,
                null as artist,
                null as author,
                null as isbn,
                actor,
                director,
                3 as clazz_ 
            from
                Movie 
        ) item0_ 
    where
        item0_.id=?

    UNION ALL

     select
        id,
        name,
        price,
        artist,
        null as author,
        null as isbn,
        null as actor,
        null as director,
        1 as clazz_ 
    from
        Album 
    union all 
    select
        id,
        name,
        price,
        null as artist,
        null as author,
        null as isbn,
        actor,
        director,
        3 as clazz_ 
    from
        Movie 
    • UNION ALL은 두 개 이상의 SELECT 결과를 행 단위로 결합한다.
  • Member member = em.find(Member.class, 1); 을 하게되면, MEMBER테이블에서 select해서 데이터를 가져온다.
    select
            movie0_.id as id1_2_0_,
            movie0_.name as name2_2_0_,
            movie0_.price as price3_2_0_,
            movie0_.actor as actor1_6_0_,
            movie0_.director as director2_6_0_ 
        from
            Movie movie0_ 
        where
            movie0_.id=?

@MappedSuperclass

  1. @MappedSuperclass로 지정한 클래스는 엔티티(@Entity)가 아니고, 테이블이 생성되지않는다.
    @Entity를 선언해줘도 테이블이 생성되지않는다.
  2. 공통 매핑 정보를 제공하는 부모클래스 역할이다.
    이때, 매핑 정보란 부모클래스에 선언된 필드와 필드에 적용된 JPA어노테이션을 말한다.
  3. 상속관계 매핑의 조인전략이랑 비슷해보이지만, @MappedSuperclass로 지정한 부모클래스는 데이터만 상속하고 테이블은 생성되지않는다. 즉, 부모 클래스(@MappedSuperclass)에서 상속받은 매핑 정보를 바탕으로 자식 테이블이 생성된다.
  4. @Entity 클래스는 @Entity나 @MappedSuperclass로 지정한 클래스만 상속 가능하다.
    @Entity나 @MappedSuperclass를 지정하지않은 일반 클래스는 상속할 수 없다.
    @MappedSuperclass
    public abstract class BaseEntity { // ✅상속 가능
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    }
    @Entity
    @Inheritance(strategy = InheritanceType.JOINED)
    public abstract class BaseEntity { // ✅상속 가능
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    }
    public class BaseEntity { // ❌상속 불가능
        private Long id;
    }
    @Entity
    public class User extends BaseEntity {  // ✅상속
        private String name;
    }

0개의 댓글