[Java/JPA] JPQL - 경로 표현식

daheenamic·2025년 12월 4일

Java

목록 보기
46/48

경로 표현식

경로 표현식은 점(.)을 찍어서 객체 그래프를 탐색하는 것이다.

select m.username        // 상태 필드
from Member m
join m.team t            // 단일 값 연관 필드
join m.orders o          // 컬렉션 값 연관 필드
where t.name = '팀A'

객체 입장에서는 단순히 .으로 접근하는 것처럼 보이지만, 내부적으로는 전혀 다르게 동작한다. 이 차이를 이해하지 못하면 성능 문제가 발생할 수 있다.


경로 표현식의 3가지 종류

1. 상태 필드 (State Field)

단순히 값을 저장하기 위한 필드다.

m.username  // String
m.age       // Integer
t.name      // String

엔티티의 기본 속성들이 여기에 해당한다.

2. 단일 값 연관 필드 (Single-Valued Association Field)

@ManyToOne, @OneToOne으로 연결된 엔티티다. 대상이 단일 엔티티다.

m.team      // Member -> Team (ManyToOne)
o.member    // Order -> Member (ManyToOne)

3. 컬렉션 값 연관 필드 (Collection-Valued Association Field)

@OneToMany, @ManyToMany로 연결된 컬렉션이다. 대상이 컬렉션이다.

t.members   // Team -> List<Member> (OneToMany)
m.orders    // Member -> List<Order> (OneToMany)

경로 표현식의 특징과 동작

각 경로 표현식은 전혀 다르게 동작한다.

1. 상태 필드 - 경로 탐색의 끝

// JPQL
select m.username, m.age from Member m

// 실행되는 SQL
select m.username, m.age from Member m

상태 필드는 경로 탐색의 끝이다. 더 이상 .을 찍어서 탐색할 수 없다.

m.username.length()  // 불가능! username은 상태 필드라 끝

JPQL과 SQL이 거의 동일하게 나간다. 조인도 발생하지 않는다.

2. 단일 값 연관 필드 - 묵시적 조인 발생

// JPQL
select o.member from Order o

// 실행되는 SQL
select m.* 
from Orders o 
inner join Member m on o.member_id = m.id

문제는 여기서 시작된다. 코드에는 join이라는 단어가 없지만, 내부적으로 INNER JOIN이 발생한다.

더 위험한 건 계속 탐색이 가능하다는 점이다.

// JPQL
select o.member.team from Order o

// 실행되는 SQL
select t.* 
from Orders o 
inner join Member m on o.member_id = m.id
inner join Team t on m.team_id = t.id

.을 하나 더 찍었을 뿐인데 조인이 2번 발생한다. o.member.team.department.company처럼 계속 이어지면 조인이 계속 늘어난다.

3. 컬렉션 값 연관 필드 - 조인 발생, 탐색 불가

// JPQL - 컬렉션 자체는 가져올 수 있음
select t.members from Team t

// 실행되는 SQL
select m.* 
from Team t 
inner join Member m on t.id = m.team_id

컬렉션도 묵시적 조인이 발생한다. 하지만 더 이상 탐색할 수 없다.

// 불가능!
select t.members.username from Team t  // 에러 발생

컬렉션에서는 .username 같은 탐색이 안 된다. 컬렉션은 여러 개의 엔티티를 담고 있기 때문이다.

대신 .size는 가능하다.

// 팀별 회원 수 조회
select t.name, t.members.size from Team t

컬렉션에서 탐색하려면?

컬렉션 값 연관 필드는 경로 탐색의 끝이다. 더 탐색하려면 명시적 조인으로 별칭을 얻어야 한다.

// 실패 - 컬렉션은 탐색 불가
select t.members.username from Team t

// 성공 - 명시적 조인으로 별칭 생성
select m.username from Team t join t.members m

join t.members m으로 별칭 m을 얻으면, 이제 m.username처럼 탐색할 수 있다.


묵시적 조인 vs 명시적 조인

명시적 조인

join 키워드를 직접 사용하는 방식이다.

select m from Member m join m.team t

코드에서 join이 명확히 보인다.

묵시적 조인

경로 표현식에 의해 자동으로 조인이 발생하는 방식이다.

select m.team from Member m  // join 키워드 없지만 조인 발생

코드에는 join이 없지만 내부적으로 INNER JOIN이 실행된다.


경로 표현식 예제

실제로 어떻게 동작하는지 예제를 보자.

// 예제 1: 성공 - 단일 값 연관 경로
select o.member.team from Order o
// JOIN이 2번 발생 (Order -> Member -> Team)

// 예제 2: 성공 - 컬렉션 크기
select t.members.size from Team t
// JOIN 1번 발생, 크기만 계산

// 예제 3: 실패 - 컬렉션은 탐색 불가
select t.members.username from Team t
// 에러! 컬렉션에서는 .username 불가능

// 예제 4: 성공 - 명시적 조인으로 해결
select m.username from Team t join t.members m
// 명시적 조인으로 별칭 m 획득 후 탐색

묵시적 조인의 문제점

1. Join이 숨어있다

// 코드만 보면 단순 조회 같지만
String jpql = "select o.member.team.name from Order o";

// 실제로는 조인이 2번 발생
select t.name
from Orders o
inner join Member m on o.member_id = m.id
inner join Team t on m.team_id = t.id

쿼리가 복잡해질수록 어디서 몇 번 조인이 발생하는지 파악하기 어렵다.

2. 항상 내부 조인만 가능

묵시적 조인은 무조건 INNER JOIN이다. LEFT JOIN 같은 외부 조인이 필요하면 명시적 조인을 써야 한다.

// 묵시적 조인 - 항상 INNER JOIN
select m.team from Member m

// 외부 조인이 필요하면 명시적으로
select m from Member m left join m.team t

3. 성능 튜닝 포인트를 놓친다

// 이 쿼리에 조인이 몇 번 발생할까?
String jpql = "select o.member.team.name, " +
              "o.product.category.name, " +
              "o.member.address.city " +
              "from Order o";

코드만 봐서는 알기 어렵다. 조인은 SQL 튜닝의 핵심 포인트인데, 묵시적 조인은 이를 한눈에 파악하기 어렵게 만든다.


실무 권장사항

1. 묵시적 조인 사용 금지

// 나쁜 예 - 묵시적 조인
select o.member.team from Order o

// 좋은 예 - 명시적 조인
select t from Order o 
join o.member m 
join m.team t

명시적 조인을 쓰면:

  • 어디서 조인이 발생하는지 명확하다.
  • 성능 튜닝 포인트를 쉽게 찾는다.
  • 외부 조인도 자유롭게 사용할 수 있다.

2. 컬렉션은 무조건 명시적 조인

// 나쁜 예
select t.members from Team t  // 묵시적 조인, 더 탐색 불가

// 좋은 예
select m from Team t join t.members m  // 명시적 조인, 탐색 가능

3. 경로 탐색은 최소화

// 나쁜 예
select t.members from Team t  // 묵시적 조인, 더 탐색 불가

// 좋은 예
select m from Team t join t.members m  // 명시적 조인, 탐색 가능

후자가 코드는 길지만, 조인 관계가 명확하고 튜닝하기 쉽다.


실무 체크리스트

코드 리뷰 시 다음을 확인하자:

  • JPQL에 .이 2개 이상 연속되는가? → 묵시적 조인 의심
  • join 키워드 없이 연관 엔티티를 조회하는가? → 명시적 조인으로 변경
  • 컬렉션에서 직접 탐색하려고 하는가? → 별칭을 통한 명시적 조인 필요
  • 외부 조인이 필요한 상황인가? → 묵시적 조인은 불가능

QueryDSL을 배우게 되면 이 모든 내용을 타입 안전하게, 더 깔끔하게 작성하는 방법을 익힐 수 있다. QueryDSL을 쓰면 묵시적 조인 같은 실수를 원천적으로 방지할 수 있다고 한다.

0개의 댓글