티스토리 뷰

 

XXXtoOne 일 경우 기본 FetchType은 EAGER 이고 XXXtoMany 일 경우 FetchType은 LAZY이다.

 

비즈니스 로직에서 단순히 Member 정보만 사용하는데 Team을 함께 조회하면 아무리 연관관계가 걸려있다해도 손해이다.

JPA는 이 문제를 지연로딩(LAZY)을 사용해서 해결한다.

 

즉시로딩 부터 살펴보자.

 

즉시로딩(FetchType.EAGER)

Member.class

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn
    private Team team;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Team getTeam() {
        return team;
    }

    public void changeTeam(Team team) {
        this.team = team;
        this.team.getMembers().add(this);
    }
}

 

Team.class

@Entity
public class Team {

    @Id @GeneratedValue
    private Long id;
    private String name;

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

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Member> getMembers() {
        return members;
    }
}

 

TestCode

@Slf4j
@SpringBootTest
public class LoadingTest {

    @Autowired
    EntityManager em;

    @Test
    @Transactional
    void loadingTest() {
        Team team = new Team();
        team.setName("teamA");
        em.persist(team);

        Member member = new Member();
        member.setName("memberA");
        member.changeTeam(team);
        em.persist(member);

        em.flush();
        em.clear();

        Member findMember = em.find(Member.class, member.getId());

        log.info("teamClass: {}",findMember.getTeam().getClass());
        log.info("teamName: {}", findMember.getTeam().getName());
    }
}

 

실행결과

select
        member0_.id as id1_0_0_,
        member0_.name as name2_0_0_,
        member0_.team_id as team_id3_0_0_,
        team1_.id as id1_1_1_,
        team1_.name as name2_1_1_ 
    from
        member member0_ 
    left outer join
        team team1_ 
            on member0_.team_id=team1_.id 
    where
        member0_.id=?
        
teamClass: class com.example.jpatest.Team
teamName: teamA
  • Team 객체는 실제 객체이다
  • 즉시로딩을 사용하면 조인을 사용하여 SQL 한번에 함께 조회한다

 

지연로딩(FetchType.LAZY)

위 코드에서 Member.class의 FetchType만 LAZY로 수정해준다.

 

Member.class

@Entity
public class Member {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn
    private Team team;
    ...
}

 

실행결과

select
        member0_.id as id1_0_0_,
        member0_.name as name2_0_0_,
        member0_.team_id as team_id3_0_0_ 
    from
        member member0_ 
    where
        member0_.id=?
        
teamClass: class com.example.jpatest.Team$HibernateProxy$voVNNFTP
  
select
        team0_.id as id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        team team0_ 
    where
        team0_.id=?
        
teamName: teamA
  • Team 객체는 프록시 객체이다
  • Team 객체를 실제로 사용할때, Team 객체가 조회된다 -> Member와 Team을 같이 사용한다면 SELECT 쿼리가 따로따로 두번 나간다.

지연로딩의 매커니즘

  • 로딩되는 시점에 LAZY 로딩 설정이 되어있는 Team 엔티티는 프록시 객체로 가져온다
  • 후에 실제 객체를 사용하는 시점에(Team을 사용하는 시점) 초기화 된다 -> 쿼리가 나간다

 

오 그럼 Member와 Team을 모두 사용할때는 즉시로딩을 사용하면 되겠네?

하지만 즉시로딩은 예상하지 못한 SQL이 발생하므로 실무에선 지연로딩을 권장한다.

 

즉시로딩 문제점

  • 즉시로딩을 적용하면 예상하지 못한 SQL이 발생한다
    • @ManyToOne이 5개 있는데, 전부 EAGER로 설정되어 있으면 조인이 5번 발생한다 -> 실무에선 테이블 수가 더 많다
  • 즉시로딩은 JPQL에서 N+1 문제를 일으킨다
    • 실무에선 복잡한 쿼리를 풀기위해 JPQL을 자주 사용한다
    • em.find()는 PK를 정해놓고 DB에서 가져오기 때문에 JPA 구현체 내부에서 최적화를 할 수 있다(한방쿼리)
    • 하지만, JPQL에선 입력 받은 query string이 그대로 SQL로 변환된다.
      • "select m from Member m" 쿼리는 Member만 조회한다
      • Member를 쭉 조회하다가 Team이 즉시로딩이면 Member 반환 시점에 다 조회가 되어야한다
      • 따라서 Member를 다 가져오고 Member와 연관된 Team을 다시 다 가져온다
    • 코드로 멤버가 2명, 팀도 2개인 상태에서 모든 멤버를 조회해보자

JPQL 즉시로딩

TestCode

@Test
    @Transactional
    void jpql() {
        Team team1 = new Team();
        team1.setName("teamA");
        em.persist(team1);

        Team team2 = new Team();
        team2.setName("teamB");
        em.persist(team2);

        Member member1 = new Member();
        member1.setName("memberA");
        member1.changeTeam(team1);
        em.persist(member1);

        Member member2 = new Member();
        member2.setName("memberB");
        member2.changeTeam(team2);
        em.persist(member2);

        em.flush();
        em.clear();

        List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList();
    }

 

실행결과

select
        member0_.id as id1_0_,
        member0_.name as name2_0_,
        member0_.team_id as team_id3_0_ 
    from
        member member0_
        
select
        team0_.id as id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        team team0_ 
    where
        team0_.id=?

select
        team0_.id as id1_1_0_,
        team0_.name as name2_1_0_ 
    from
        team team0_ 
    where
        team0_.id=?
  • 먼저 Member를 조회해서 가져온다
  • 그리고 나서 Member들의 Team을 채우기 위해 각각 쿼리를 날려서 가져온다
  • 멤버가 수천, 수만명이고 각각 팀이 다를 경우 쿼리양은 엄청날 것이다...

위처럼 N+1의 문제쿼리를 1개 날렸는데, 그것 때문에 추가 쿼리가 N개 나간다는 의미이다.

 

결론

  • LAZY 로딩 전략을 기본으로 가져가자
    • 그럼 Member, Team을 다 사용할때는 Team 객체를 실제 사용할때마다 매번 쿼리를 날려서 조회해야되나?
    • 이런 경우를 위해서 JPQL의 fetch join 이나 엔티티 그래프 기능이 존재한다 -> 대부분 fetch join으로 해결한다

 

 

 

참고

자바 ORM 표준 JPA 프로그래밍(김영한)

링크
최근에 올라온 글
글 보관함
«   2025/08   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31