0. 문제상황 🤔 

Spring Data JPA를 사용하다가 다음과 같은 상황이 있었습니다. A 라는 엔티티가 B 라는 엔티티를 필드로 가지고 있는데 A를 찾기 위해 B 조건을 걸고 싶었던 것이죠. 원래 같았으면 findByB(B b) 와 같은 메서드를 Spring Data JPA를 사용해 추가했을 텐데 제약사항때문에 findByBId(Long id)와 같이 B의 Id 값을 통해 불러오고 싶었습니다. 근데 왠걸 findByBId(Long id)를 호출하고 쿼리를 확인해보았더니 join문이 끼어있더군요. 이런 상황이 왜 벌어졌는지 알아보았고, 이를 알아본 내용을 정리하려합니다.


1. 테스트 해보기❗️

우선 상황을 가정해보도록 하겠습니다. 아래와 같이 Member 엔티티와 Team 엔티티가 있다고 생각해봅시다.

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    Team team;

    String name;
}

@Entity
public class Team {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;

    @OneToMany(mappedBy = "team")
    List<Member> members = new ArrayList<>();

    String name;
}

그리고 Spring Data JPA를 사용한 Repository 코드는 아래와 같습니다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByTeam(Team team);
    List<Member> findByTeamId(Long teamId);
}

처음 코드를 작성할때는 findByTeam, findByTeamId 모두 같은 쿼리를 만들어낼 것이라고 생각했습니다. 다음과 같은 쿼리를 말이죠.

 

select * from member where member.team_id = ?

 

그런데 실제로 동작시켜보니 각각 아래와 같은 쿼리가 나갔습니다.

findByTeam
findByTeamId

위 결과를 보면 findByTeam의 경우 예상과 같은 쿼리가 나갔지만 findByTeamId의 경우 left outer join이 걸리고 where절 조건에 team_id = ? 가 있는 것을 확인할 수 있습니다.

 

left outer join을 했다는건 member 테이블만이 아니라 team 테이블까지 봐야했다는거고.. member 테이블에는 이미 외래키로 team_id 필드가 있을건데 왜 join을 했지.. 라는 생각이 들었습니다.


2. 해결 😎

열심히 구글링하고 Spring Data JPA 문서를 보고나니 이유를 알게 되었습니다. 특히 Spring Data JPA 문서에 Property Expressions 부분을 보고 알게 되었는데요. 내용은 아래와 같습니다.

나름대로 요약하자면 내용은 다음과 같습니다. Spring Data JPA를 활용하여 Repository 메서드를 만들기 위해서는 entity에 있는 property 네이밍을 그대로 사용해야합니다. findByTeam 이라는 메서드는 Member entity에 team 이라는 property가 있었기 때문에 예상대로 잘 동작했던 것이죠. 하지만 이외에도 추가적인 규칙이 있습니다. findByTeamId 라고 했을 경우 Member entity에는 teamId라는 Property가 없지만 카멜케이스 규칙에 따라 Member의 property 중 team을 타고가서 id라는 필드를 이용해 찾게 됩니다. 이 과정에서 join이 필요한 것이죠. 또한 where 절 조건에 team_id = ? 라는 조건이 있는 이유도 여기에 있습니다. 

 

그렇다면 이를 응용해서 repository에 다음과 같은 메서드도 추가해서 잘 동작하는지 확인해보겠습니다!

public interface MemberRepository extends JpaRepository<Member, Long> {
	...
    
    List<Member> findByTeamName(String teamName); // team.name을 통해 찾는지 확인해보자!
}

위 메서드를 실행시켜보면 아래와 같은 쿼리를 만들어내는 것을 알 수 있습니다.

findByTeamName

역시 위에서 소개한대로 Member entity에는 teamName이라는 필드가 없기 때문에 team 테이블과 join을 하였고 그 결과에 where team.name = ? 를 추가하여 결과를 얻어내는 모습을 확인할 수 있습니다.

 

그렇다면 제가 처음에 소개한 것처럼 findByTeamId로 꼭 하고싶다면 어떻게 해야할까요? 그때는 @Query 어노테이션을 통해 JPQL을 직접 써주면 됩니다. 아래와 같이 해결이 가능하죠!

    @Query("select m from Member m where m.team.id = :teamId")
    List<Member> findByTeamId(Long teamId);

이렇게 직접 JPQL을 써주게 되면 아래와 같은 쿼리를 날리게 되고 m.team = team 이나 m.team.id  = teamId 나 결국 JPQL에서는 같기때문에 원하는 결과를 얻을 수 있게 됩니다.

JPQL


3. 결론 💨

1. Spring Data JPA를 활용해 Repository 메서드를 만들려면 entity에 있는 property 이름을 가지고 만들자. 그렇지 않으면 카멜케이스 규칙에 따라 Property traversal을 하게되고, 이 때 예상치 못한 조인이 발생할 수 있다.

2. 필요하다면 @Query를 사용해 직접 JPQL을 써주자.


 

4. 레퍼런스📚

 

Spring Data JPA - Reference Documentation

Example 121. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

 

[Spring Data JPA] findByXXXId 는 불필요한 join을 유발한다

프로젝트에서 JPA를 사용하던 도중 이상한 부분을 발견했습니다. 엔티티 끼리 연관관계가 있을 때 어떤 곳에서는 findByXXX 형태의 쿼리 메서드를, 어떤 곳에서는 findByXXXId 형태의 쿼리 메서드를 사

velog.io

 

외래키에 해당하는 컬럼으로 조회하고 싶을때는 어떻게 하면 될까요??? - 인프런 | 질문 & 답변

특정 팀 소속인 사용자 목록을 얻고 싶다면Member Repository에서 아래와 같이 만들어서findByTeam(Team team, Pageable pageable);Team 객체에 해당하는 Team의 id 값을 담아서 조회 하고 있는데 이러한 방식 맞나

www.inflearn.com

 

반응형

'Spring > JPA' 카테고리의 다른 글

[Spring & JPA] N + 1 문제에 대하여.  (1) 2023.02.01
복사했습니다!