JPA

서로 연관관계가 없는 엔티티를 DTO로 조회하기

문제 상황

전세계의 프로 축구 선수를 관리하는 시스템을 만든다고 가정하자. 이 시스템에는 나라, 리그, 선수 이렇게 세가지 엔티티가 있다. 이 세 엔티티 모두 직접 참조는 되어 있지 않은 상태로 모두 물리적인 연관관계는 없다. 다만 리그가 나라를 id로 간접 참조하고, 선수가 리그를 id로 간접 참조하고 있다고 하자. 그렇다면 코드는 다음과 같을 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Entity
@Getter
public class Nation {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String name;

protected Nation() {
}

public Nation(final String name) {
this.name = name;
}
}


@Entity
@Getter
public class League {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String name;

@Column(nullable = false)
private Long nationId;

protected League() {
}

public League(final String name, final Long nationId) {
this.name = name;
this.nationId = nationId;
}
}

@Entity
@Getter
public class Player {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String name;

private Long leagueId;

protected Player() {
}
}

엔티티가 이런 상황에서 특정 국가에서 뛰고 있는 모든 선수의 이름, 그 선수의 소속 리그 이름, 그 선수 소속 리그의 국가 이름을 조회하려고 한다. 물리적인 관계가 있다면 JPQL fetch join으로 모든 엔티티의 정보를 가져와서 DTO에 매핑하는 방법을 사용할 수 있다. 하지만 간접 참조로 fetch join으로 최적화가 안된다. 이 문제를 해결하기 위해서는 일단 두 단계로 문제를 쪼개보자.

  1. 일단 원하는 정보가 여러 테이블에 흩어져 있기 때문에 이를 모아서 조회해야 한다. 즉 조인을 활용해야 한다.
  2. 조인 한 결과물을 담을 수 있는 객체가 필요하다. 우리의 문제 상황에서는 조회된 결과물이 한 엔티티에 담을 수 없기 때문에 DTO를 만들어서 담을 것이다.

JPQL에서 연관 관계가 없는 엔티티 조인하기

사실 SQL과 매우 비슷하다. join {엔티티} on {조건} 이런 형태로 조인을 해주면 된다. 우리의 사레를 JQPL로 표현하면 다음과 같다.

1
2
3
4
5
6
7
8
9
@Query(
"""
select {나중에 만들 DTO}
from Player p
join League l on p.leagueId = l.id
join Nation n on n.id = l.nationId
where n.name = :name
""")
List<PlayerDto> findDtoByNation(String name);

위의 예시는 각 조인을 명시적으로 표현해줬다. 하지만 조인을 명시적으로 표현하지 않아도 from 절에 명시하고 where 절에서 조인 조건을 명시해도 된다.

1
2
3
4
5
6
7
@Query(
"""
select {나중에 만들 DTO}
from Player p, League l, Nation n
where p.leagueId = l.id and n.id = l.nationId and n.name = :name
""")
List<PlayerDto> findDtoByNation(String name);

조인 종류

여기서 잠깐 JPQL의 조인 종류에 대해서 알아보자. 왜냐면 LEFT JOIN에 대해 잘 모르고 사용했다가 잘못된 테스트임에도 테스트가 통과되고 있었다.
일단 조인은 여러 테이블을 하나의 테이블로 합치는 행동이다. 이때 합칠 때 특정 조건을 걸어서 우리가 원하는 결과물만 받을 수 있게 된다.
조인의 종류는 이때 중요해지는데, 조인의 종류에 따라 결과물이 달라지기 때문이다.

먼저 INNER JOIN. 이 경우 두 테이블을 합칠 때 조인 조건이 맞는 데이터만 가져와서 합친다.
INNER JOIN이 아닌 LEFT JOIN, RIGHT JOIN, OUTER JOIN은 조인 조건을 만족하지 않는 데이터도 결과에 포함된다.
LEFT JOIN은 조인 조건에 맞는 데이터 + 왼쪽 테이블에서 조인 조건이 맞지 않는 데이터까지 포함해서 보여준다. (즉 왼쪽 테이블의 모든 데이터가 결과값으로 반환된다.)
RIGHT JOIN은 조인 조건에 맞는 데이터 + 오른쪽 테이블에서 조인 조건이 맞지 않는 데이터까지 포함해서 보여준다. (즉 오른쪽 테이블의 모든 데이터가 결과값에 포함된다.)
OUTER JOIN은 조인 조건에 맞는 데이터를 제외한 왼쪽 테이블과 오른쪽 테이블의 모든 데이터를 결과값을 보여준다. (즉 INNER JOIN의 여집합이겠다.)

JPQL에서 조인

JPQL에서는 위에서 언급한 INNER, LEFT JOIN을 지원한다. (RIGHT JOIN은 지원하지 않으므로 두 테이블의 적용 위치를 바꿔서 LEFT JOIN으로 하면 동일한 결과를 얻을 수 있다.)
그리고 FETCH JOIN이라는 것을 지원한다. FETCH JOIN은 연관관계가 있는 엔티티가 Lazy Loading일 때 쿼리 한번에 연관된 객체를 같이 조회해서 가져오는 방법이다. 연관관계가 있는 엔티티를 조회하는 경우 매우 중요한 개념이지만 현재는 연관관계가 없는 엔티티 조회하는 상황이므로 넘어가겠다.

결과값을 DTO로 받기

JQPL에서 결과값을 DTO로 받으려면 다음과 같다. SELECT new {패키지}.{DTO 클래스}(...생성자...) 이를 적용한 코드는 다음과 같다!

1
2
3
4
5
6
7
8
@Query(
"""
select new com.example.jointest.blogging.PlayerDto(p.id, p.name, l.name, n.name)
from Player p, League l, Nation n
where p.leagueId = l.id and n.id = l.nationId
and n.name = :name
""")
List<PlayerDto> findDtoByNation(String name);

JPQL에서 엔티티나 값 객체가 아닌 (@Entity@Embeddable이 안붙은 클래스) 경우는 이렇게 패키지 명을 써줘야 해서 매우 코드가 불편한 게 단점이다.
그래서 QueryDSL을 활용하면 더 깔끔한 코드를 작성할 수 있게 된다!!! 나중에 QueryDSL로 더 개선해보자!!

Share