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

인플언님의 프로필 이미지

작성한 질문수

실전! Querydsl

시작 - JPQL vs Querydsl

QueryDSL 에서 leftJoin & fetchJoin 후 lazy loading 이 되는 현상

23.03.08 02:32 작성

·

1.9K

·

수정됨

0

영한님 안녕하세요, QueryDSL 공부 중 막히는 부분이 있어 질문드립니다.

1:N 연관관계를 가지고 있는 두 엔티티 Team 과 Member 가 있을 때, QueryDSL 로 leftJoin & fetchJoin 으로 두 테이블을 조인하여 Member 목록을 조회하고 싶은데, 만약 Team 테이블에 FK 에 해당하는 row 가 존재하지 않는 경우에는 Member.team 에 그냥 null 이 들어있고 객체에 접근하더라도 추가적인 select 쿼리가 실행되지 않도록 하고 싶습니다. 그런데 제 바람과는 달리 Member.team 을 참조하는 시점에 lazy loading 이 되면서 select 쿼리가 실행되더라고요.

실제 코드를 바탕으로 설명해보겠습니다.

아래와 같이 1:N 연관 관계를 갖는 Team 과 Member 라는 엔티티가 있습니다.

@Table(name = "member")
@Entity
class Member(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int = 0,
    @Column(name = "team_id")
    var teamId: Long? = null,
    @Column(name = "name")
    var name: String? = null,
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id", insertable = false, updatable = false, foreignKey = ForeignKey(name = "none"))
    val team: Team? = null,
)

@Table(name = "team")
@Entity
class Team(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,
    @Column(name = "name")
    var name: String? = null,
)

여기서 아래의 코드로 left outer join 쿼리를 실행합니다.

val members = from(member)
    .leftJoin(member.team, team).fetchJoin()
    .fetch()

DB 는 아래와 같이 데이터가 저장되어 있습니다.

// team
+----+-------+
| id | name  |
+----+-------+
| 1  | team1 |
+----+-------+

// member
+----+---------+------+
| id | team_id | name |
+----+---------+------+
| 1  | 2       | John |
+----+---------+------+

그럼 저는 아래와 같은 Member 객체 하나로만 이루어진 List 를 얻을 수 있을 거라고 생각했고, team 변수에 접근할 때 select 쿼리 실행 없이 null 만을 반환할 것이라고 기대했습니다.

{
  "id": 1,
  "team_id": 2,
  "name": "John",
  "team": null
}

하지만 아래와같이 member 테이블을 lazy loading 하는 로그가 찍히네요.

Hibernate: insert into team (id, name) values (default, ?)
Hibernate: insert into member (id, name, team_id) values (default, ?, ?)
Hibernate: select member0_.id as id1_7_0_, team1_.id as id1_9_1_, member0_.name as name2_7_0_, member0_.team_id as team_id3_7_0_, team1_.name as name2_9_1_ from member member0_ left outer join team team1_ on member0_.team_id=team1_.id
Hibernate: select team0_.id as id1_9_0_, team0_.name as name2_9_0_ from team team0_ where team0_.id=?

그런데 만약 DB 의 데이터 중 member 의 team_id 만 1로 변경하니 쿼리 후 member.team 에 접근하더라도 아래와 같이 lazy loading 하는 로그가 찍히지 않았습니다.

Hibernate: insert into team (id, name) values (default, ?)
Hibernate: insert into member (id, name, team_id) values (default, ?, ?)
Hibernate: select member0_.id as id1_7_0_, team1_.id as id1_9_1_, member0_.name as name2_7_0_, member0_.team_id as team_id3_7_0_, team1_.name as name2_9_1_ from member member0_ left outer join team team1_ on member0_.team_id=team1_.id

테스트에 사용한 코드는 아래와 같습니다.

@DataJpaTest
@Import(MemberService::class) // MemberService.listMembers() 에서 QueryDsl 로 쿼리를 합니다.
class MyTest(
    private val sut: MemberService,
    private val em: EntityManager,
) : FunSpec(
    {
        beforeEach {
            val team = Team(name = "team1")
            em.persist(team)
            val member = Member(
                name = "John",
                teamId = team.id + 1, // 이것만 team.id 로 바꾸면 team 접근 시 select 로그가 찍히지 않습니다.
            )
            em.persist(member)
            em.clear()
        }

        test("my test") {
            val members = sut.listMembers()
            members.shouldNotBeEmpty()
            val team = members.first().team
            println(team)
        }
    },
)

어차피 조회한 엔티티에 변경을 가하지는 않을 것이라, Member 엔티티를 detach 시키고 team 에 접근하면 lazy loading 이 안될까 싶어서 해보았는데 여전히 lazy loading 이 되더라구요 ^^;

일단 @QueryProjection 을 붙인 별도의 DTO 를 정의해 아래와같이 쿼리하는 식으로 해결하려고 하는데 더 좋은 방법은 없을까요?

class MemberDto @QueryProjection constructor(
    val id: Int,
    val teamId: Long? = null,
    val name: String? = null,
    val teamName: String? = null,
)

@Service
@Transactional(readOnly = true)
class MemberService : QuerydslRepositorySupport(Member::class.java) {
    private val member = QMember.member
    private val team = QTeam.team

    fun listMembers(): List<MemberDto> {
        val members = from(member)
            .select(QMemberDto(member.id, member.teamId, member.name, team.name))
            .leftJoin(member.team, team)
            .fetch()

        return members
    }
}

감사합니다.

답변 1

0

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

2023. 03. 09. 22:18

안녕하세요. 인플언님

fetch join을 사용했기 때문에 이미 데이터를 다 불러왔습니다. 따라서 지연 로딩이 발생하지 않아야 합니다.

해당 코드를 자바로 작성해보시면 정상 동작하는 것을 확인하실 수 있을거에요(혹시 정상 동작하지 않는다면 댓글 남겨주세요)

이 문제는 아마도 코틀린 이슈인 것 같아요. 코틀린 lazy loading 이슈로 검색해보시면 원하시는 답을 찾으실 수 있을거에요^^

감사합니다.

인플언님의 프로필 이미지
인플언
질문자

2023. 06. 15. 00:06

영한님 안녕하세요, 답변 감사합니다!

답이 조금 늦었습니다.

말씀 주신대로 코틀린 lazy loading 으로 검색하여 이것저것 많이 살펴보았지만 거의 모든 글이, 코틀린은 기본적으로 final class 이기 때문에 Hibernate 가 프록시 객체를 만들지 못해서 lazy loading 이 되지 않고 eager loading 이 되는 이슈에 대한 글들이고, all-open 플러그인 적용시 바로 해결되는 경우였습니다. left outer join 을 하는 케이스는 아니었습니다.

제 경우는 lazy loading 이 되지 않고 eager loading 이 되는 이슈라기보다는,

fetch join 을 했는데도 값이 불러와지지 않고 lazy loading 이 되는 경우라(부모 엔티티가 없는 경우) 조금 다른 경우라고 생각이 드는데요,

혹시 생각하셨던 이슈는 다른 이슈일까요?

감사합니다.

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

2023. 06. 15. 16:35

안녕하세요. 인플언님

해당 코드를 자바로 작성해서 실행해보시면 정상 동작하는 것을 확인할 수 있을거에요.

만약 정상 동작하지 않는다면 코드를 올려주시면 도움을 드리겠습니다.

코틀린 관련해서 발생하는 이슈는 저도 잘 모르겠습니다.

감사합니다.