상속관계 매핑 - 조인전략

PPakSSam·2022년 1월 7일
0
post-thumbnail

상속관계 매핑 목차


상속관계 매핑

  • 관계형 데이터베이스는 상속 관계가 없다.
  • 슈퍼타입과 서브타입 관계라는 모델링 기법에 객체 상속과 유사하다.
  • 상속관계 매핑 : 객체의 상속구조와 DB의 슈퍼타입, 서브타입 관계를 매핑

상속관계 매핑의 종류

  • 슈퍼타입 서브타입 논리모델을 실제 물리모델로 구현하는 방법
    • 각각 테이블로 변환 -> 조인 전략
    • 통합 테이블로 변환 -> 단일 테이블 전략


조인 전략

[Item]

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public class Item {

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

[Album]

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

[Movie]

@Entity
@DiscriminatorValue("MOVIE");
public class Movie extends Item {
	
    private String director;
    private String actor;
}

[JpaMain]

Movie movie = new Movie();
movie.setDirector("aaaaa");
movie.setActor("bbbb");
movie.setName("바람과 함께 사라지다");
movie.setPrice(10000);

em.persist(movie);

  • @Inheritance(strategy = InheritanceType.JOINED) : 조인전략 실행
  • @DiscriminatorColumn : 테이블에 DTYPE이라는 컬럼 생성
  • @DiscriminatorValue("ALBUM") : DTYPE 값으로 어떤 값이 들어갈지 명시

장점

  • 테이블을 정규화 시킨 것이다.
  • 외래키 참조 무결성 제약조건 활용 가능
    • Item 테이블의 Id로 다 연결되어 있기 때문에 이를 활용할 수 있다.
    • 예를 들어 movie의 돈을 다 합산한 값을 알고싶으면 Item테이블만 뒤져도 된다.
  • 저장공간 효율화

단점

  • 조회시 조인을 많이 사용하여 성능이 저하된다.
  • 조회 쿼리가 복잡하다.
  • 데이터 저장시 INSERT 쿼리를 2번 호출한다.


참고

외래키 참조 무결성 제약 조건

외래키 값은 NULL이거나 참조 테이블의 기본키 값과 동일해야 한다는 것이다.
즉 참조할 수 없는 외래키 값을 가질 수 없다는 것이다.
ex) 수강 테이블의 학번 속성에는 학생 테이블의 학번 속성에 없는 값을 입력할 수 없다.

무결성 제약조건 관련 내용은 무결성 제약조건를 참고하길 바란다.

조회 쿼리가 복잡하다

조인테이블 전략의 단점으로

  • 조회시 조인을 많이 사용하여 성능이 저하된다.
  • 조회 쿼리가 복잡하다.

라고 설명이 되어있는데 그냥 그런갑다... 하고 넘어간 내 자신을 반성한다 ㅎㅎ.

JPA 스터디원 중 한명인 핑구님이 상속관계 매핑의 문제점(이 포스트를 한번 읽어보는 걸 추천한다)에서 조인테이블 전략의 단점을 자세히 설명하였고, 이에 대해 간단히 정리하고자 한다.

 @Test
 @DisplayName("상속관계 매핑 조회 테스트")
 public void findById() throws Exception {

     // given
     Movie movie = Movie.builder()
             .name("바람과 함께 사라지다")
             .actor("ppak")
             .price(10000)
             .director("mrPPak")
             .build();

     Album album = Album.builder()
             .name("명곡 앨범")
             .artist("mrPPak")
             .price(100000)
             .build();

     movieRepository.save(movie);
     albumRepository.save(album);

     // when
     Item item1 = itemRepository.findById(1L).get();
}

위와 같이 테스트코드를 작성한 후 실행해보니 다음과 같은 쿼리가 호출되었다.

  select
        item0_.id as id2_5_0_,
        item0_.name as name3_5_0_,
        item0_.price as price4_5_0_,
        item0_1_.artist as artist1_0_0_,
        item0_2_.actor as actor1_6_0_,
        item0_2_.director as director2_6_0_,
        item0_.dtype as dtype1_5_0_ 
    from
        item item0_ 
    left outer join
        album item0_1_ 
            on item0_.id=item0_1_.id 
    left outer join
        movie item0_2_ 
            on item0_.id=item0_2_.id 
    where
        item0_.id=?

보다시피 join이 2번 호출됨을 알 수 있다.
그래서 나는 join하지 않고 그냥 Item 테이블만 검색하기 위해 다음과 같이 작성했다.

// queryDsl
@Override
public Optional<Item> findByIdCustom(Long id) {
     return Optional.ofNullable(
     queryFactory.selectFrom(item).where(item.id.eq(id)).fetchOne());
}

Item item2 = em.createQuery("select i from Item i where i.id = 1", Item.class)
               .getSingleResult();
Item item3 = itemRepository.findByIdCustom(1L).get();

그리고 테스트를 진행하였는데도 불구하고 위의 쿼리와 같은 쿼리가 호출되는것이 아니겠는가?

응 뭐지? join 안쓰도록 강제했는데....?????

답은 간단하였다... (이럴 때마다 바보같음을 느낀다 ㅋㅋㅋ)
ItemRepositoryfindById()도 결국 jpql로 구성된 것인데,
itemRepository.findById()를 호출하든,
em.createQuery("select i from Item i where i.id = 1", Item.class)를 호출하든
결국 똑같은 jpql을 호출한 것이라는 것을 알게되었다...

정리하면 분명히 select i from Item i where i.id = 1이라는 jpql을 호출하였으나
하이버네이트에서 이 jpql을 보고 join절까지 추가하여 쿼리를 내보내는 것이었다...

난 join절 쓰기 싫다고!!!

@Inheritance 를 쓰지말고 직접 OneToOne으로 구현하는 방법이 있고,
최후의 방법으로 마이바티스같은 것을 이용하는 방법이 있다.
이에 대한 내용도 상속관계 매핑의 문제점에 있으니 참고하길 바란다.

profile
성장에 대한 경험을 공유하고픈 자발적 경험주의자

0개의 댓글