인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

hjhello423님의 프로필 이미지
hjhello423

작성한 질문수

실전! Querydsl

fetch join 관련 질문 드립니다!!

작성

·

4.4K

6

안녕하세요 영한님

올려주시는 강의를 들으며 JPA와 queryDsl을 공부중 입니다.

좋은 강의 감사합니다 :)

이것저것 해보는 과정중에 궁금증이 생겨 질문 드립니다.

먼저 Team 과 Member 엔티티를 단순화 해 보았습니다.

@Entity

public class Team {

    @Id @GeneratedValue

    @Column(name = "team_id")

    private Long id;

    private String name;

    private Integer rank;

    @OneToMany(mappedBy = "team")

    List<Member> members = new ArrayList<>();

}

@Entity

public class Member {

    @Id

    @GeneratedValue

    @Column(name = "member_id")

    private Long id;

    private String username;

    private int age;

    @ManyToOne(fetch = FetchType.LAZY)

    @JoinColumn(name = "team_id")

    private Team team;

}

(질문 1)

Team과 Member는 서로 Lazy로 설정해 두었는데요 (Team에 rank 필드를 추가해 보았습니다.)

만약, Team을 조회 할때는 항상 Member가 필요 하다고 가정을 한다면(fetchJoin)

rank가 5이상인 Team을 조회를 하면서 (Team은 항상 조회 -> left join)

Team에 속한 Member의 나이가 20살 이상인 데이터를 즉시 조회 하고 싶다면 어떻게 작성 해야 할까요?

예를 들어 위의 조건은 sql로 아래와 같이 사용할수 있습니다.

select *

from team

left join member on (team.id = member.id and member.age > 20)

where team.rank > 5

위의 쿼리를 querydsl로 작성 한다면 아래와 같이 작성 가능 할것 같은데요 (fetchJoin 사용)

QTeam team = QTeam.team;

QMember member = QMember.member;

JPAQUERY<Team> query = queryFactory.selectFrom(team)

        .distinct();

query.leftJoin(team.members, member)

        .on(member.age.gt(20))

        .fetchJoin();

query.where(team.rank.gt(5));

하지만 이를 querydsl로 작성하면 아래와 같은 오류가 발생합니다.

witch-clause not allowed on fetched associations

찾아보니까 fetch 조인을 사용할 때는 on절을 사용할수 없다고 하더라구요..

이런경우에 어떤 방법으로 해결할 수 있을까요?

(질문 2)

아래 와 같이 조회를 했을때

QTeam team = QTeam.team;

QMember member = QMember.member;

JPAQUERY<Team> query = queryFactory.selectForm(team)

        .distinct();

query.leftJoin(team.members, member)

query.where(team.name.eq("AAA")

        .and(member.age > 20));

Team의 데이터를 꺼내보면 ( name = 'AAA' ) 조건이 잘 적용되어 팀 이름이 AAA인 팀만 조회가 됩니다.

문제는 Lazy설정된 Member를 get() 할 때 인데요

Team.members를 get() 하면 Lazy 로딩이기 때문에 select 쿼리가 각각 다시 발생하는데

이때 ( age > 20 ) 조건이 적용 되지 않고

AAA Team에 속한 모든 Member가 조회 되어 집니다.

같은 트렌젝션에서 수행이 된다면  Member을 get()할때  ( age > 20 ) 조건이 적용될꺼라 생각했는데

조건 적용없이 모든 Member를 조회 해서 당황 스럽네요

이부분은 왜 조건문이 적용되지 않은 결과가 get() 되는 건가요?

답변 7

8

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. 홍준님

가장 궁금해하시는 부분에 답을 드릴께요^^

결론부터 말씀드리면 원하는 결과를 엔티티로 한번에 조회하는 것은 불가능합니다.

원하는 결과를 한번에 얻으려면 fetch join을 작성해야 합니다.

그런데 fetch join은 조인 대상의 필터링을 지원하지 않습니다. 그래서 on 절 같은 것을 사용하면 예외가 발생합니다.

그러면 왜 안될까요?

왜냐하면 JPA의 엔티티 객체 그래프는 DB와 데이터 일관성을 유지해야 하기 때문입니다.

예를 들어서 DB에 데이터가 다음과 같이 있습니다.

team1 - memberA

team1 - memberB

team1 - memberC

그런데 조인 대상의 필터링을 제공해서 조회결과가 memberA, memberB만 조회하게 되면 JPA 애플리케이션은 다음과 같은 결과로 조회됩니다.

team1 - {memberA, memberB}

team1에서 회원 데이터를 찾으면 memberA, memberB만 반환되는 것이지요.

이렇게 되면 JPA 입장에서 DB와 데이터 일관성이 깨지고, 최악의 경우에 memberC가 DB에서 삭제될 수도 있습니다.

왜냐하면 JPA의 엔티티 객체 그래프는 DB와 데이터 일관성을 유지해야 하기 때문입니다! 잘 생각해보면 우리가 엔티티의 값을 변경하면 DB에 반영이 되어버리지요.

정리하면 JPA의 엔티티 데이터는 DB의 데이터와 일관성을 유지해야 합니다. 내가 임의로 데이터를 빼고 조회해버리면 DB에 해당 데이터가 없다고 판단하는 것과 똑같습니다.

그래서 엔티티를 사용할 때는 이 부분을 매우 조심해야 합니다! fetch join에서 제공하지 않는 이유도 이 데이터 일관성이 깨지기 때문에 제공하지 않습니다.

그러면 어떻게 해결할 수 있을까요? 바로 엔티티가 아닌 그냥 값으로 조회하면 됩니다. 엔티티는 객체 그래프를 유지하고 DB와 데이터 일관성을 유지합니다. 그런데 엔티티가 아닌 일반 값들은 그럴 필요가 없지요^^

일반 DB 조회하듯이 필요한 값을 나열해서 조회하면 됩니다.

예를 들어서 DTO 같은 것으로 조회하면 되는 것이지요.

물론 이 DTO안에 또 엔티티를 넣거나 그러지는 말고, 정말 일반 DB 조회하듯이 값을 풀어서 조회하면 됩니다.

그래서 코드로 보여드릴께요^^

package com.github.hjdeepsleep.toy.domain.mamber.dto;

import com.querydsl.core.annotations.QueryProjection;

import lombok.Data;

@Data

public class TeamMemberDto {

    private Long teamId;

    private String teamName;

    private Integer rank;

    private Long memberId;

    private String username;

    private int age;

    @QueryProjection

    public TeamMemberDto(Long teamId, String teamName, Integer rank, Long memberId, String username, int age) {

        this.teamId = teamId;

        this.teamName = teamName;

        this.rank = rank;

        this.memberId = memberId;

        this.username = username;

        this.age = age;

    }

}

@Test

public void findByDtoValue() {

    List<TeamMemberDto> result = queryFactory

            .select(new QTeamMemberDto(team.id, team.name, team.rank, member.id, member.username, member.age))

            .from(team)

            .leftJoin(team.members, member).on(member.age.goe(20))

            .where(team.rank.loe(2))

            .fetch();

    for (TeamMemberDto teamMemberDto : result) {

        System.out.println("teamMemberDto = " + teamMemberDto);

    }

}

결과

teamMemberDto = TeamMemberDto(teamId=1, teamName=team1, rank=1, memberId=6, username=member1-2, age=20)

teamMemberDto = TeamMemberDto(teamId=1, teamName=team1, rank=1, memberId=7, username=member1-3, age=30)

teamMemberDto = TeamMemberDto(teamId=2, teamName=team2, rank=2, memberId=9, username=member2-2, age=20)

teamMemberDto = TeamMemberDto(teamId=2, teamName=team2, rank=2, memberId=10, username=member2-3, age=30)

teamMemberDto = TeamMemberDto(teamId=4, teamName=team4, rank=1, memberId=null, username=null, age=0)

추가로 상황에 따라서 언제 엔티티로 조회하고, 언제 DTO로 직접 조회하는게 좋은지, 그리고 각각의 상황에 따른 성능 최적화는 어떻게하면 좋은지 자세히 설명하는 활용2편을 보시는 것을 꼭! 추천드립니다^^

도움이 되셨길 바래요.

2

hjhello423님의 프로필 이미지
hjhello423
질문자

답변 감사합니다!!

쉽게 설명해주셔서 궁금했던 점들이 한번에 해결 되었습니다 :)

지금 체크해 보니까 querydsl 수업이 75% 수강한 상태인데 다음주 까지 빨리 완강하고 활용 2편도 수강 해봐야겠네요

다시한번 친절한 답변 감사드립니다!! :)

1

김영한님의 프로필 이미지
김영한
지식공유자

ㅎㅎ 네 홍준님께서 그동안 많이 고민을 하신 덕분에 더 많이 이해하셨다 생각합니다.

활용2편을 들으면 궁금증을 해결할 수 있지만 먼저 기본기를 확실히 다져야 활용2편을 온전히 본인 것으로 만들 수 있습니다.

그래서 순서는 먼저 JPA 기본편을 들으시고 -> 활용1편 -> 활용2편 순으로 듣는 것을 권장드립니다.

물론 본인이 기본편 정도의 확실한 JPA 지식이 있고, 스프링 부트와 JPA로 실무 웹 애플리케이션 개발 경험이 있으면 활용2편으로 넘어가도 됩니다^^

화이팅!

1

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. 최홍준님

코드를 잘 보았습니다. 아~~ 정말 테스트로 파신 모습에 감동했어요^^!

우선 이 문제를 해결하려면 몇가지 기본 지식이 필요합니다.

홍준님이 어디까지 알고 있는지를 제가 알아야 어떤 부분을 찾아보시면 도움이 되는지 말씀드릴 수 있습니다^^

그래서 제가 드리는 질문에 자세히 답변을 먼저 부탁드릴께요.

1. fetch join과 일반 조인의 차이를 설명해주세요. 예제를 가지고, 실제 실행되는 JPQL, SQL, 그리고 애플리케이션에서 객체가 가지고 있는 데이터를 기반으로 비교해서 설명해주세요.

2. 제가 링크에 드린 글을 읽고 fetch join의 한계점을 정리해서 적어주세요. 그리고 한계점을 실제 테스트 코드로 작성해보세요.

3. 원하는 결과를 얻기위해 DB SQL을 작성해주세요. 자바 코드 없이 데이터베이스 쿼리로만 해당 데이터를 조회하는 쿼리를 작성해주세요.

4. 원하는 결과를 얻기 위해 JPQL과 엔티티 대신에 DTO로 한번에 바로 조회해보세요.

그럼 답변 기다릴께요.

0

hjhello423님의 프로필 이미지
hjhello423
질문자

join에 관한 코드는 너무 길어서 이전처럼 깃으로 공유 드립니다.

https://github.com/hjdeepsleep/toy/blob/master/src/test/java/com/github/hjdeepsleep/toy/domain/member/JoinTest2.java

1. fetch join과 일반 조인의 차이를 설명해주세요. 예제를 가지고, 실제 실행되는 JPQL, SQL, 그리고 애플리케이션에서 객체가 가지고 있는 데이터를 기반으로 비교해서 설명해주세요.

일반 join과 fetch join의 가장 큰 차이점은 엔티티를 조회 하는 시점입니다.

일반 join은 sql의 join과 동일 하게 동작 합니다.

fetch join은 jpql을 이용하여 조회 할 때, 연관된 엔티티를 한번에 같이 조회하는 기능입니다.

별칭을 줄수 없기 때문에 select, where, 서브쿼리에는 사용이 불가 합니다.

둘 이상의 컬렉션을 fetch join 할 수 없습니다.

1-1)

test3() 코드 작성죽 그동안 눈치 채지 못한 부분을 발견했습니다..

JoinTest2#before()에서 마지막에 em.flush(); em.clear();를 실행해 주었는데요

영속성 컨텍스트를 초기화 해주고 나니 test3()의 결과물이 제가 생각한데로 team.members에 age=10인 값만 들어갔습니다.

반면에 test4()에서는 쿼리 이전에 모든 team을 조회 하는 로직을 추가해 주었습니다. 그랬더니 test3()과 같은 쿼리에서도 서로 다른 결과가 확인 되었습니다.

 1차 캐시에 캐싱되어서 그런거 같은데 맞나요?

1-2)

test5()에서 projection을 이용해 team, member를 동시에 조회 하려고 해보았습니다.

tuple를 출력해본 결과 team과 별도로 member가 아래와 같이 조회 되었습니다.

[Team(id=1, name=team1, rank=1, members=[Member(id=5, username=member1-1, age=10), Member(id=6, username=member1-2, age=20), Member(id=7, username=member1-3, age=30)]), Member(id=5, username=member1-1, age=10)]

DB조회 결과에도 age=10인 데이터만 조회 되었습니다만, team을 출력 하는 과정에서 select member 쿼리를 각각 실행 하는것을 확인 했습니다.

team과 member를 함께 조회 하였는데도 team.members에 age=10이 아닌 모든 member가 포함되는 이유가 무엇인가요?

2. 제가 링크에 드린 글을 읽고 fetch join의 한계점을 정리해서 적어주세요. 그리고 한계점을 실제 테스트 코드로 작성해보세요.

* fetch join 대상은 별칭을 줄수 없다.

이로 인해 select, where, 서브쿼리에서 fetch join 대상을 사용 할 수 없다.

하이버네이트는 별칭을 지원하지만 연관 데이터의 수에 대한 무결성이 깨질수도 있다.

* 둘 이상의 컬렉션을 fetch 할 수 없다.

컬렉션x컬렉션의 곱이 만들어지므로 주의 해야 한다.

* 페이징 API 사용이 불가하다.

컬렉션(OneToMany)에 fetch join을 사용하면 페이징 api가 사용 불가하다.

2-1)

test3()에서 where조건에 fetch대상을 사용하였습니다.

fetch 대상을 on, where 조건등에 사용하면 안된다고 하셨는데요. 왜그럴까 생각해봤는데요.

일단 test3()에서 나온 결과물이 아래와 같았습니다.

Team(id=1, name=team1, rank=1, members=[Member(id=5, username=member1-1, age=10)])

Team(id=2, name=team2, rank=2, members=[Member(id=8, username=member2-1, age=10)])

영한님의 책에는 연관된 데이터 수가 달라져서 조심해야 한다고 적혀 있는데요. 아래 2가지를 생각해 봤습니다.

첫번째. 위 결과에서 team1을 save 한다고 할 때 id=5(age=10)인 멤버만 저장이 되고 나머지 멤버들이 삭제 처리 돼서.

두번째. 1-1 질문에서 처럼(em.clear) 영속성 컨텍스트의 영향을 받게 되는데 그래서 test4()를 작성해 보았습니다.

test4()에서는 test3()과 동일 작업을 하는데(query2), 쿼리 실행 이전에 모든 team을 조회 하는 로직을 추가해 보았습니다.(query1)

그랬더니 test3()과 같은 쿼리에서 서로 다른 결과값이 조회 되었습니다.

이 부분은 query1에서 조회한 결과가 캐싱 되었고, query2에서 조회 할 때 영향을 끼치는것 같습니다.

이렇게 같은 쿼리에서도 서로 다른 결과가 도출 될 수 있기 때문에 where조건에 사용하면 안되는것 같습니다. 맞을까요?

3. 원하는 결과를 얻기위해 DB SQL을 작성해주세요. 자바 코드 없이 데이터베이스 쿼리로만 해당 데이터를 조회하는 쿼리를 작성해주세요.

select team.*, member.*
from team 
left join member 
    on(team.id = member.team_id and member.age >= 20) 
where team.rank <= 2 ;


*결과

ID  	NAME  	RANK  	MEMBER_ID  	AGE  	USERNAME  	TEAM_ID  
1	team	1	6	20	member1-2	1
1	team	1	7	30	member1-3	1
2	team2	2	9	20	member2-2	2
2	team2	2	10	30	member2-3	2
4	team4	1	null	null	null	null

4. 원하는 결과를 얻기 위해 JPQL과 엔티티 대신에 DTO로 한번에 바로 조회해보세요.

우선 생성힌 dto의 소스 링크 공유 드립니다.

https://github.com/hjdeepsleep/toy/blob/master/src/main/java/com/github/hjdeepsleep/toy/domain/mamber/dto/TeamDto.java

https://github.com/hjdeepsleep/toy/blob/master/src/main/java/com/github/hjdeepsleep/toy/domain/mamber/dto/TeamDto2.java

4-1)

test6()에서 TeamDto를 이용해 조회 해 보았습니다.

이전 테스트에서도 계속해서 team.members에는 모든 member가 조회 되고 있습니다.

test6()에서 생선된 sql을 이용한 db조회 결과에서 age>=20인 memeber를 조회 했는데도 team.members에 조건에 맞지 않은 모든 멤버가 포함되고 있습니다.

test7() 에서는 team당 2개씩 조회되는 member를 dto에 리스트로 받아보려 했지만 실패 했습니다.

영한님께서 강의중에 Tuple는 querydsl에 종속 되기 때문에 서비스 계층으로 return 하지 말라고 하셨는데요.

이 이유때문에 dto를 사용하는것이라고 생각 들었습니다.(@QueryProjection으로 인한 종속은 일단 제외)

제가 얻고 싶었던 결과 값은 Team 객체를 리턴 받고 각 team.members에는 나이가 20, 30인 member만 포함 되는 결과였는데요

test6()에서와 같이 member쪽에 아무리 조건을 주어도 team.members에는 결국 모든 memeber가 포함되는것을 확인 했습니다.

원하는 member를 얻기 위해선 team이 아니라 dto를 리턴 받고 해당 dto에서 원하는 결과 값을 도출 해야 하는것인가요?

0

hjhello423님의 프로필 이미지
hjhello423
질문자

안녕하세요 영한님

궁금증이 풀리지 않아 다시 한번 질문 드립니다 ㅠ

강의를 보면서 공부해본 내용을 이용해 위에서 질문한 select 쿼리를 실행해 보려고 여러가지 방법으로 시도해 보았으나

모두 실패 했습니다..

우선 제가 테스트 해본 테스트 코드를 공유 드립니다.(너무 길어지는거 같아서 깃 주소로 공유 드립니다)

https://github.com/hjdeepsleep/toy/blob/master/src/test/java/com/github/hjdeepsleep/toy/domain/member/JoinTest.java

rank가 2등 이상인 팀과 함께,  해당 팀에 속한 20세 이상의 팀원을 조회 하려고 하였는데요

위의 테스트를 통해 결과적으로 저는 아래와 같은 결과를 Team엔티티를 통해 얻고자 하였습니다.

{
    "name": "team1",
    "rank": 1,
    "members": [
        {
            "name": "member1-2",
            "age": 20
        },
        {
            "name": "member1-3",
            "age": 30
        }
    ]
},
{
    "name": "team2",
    "rank": 2,
    "members": [
        {
            "name": "member2-2",
            "age": 20
        },
        {
            "name": "member2-3",
            "age": 30
        }
    ]
},
{
    "name": "team4",
    "rank": 1,
    "members": [    ]
},

위에서 공유드린 깃의 7개의 테스트 모두 결과적으로 실패 하였습니다.. ㅠ

Team 엔티티에 위 결과를 조회 하고 싶은데 어떤 방법으로 해결 해야 할까요?

test5에서는 tuple로 조회해 

[

    Team(id=1, name=team1, rank=1, 

        members=[

            Member(id=5, username=member1-1, age=10), 

            Member(id=6, username=member1-2, age=20),

            Member(id=7, username=member1-3, age=30)]

    ),

 Member(id=6, username=member1-2, age=20)]

이런 결과물을 얻었지만 나이가 30인 member1-3이 조회되지 않았습니다.

test6에서는 test5에서의 문제를 해결해 보려고 했지만  member를 list로 받는데 실패 했습니다..

이 문제를 어떻게 해결할 수 있나요??

0

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. 최홍준님 좋은 질문입니다^^

이제 한발 더 깊이 오셨네요 ㅎㅎ

다음 두 글을 자세히 읽어보시면 도움이 되실꺼에요^^

https://www.inflearn.com/questions/39516

https://www.inflearn.com/questions/15876

hjhello423님의 프로필 이미지
hjhello423

작성한 질문수

질문하기