JPA - 상속관계 매핑

태히·2022년 9월 27일
0

SpringBoot/JPA

목록 보기
3/3

서론

영한님의 강의들(JPA 기본, QueryDsl ...)을 들으면서 사내서비스를 리팩토링 중이었다.
개발 도중 JPA기본편 강의에서 들었던 상속관계 매핑을 사용할 기회가 있어 적용해보았다.

상속관계 매핑이란.

우선 간단하게 JPA 상속관계 매핑의 전략들에 대해 알아보겠다.

  • JOIN 전략 - 부모 및 자식 테이블들을 각각 두어 정규화를 사용한 전략
    • 장점 : 외래키 제약조건 활용 / 테이블 정규화
    • 단점 : 조회 시 join 사용 / 성능저하 우려 / 조회쿼리 복잡 / INSERT 시 2번 호출
  • SINGLE_TABLE 전략 - 부모 테이블 하나로 통합 테이블 관리.
    • 장점 : 조인이 필요없어 조회가 빠름 / 조회 쿼리 단순
    • 단점 : 자식 엔티티가 매핑한 컬럼은 null 허용 / 한 테이블에 모든 데이터를 저장하여 오히려 성능저하를 유발 할 수 있음.

구조

상속관계의 구조는 아래와 같다.

Server를 상속하는 App서버와 DB서버가 있다.
서버 IP, 서버 이름, OS버전 등은 공통적으로 사용되는 필드들이라 Server에 두었고, APP과 DB 서버에서 필요한 필드들은 각각 두었다. 이 상속관계를 이용하여 JPA에서 관계를 만들었다.


엔티티 코드

  • Server 엔티티
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "s_dtype")
@Getter
@Setter
public abstract class Server {
    @Id
    @GeneratedValue
    private int serverKey;

    @Column(name="s_dtype",insertable = false,updatable = false)
    private String dtype;

    private String ip;
	private String os;
    private String serverName;
    .....(생략)
}

위는 Server 엔티티이다. 하위 엔티티들의 컬럼들이 많지 않고, 복잡한 엔티티가 아니라 SINGLE TABLE 전략을 사용했다.


  • App / DB 엔티티
@Getter
@Setter
@Entity
@DiscriminatorValue("APP")
public class App extends Server{

    private String ssl;
    private String apache;

}

@Getter
@Setter
@Entity
@DiscriminatorValue("DB")
public class DB extends Server {

    private String dbVersion;
    private String dbName;
    private String dbUserName;
    private String dbUserPwd;
}

위는 App/DB 엔티티이다. Server를 상속받고 각각의 필드들이 존재한다.


  • 테이블
    이렇게 객체를 구성하고 실행하게 되면 SINGLE_TABLE 전략이기 때문에 DB에 한 테이블에 3개의 엔티티들이 들어가게 된다. (통합 테이블!!)

본론

Server에 대한 API 중 Server의 List를 DTO로 변환하여 return하고 싶었다. 그래서 아래와 같이 코드를 작성했다.

조회 쿼리 작성

  1. JPQL

    List<Server> serversByEm = em.createQuery("select s from Server s", Server.class).getResultList();
    쿼리의 결과를 보면 List안의 객체들은 자식타입(App/DB)이지만 다형성으로 Server에 담겨져 나왔다.

    문제는 여기 부터였다.
    나는 조회된 List<Server>Dto로 변환하고 싶었는데 그게 생각처럼 쉽지 않았다.
    간단하게 보자면, 각 객체에는 자식 엔티티의 값들이 들어있지만 Server타입(부모타입)으로 조회가 되기때문에 자식타입(App,DB)으로 다운캐스팅을 하지 않는이상 자식 값에 접근 할 수 없었다. (다운캐스팅을 한다고 해도 List안의 객체가 App인지 DB인지 조건문을 걸어서 다운캐스팅을 해야 될 것 같다.... 괜한 로직이 발생하는 것 같다..! 또는 자식 타입 각각 조회하는 방법도 있다..!)

  2. QueryDsl

    QueryDsl도 위와 같은 결과가 나왔지만 다른방법은 적용해봤다.
    바로 부모엔티티와 자식엔티티를 Join하는 방법이다.... 위에서도 언급했지만 SINGLE_TALBE 의 장점은 JOIN을 사용하지 않는 것 이다. 그래도 테스트를 위해 한 번 작성해봤다.

    List<ServerResponseDto> serverList = queryFactory
                .select(Projections.bean(ServerResponseDto.class,
                    server.serverKey,
                    server.dtype,
                    server.ip,
                    server.serverName,
                    server.os,
                    app.ssl,
      				app.apache,
                    dB.dbVersion,
                    dB.dbName,
      				db.dbUserName,
      				db.dbUserPwd,
                ))
                .from(server)
                .leftJoin(app).on(app.eq(server))
                .leftJoin(dB).on(dB.eq(server))
                .fetch(); 

    FROM절은 이러하다. from t_server server0_ left outer join t_server app1_ on (app1_.s_server_key=server0_.s_server_key) left outer join t_server db2_ on (db2_.s_server_key=server0_.s_server_key)
    위처럼 AppDBJOIN하여 DTO로 바로 조회했더니 원하는 결과를 가져오긴 했다.
    하지만 JOIN을 사용하지 않는 것이 SINGLE_TABLE전략의 장점이기 때문에 이 방법은 좀 꺼림직 했다.

그럼 해결책은?

어렵게 찾은 해결책....상속관계에서 DTO로 바로 조회하는법
정말 거짓말 안하고 하루 꼬박 해서 이 질문에 대한 답을 서칭했다. 구글과 영한님 JPA강의의 커뮤니티까지 많이 찾아봤는데 결국 QueryDsl 강의 커뮤니티에서 찾을 수 있었다....
결국 위에서 JPQL로 했던 것 처럼 Server 엔티티를 DTO로 직접변환 하거나, 네이티브쿼리를 사용해야만 한다... 이러고보니
네이티브쿼리로 하는게 가장 적합한 방법이지 않나 싶다.
위 답변에서 가장 눈에 띄는 말은 "만약 비즈니스 로직에 큰 차이가 없고, 단순히 데이터의 차이만 있다면, 상속 관계를 사용하지 말고, 한 테이블에 합치는 것을 권장합니다." 이었다,,, 지금까지 상속관계 매핑을 생각해서 설계했던 구조와 삽질했던 순간이 머릿속을 지나갔다. 물론 꼭 사용해야 하는 곳에는 사용해라. 라고 하셨고 약간의 허탈함이 나를 채웠다.

결론

이후 나도 영한님 강의에 질문을 올렸다.

Q. 상속관계 매핑을 실무에서 잘 사용하지 않는 이유는 뭔가요?
A. JPA에서 상속관계 매핑은 매핑이 복잡하고, 성능까지 고민하면서 사용하기가 쉽지 않기 때문에 꼭 필요한 경우에 부분적으로 사용하는 것을 권장합니다.

Q. 상속관계 매핑을 사용하지 않고 한 테이블에 데이터를 모두 넣으면 컬럼에 null을 허용하는 것은 상속관계 매핑과 다를게 없을텐데 여기서 오는 이점이 무엇일까요?
A. 이 부분은 객체의 상속관계를 사용할 것인가? 아니면 객체 내부에 타입을 두고 해당 타입으로 구분할 것인가 하는 객체와 자료구조의 문제로 보시면 됩니다.

결론적으로 상속관계 매핑은 지양해야 하고, 꼭 필요로 할 때만 사용해야 된다는 것이다. 또 JOIN 전략이 정석적이며, SINGLE_TABLE전략을 사용 하고자 할 때 단순히 데이터의 차이만 있다면 상속관계 없이 한 테이블로 구현하는 것이 좋을 것 같다.

추가

상속에 관한 레퍼런스를 찾다보니 "상속(Inheritance) 보단 조합(Composition)을" 이라는 말이 있더라. 나중에 공부해보고 적용하고 추가로 작성하겠다.

profile
하고싶은게 많은 개발자가 되고싶은

0개의 댓글