본문 바로가기
Java, Kotlin, Spring/JPA

JPA - 상속 객체와 테이블 매핑

by Wordbe 2021. 9. 19.
728x90

JPA 006 - 상속 객체와 테이블 매핑

객체는 상속을 통해 공통된 필드를 부모클래스에 만들고, 나머지 필드를 각 자식클래스에 둘 수 있다.

하지만, 관계형 데이터베이스에는 상속의 개념이 없다. 대신 슈퍼타입-서브타입 모델링 기법을 사용할 수 있다.

ORM 은 객체의 상속과 데이터베이스의 슈퍼타입-서브타입 관계를 매핑한다.

JPA 에서는 객체와 테이블을 매핑할 때 3가지 전략을 제시한다.

1 Joined 전략

부모테이블과 자식테이블 각각을 모두 생성하고, 원하는 데이터를 조인해서 가져온다.

각 자식테이블은 부모와 같은 PK 를 가지면서, 이 PK는 부모의 PK와 관계를 맺는 FK 가 된다.

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

@Entity
public class Game extends Play {
    private int numberOfPlayers;
}

@Entity
public class Music extends Play{
    private String artist;
}

@Entity
public class Video extends Play {
    private String director;
}

테이블이 정규화가 되기 때문에 중복된 컬럼을 제거하여 저장공간을 효율적으로 사용할 수 있게 된다.

하지만 조회시 쿼리가 복잡할 수 있다. 테이블이 여러개 생기니 데이터베이스는 좀 더 복잡해질 것이다.

 

2 Single Table 전략

@Data
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "play_type")
public class Play {
    @Id
    @GeneratedValue
    private Long id;
    private String name;
    private int price;

    @Column(name = "play_type", insertable = false, updatable = false)
    private String playType;
}

@Data
@ToString(callSuper = true)
@Entity
@DiscriminatorValue("MUSIC")
public class Music extends Play{
    private String artist;
}

... Video, Game

Inheritance 의 기본 전략은 SINGLE_TABLE 이다. (추천 전략)

그리고 상속된 엔티티를 분류하기 위해 한 컬럼이 추가되는데 이 컬럼의 이름은 기본으로 DTYPE 이다. 이 컬럼의 이름을 @DiscriminatorColumn(name = "play_type") 와 같이 사용해 다른 이름으로 설정할수도 있다.

또한 DTYPE 값은 기본적으로 상속받은 객체의 클래스이름이 된다. 위의 경우는 Game, Music, Video 가 된다. 이 값 역시 @DiscriminatorValue("MUSIC") 와 같이 다른 이름으로 설정 수 있다.

 

조인이 필요없어서 쿼리가 단순한 장점이 있다.

단, 한 자식 엔티티가 저장했을 때, 다른 자식 엔티티의 컬럼은 모두 null 이 들어갈 것이다. ( Music 을 저장했을 때 Video, Game 의 컬럼은 null 일 것이다.)

 

아래와 같이 테스트를 해보자.

@DataJpaTest
class MusicTest {
    @Autowired
    MusicRepository musicRepository;

    @Autowired
    EntityManager em;

    @Test
    void Music_저장시_PlayType에_MUSIC이_들어간다() {
          // given
        Music music = new Music();
        music.setName("사랑은 은하수 다방에서");
        music.setPrice(600);
        music.setArtist("10cm");

          // when
        Music save = musicRepository.save(music);
        System.out.println(save);

        // DataJpaTest 는 @Transactional 을 포함하고, 테스트 종료시 변경된 데이터를 rollback 한다.
        // 따라서 insert 문이 생기지 않는다. 테스트에서는 insert 가 필요하므로 영속성컨텍스트를 초기화해준다.
        em.flush();
        em.clear();

        Music foundMusic = musicRepository.findById(save.getId()).get();
        System.out.println(foundMusic);

                // then
        assertThat(foundMusic.getPlayType()).isEqualTo("MUSIC");
    }
}

테스트 출력

Music(super=Play(id=1, name=사랑은 은하수 다방에서, price=600, playType=null), artist=10cm)

Hibernate: insert into play (name, price, artist, play_type, id) values (?, ?, ?, 'MUSIC', ?)

Hibernate: select music0_.id as id2_7_0_, music0_.name as name3_7_0_, music0_.play_type as play_typ1_7_0_, music0_.price as price4_7_0_, music0_.artist as artist6_7_0_ from play music0_ where music0_.id=? and music0_.play_type='MUSIC'

Music(super=Play(id=1, name=사랑은 은하수 다방에서, price=600, playType=MUSIC), artist=10cm)
  • 생성할 때 play_type 에 MUSIC 을 잘 넣는 것을 볼 수 있다.
  • 조회할 때 where play_type = MUSIC 으로 잘 조회하는 것을 볼 수 있다.

 

3 Table per Class 전략

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

@Entity
public class Game extends Play {
    private int numberOfPlayers;
}

@Entity
public class Music extends Play{
    private String artist;
}

@Entity
public class Video extends Play {
    private String director;
}

자식 테이블 3개가 만들어지고, 부모 테이블은 만들어지지 않는다.

부모 테이블의 컬럼이 자식 테이블에 각각 모두 들어간다. 정규화가 되지 않고 중복이 있는 구성이 된다.

여러 자식 테이블을 함께 조회할 때 성능이 느리고 (SELECT 여러 번에 UNION 을 해서 가져온다.) 자식 테이블을 통합해서 쿼리하기 힘들다.

그러므로 일반적으로 추천하지 않는 전략이다.

 


본 글은 자바 ORM 표준 JPA 프로그래밍 (김영한 저)참고하여 재구성했습니다.



728x90

댓글