영한님의 강의들(JPA 기본, QueryDsl ...)을 들으면서 사내서비스를 리팩토링 중이었다.
개발 도중 JPA기본편 강의에서 들었던 상속관계 매핑을 사용할 기회가 있어 적용해보았다.
우선 간단하게 JPA 상속관계 매핑의 전략들에 대해 알아보겠다.
상속관계의 구조는 아래와 같다.
Server를 상속하는 App서버와 DB서버가 있다.
서버 IP, 서버 이름, OS버전 등은 공통적으로 사용되는 필드들이라 Server에 두었고, APP과 DB 서버에서 필요한 필드들은 각각 두었다. 이 상속관계를 이용하여 JPA에서 관계를 만들었다.
@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 전략을 사용했다.
@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를 상속받고 각각의 필드들이 존재한다.
Server에 대한 API 중 Server의 List를 DTO로 변환하여 return하고 싶었다. 그래서 아래와 같이 코드를 작성했다.
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인지 조건문을 걸어서 다운캐스팅을 해야 될 것 같다.... 괜한 로직이 발생하는 것 같다..! 또는 자식 타입 각각 조회하는 방법도 있다..!)
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)
위처럼 App과 DB를 JOIN하여 DTO로 바로 조회했더니 원하는 결과를 가져오긴 했다.
하지만 JOIN을 사용하지 않는 것이 SINGLE_TABLE전략의 장점이기 때문에 이 방법은 좀 꺼림직 했다.
어렵게 찾은 해결책....상속관계에서 DTO로 바로 조회하는법
정말 거짓말 안하고 하루 꼬박 해서 이 질문에 대한 답을 서칭했다. 구글과 영한님 JPA강의의 커뮤니티까지 많이 찾아봤는데 결국 QueryDsl 강의 커뮤니티에서 찾을 수 있었다....
결국 위에서 JPQL로 했던 것 처럼 Server 엔티티를 DTO로 직접변환 하거나, 네이티브쿼리를 사용해야만 한다... 이러고보니
네이티브쿼리로 하는게 가장 적합한 방법이지 않나 싶다.
위 답변에서 가장 눈에 띄는 말은 "만약 비즈니스 로직에 큰 차이가 없고, 단순히 데이터의 차이만 있다면, 상속 관계를 사용하지 말고, 한 테이블에 합치는 것을 권장합니다." 이었다,,, 지금까지 상속관계 매핑을 생각해서 설계했던 구조와 삽질했던 순간이 머릿속을 지나갔다. 물론 꼭 사용해야 하는 곳에는 사용해라. 라고 하셨고 약간의 허탈함이 나를 채웠다.
이후 나도 영한님 강의에 질문을 올렸다.
Q. 상속관계 매핑을 실무에서 잘 사용하지 않는 이유는 뭔가요?
A. JPA에서 상속관계 매핑은 매핑이 복잡하고, 성능까지 고민하면서 사용하기가 쉽지 않기 때문에 꼭 필요한 경우에 부분적으로 사용하는 것을 권장합니다.
Q. 상속관계 매핑을 사용하지 않고 한 테이블에 데이터를 모두 넣으면 컬럼에 null을 허용하는 것은 상속관계 매핑과 다를게 없을텐데 여기서 오는 이점이 무엇일까요?
A. 이 부분은 객체의 상속관계를 사용할 것인가? 아니면 객체 내부에 타입을 두고 해당 타입으로 구분할 것인가 하는 객체와 자료구조의 문제로 보시면 됩니다.
결론적으로 상속관계 매핑은 지양해야 하고, 꼭 필요로 할 때만 사용해야 된다는 것이다. 또 JOIN 전략이 정석적이며, SINGLE_TABLE전략을 사용 하고자 할 때 단순히 데이터의 차이만 있다면 상속관계 없이 한 테이블로 구현하는 것이 좋을 것 같다.
상속에 관한 레퍼런스를 찾다보니 "상속(Inheritance) 보단 조합(Composition)을" 이라는 말이 있더라. 나중에 공부해보고 적용하고 추가로 작성하겠다.