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

인플언님의 프로필 이미지
인플언

작성한 질문수

실전! Querydsl

시작 - JPQL vs Querydsl

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

작성

·

2K

·

수정됨

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

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

안녕하세요. 인플언님

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

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

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

감사합니다.

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

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

답이 조금 늦었습니다.

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

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

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

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

감사합니다.

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

안녕하세요. 인플언님

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

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

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

감사합니다.

인플언님의 프로필 이미지
인플언

작성한 질문수

질문하기