이 글은 Velog에서 이전한 글입니다. Velog 원문 보기
⭐️ 즉시 로딩과 지연 로딩
JPA에서는 데이터를 조회할 때 즉시 로딩(EAGER)과 지연 로딩(LAZY) 두 가지 방식이 있다. 이 두 가지 방식을 간단하게 설명하면 즉시 로딩은 데이터를 조회할 때 연관된 데이터까지 한 번에 불러오는 것이고, 지연 로딩은 필요한 시점에 연관된 데이터를 불러오는 것이라고 할 수 있다.

우선 위 사진의 상황을 가정하고 엔티티를 작성해보자.
Member Entity
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
Team Entity와 결과 쿼리
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
관계를 설정하자면 Member, Team Entity는 양방향 연관관계이고, 연관관계의 주인은 Member이다.
여기서 질문이 있다.
멤버를 조회할 때 팀도 함께 조회해야 할까?
단순히 멤버 정보만 사용하는 비즈니스 로직일 때는 멤버를 조회할 때 팀까지 Join해서 가지고 오게 되면 손해가 아닐까?
그래서 JPA는 지연 로딩이라는 옵션을 제공한다.
📌 지연 로딩(LAZY)
- @xxToxx(fetch = fetchType.LAZY)
지연 로딩 LAZY을 사용하서 프록시로 조회
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne(fetch = FetchType.LAZY) //proxy 객체로 조회(Member 클래스만 db에서 조회)
@JoinColumn(name = "TEAM_ID")//Fk, 외래키
private Team team; //연관관계의 주인
}
@ManyToOne에다가 fetch = FetchType.LAZY을 추가했다. fetch 타입을 LAZY로 하면 Member를 조회할 때는 Member만 DB에서 조회해야 한다.
코드를 실행해서 확인해보자.
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("hello");
member.setTeam(team)
em.persist(member);
em.flush(); //영속성 컨텍스트의 1차 캐시에 있는 정보를 db로 강제 전송
em.clear(); //영속성 컨텍스트 초기화
Member m = em.find(Member.class, member.getId());
System.out.println("m: " + m.getTeam().getClass());
tx.commit();
----------------------------------------------
## 결과 ##
Hibernate:
select
member0_.MEMBER_ID as member_i1_3_0_,
member0_.INSERT_MEMBER as insert_m2_3_0_,
member0_.createdDate as createdd3_3_0_,
member0_.UPDATE_MEMBER as update_m4_3_0_,
member0_.lastModifiedDate as lastmodi5_3_0_,
member0_.TEAM_ID as team_id7_3_0_,
member0_.USERNAME as username6_3_0_
from
Member member0_
where
member0_.MEMBER_ID=?
m: class hellojpa.Team$HibernateProxy$rNT34pzA
지연로딩으로 설정한 뒤 Member를 조회해보면 Member를 조회하는 쿼리만 나가고 Team을 조회하는 쿼리가 나가지 않았다.
그리고 m.getTeam().getClass()에서 Team은 프록시 객체인 것을 알 수 있다.
실제 Team을 사용하는 시점에 프록시 객체가 초기화되면서 DB에서 Team과 관련된 데이터를 가져온다.
지연 로딩으로 설정하면 연관된 것을 Proxy로 가져온다.

사진으로 보면 이해가 쉽다. 내부 메커니즘이 member1을 로딩 할 때 Team Entity 인스턴스는 지연 로딩으로 세팅이 되어 있어서 Proxy로 가져온 것이다.

- em.find(Member.class, 1L);로 멤버 객체를 가져옴.
- Member와 Team이 지연 로딩으로 설정되어 있으면 Team의 가짜 프록시 객체를 받아 놓는다.
- Member에서 Team을 가져와서 팀의 어떤 값을 실제 사용하는 시점에 DB 초기화
- 쿼리 발생
팀을 가져올 때가 아닌 팀에 있는 무언가를 실제 사용할 때 초기화되는 것이다.
전체 애플리케이션에서 대부분 멤버를 조회했을 때 멤버만 사용하고 팀을 거의 사용하지 않는다면 LAZY를 사용하는 것이 효율적이다.
그런데 반대로 비즈니스 로직의 거의 대부분이 멤버랑 팀을 같이 사용한다면?
📌 즉시 로딩(EAGER)
- @xxToxx(fetch = fetchType.EAGER)
즉시 로딩은 엔티티를 조회할 때 처음부터 연관관계에 있는 것을 모두 Join하는 것이다.
코드로 알아보자.
Member Entity
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID")//Fk, 외래키
private Team team; //연관관계의 주인
}
Team Entity와 결과 쿼리
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("hello");
member.setTeam(team)
em.persist(member);
em.flush(); //영속성 컨텍스트의 1차 캐시에 있는 정보를 db로 강제 전송
em.clear(); //영속성 컨텍스트 초기화
Member m = em.find(Member.class, member.getId());
System.out.println("m: " + m.getTeam().getClass());
tx.commit();
----------------------------------
## 결과 ##
Hibernate:
select
member0_.MEMBER_ID as member_i1_3_0_,
member0_.INSERT_MEMBER as insert_m2_3_0_,
member0_.createdDate as createdd3_3_0_,
member0_.UPDATE_MEMBER as update_m4_3_0_,
member0_.lastModifiedDate as lastmodi5_3_0_,
member0_.TEAM_ID as team_id7_3_0_,
member0_.USERNAME as username6_3_0_,
team1_.TEAM_ID as team_id1_5_1_,
team1_.INSERT_MEMBER as insert_m2_5_1_,
team1_.createdDate as createdd3_5_1_,
team1_.UPDATE_MEMBER as update_m4_5_1_,
team1_.lastModifiedDate as lastmodi5_5_1_,
team1_.name as name6_5_1_
from
Member member0_
left outer join
Team team1_
on member0_.TEAM_ID=team1_.TEAM_ID
where
member0_.MEMBER_ID=?
m: class hellojpa.Team
fetch를 EAGER로 설정하고 결과 쿼리를 보니까 LAZY와 다른 점이 보인다. 쿼리가 Member를 조회할 때 처음부터 Member와 Team을 Join한다. 이때 프록시 객체로 가져오는 것이 아닌 진짜 엔티티 객체로 가져온다.

애플리케이션 개발을 했는데 대부분의 경우 Member을 사용할 때 무조건 Team을 같이 사용한다면 즉시 로딩을 사용하는게 유리해보인다.
하지만 실무에서는 즉시 로딩을 사용하면 안된다.
왜? 한 번에 조인 쿼리로 가져오면 편하고 좋을 거 같은데 왜 사용하면 안될까?
- 즉시 로딩을 사용하면 전혀 예상하지 못한 SQL이 막 나간다.
- 위의 예시처럼 Member, Team 테이블만 있다면 괜찮지만 테이블이 10개, 100개가 된다면 find할 때 연관관계로 이어져있는 모든 테이블이 join된다.
- 즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
- em.find는 Pk를 찍어서 가져오는 것이기 때문에 JPA가 내부적으로 최적화를 할 수가 있다. 그런데 JPQL은 SQL로 번역이 되기에 최적화가 안된다.
@ManyToOne,@OneToOne은 기본이 즉시 로딩(EAGER) -> 지연 로딩(LAZY)로 설정@OneToMany,@ManyToMany는 기본이 지연 로딩(LAZY)
게시글 속 자료는 모두 위 강의 속 자료를 사용했습니다.
'Backend > JPA' 카테고리의 다른 글
| ✏️ JPA 값 타입 (0) | 2026.07.22 |
|---|---|
| ✏️ JPA 영속성 전이(CASCADE), 고아 객체 (0) | 2026.07.22 |
| ✏️ JPA 프록시(Proxy) (0) | 2026.07.22 |
| ✏️ JPA 고급 매핑 (0) | 2026.07.22 |
| ✏️ JPA 연관 관계 (2) (0) | 2026.07.21 |