
member1이 team1에 소속해 있다고 가정해보자.
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); //객체 그래프 탐색
System.out.println (team.getName()); //팀 엔티티 사용
회원 엔티티를 조회할 때 연관된 팀 엔티티도 함께 데이터베이스에서 조회하는것이 좋을까?
아니면 회원 엔티티만 조회해 두고 팀 엔티티는 실제 사용하는 시점에 데이터베이스에서 조회하는 것이 좋을까?
JPA는 개발자가 연관된 엔티티의 조회 시점을 선택할 수 있도록 다음 두 가지방법을 제공한다.
즉시 로딩
- 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
- em.find(Member.class, "member1")를 호출할 때 회원 엔티티와 연관된 팀 엔티티도 함께 조회한다.
- 설정 방법: @ManyToOne(fetch = FetchType.EAGER)
@Entity
public class Member {
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "team_id")
private Team team;
...
}
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); //객체 그래프 탐색
회원과 팀을 즉시 로딩으로 설정했다. 따라서 em.find (Member.class, "member1")로 회원을 조회하는 순간 실제 Team Entity도 같이 조회한다.
이때 회원과 팀 두 테이블을 조회해야 하므로 쿼리를 2번 실행할 것 같지만, 대부분의 JPA 구현체는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용한다. 여기서는 회원과 팀을 조인해서 쿼리 한 번으로 두 엔티티를 모두 조회한다.
SELECT
M.MEMBER ID AS MEMBER ID,
M.TEAM ID AS TEAM ID,
M.USERNAME AS USERNAME,
T.TEAM_ID AS TEAM_ID,
T.NAME AS NAME
FROM MEMBER M
LEFT OUTER JOIN TEAM T ON M. TEAM ID=T.TEAM ID
WHERE M.MEMBER ID='member1'
실행되는 SQL을 분석해보면 회원과 팀을 조인해서 쿼리 한 번으로 조회한 것을 알 수 있다.
member.getTeam()을 호출하면 이미 로딩된 팀1 엔티티를 반환한다.
지연 로딩
- 연관된 엔티티를 실제 사용할 때 조회한다.
- member.getTeam().getName()처럼 조회한 팀 엔티티를 실제 사용하는
시점에 JPA가 SQL을 호출해서 팀 엔티티를 조회한다. - 설정 방법: @ManyToOne(fetch = FetchType.LAZY)
@Entity
public class Member {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); //객체 그래프 탐색
team.getName(); //팀 객체 실제 사용
회원과 팀을 지연 로딩으로 설정했다. 따라서 em.find (Member.class, "member1")를 호출하면 회원만 조회하고 팀은 조회하지 않는다. 대신에 같이 조회한 회원의 team 멤버변수에 프록시 객체를 넣어둔다.
Team team = member.getTeam(); // 프록시 객체
반환된 팀 객체는 프록시 객체다. 이 프록시 객체는 실제 사용될 때까지 데이터 로딩을 미룬다. 그래서 지연 로딩이라 한다.
team.getName(); // 팀 객체 실제 사용
이처럼 실제 데이터가 필요한 순간이 되어서야 데이터베이스를 조회해서 프록시 객체를 초기화한다. em.find (Member.class, "member1") 호출 시 실행되는 SQL은 다음과 같다.
SELECT FROM MEMBER
WHERE MEMBER_ID='member1'
team.getName() 호출로 프록시 객체가 초기화되면서 실행되는 SQL은 다음과 같다.
SELECT FROM TEAM
WHERE TEAM ID='team1'
처음부터 연관된 엔티티를 모두 영속성 컨텍스트에 올려두는 것은 현실적이지 않고, 필요할 때마다 SQL을 실행해서 연관된 엔티티를 지연 로딩하는 것도 최적화 관점에서 보면 꼭 좋은 것만은 아니다.
예를 들어 대부분의 애플리케이션 로직에서 회원과 팀 엔티티를 같이 사용한다면 SQL 조인을 사용해서 회원과 팀 엔티티를 한 번에 조회하는 것이 더 효율적이다. 결국 연관된 엔티티를 즉시 로딩하는 것이 좋은지 아니면 실제 사용할 때까지 지연해서 로딩하는 것이 좋은지는 상황에 따라 다르다.
지연 로딩과 즉시 로딩을 간단히 정리하면 다음과 같다.
■ 지연 로딩(LAZY): 연관된 엔티티를 프록시로 조회한다. 프록시를 실제 사용할 때 초기화하면서 데이터베이스를 조회한다.
■ 즉시 로딩(EAGER): 연관된 엔티티를 즉시 조회한다. 하이버네이트는 가능하면 SQL 조인을 사용해서 한 번에 조회한다.
nullable로 JPA 조인 전략 설정하기
즉시 로딩 실행 SQL에서 JPA가 내부 조인(INNER JOIN)이 아닌 외부 조인 (LEFT OUTER JOIN)을 사용한 것을 유심히 봐야 한다.
현재 회원 테이블에 TEAM_ID 외래 키는 NULL 값을 허용하고 있다. 팀에 소속되지 않은 회원도 있기 때문이다.
TEAM_ID 외래키를 기준으로 , 내부 조인은 팀과 회원 테이블 각각의 TEAM_ID 외래키가 일치하는 레코드끼리만 조인하므로 팀에 소속하지 않은 회원과 팀을 내부 조인하면 팀은 물론이고 회원 데이터도 조회할 수 없다.
JPA는 이런 상황을 고려해서 외부 조인을 사용한다. 하지만 외부 조인보다 내부 조인이 성능과 최적화에서 더 유리하다. 그럼 내부 조인을 사용하려면 어떻게 해야 할까?
외래 키에 NOT NULL 제약 조건을 설정하면 값이 있는 것을 보장한다. 따라서 이때는 내부 조인만 사용해도 된다.
JPA에게도 이런 사실을 알려줘야 한다. 다음 코드처럼 @JoinColumn에 nullable = false을 설정해서 이 외래 키는 NULL 값을 허용하지 않는다고 알려주면 JPA는 외부 조인 대신에 내부 조인을 사용한다.
@Entity
public class Member {
//...
@ManyToOne (fetch = FetchType.EAGER)
@JoinColumn(name = "TEAM_ID", nullable = false)
private Team team;
//...
}
//nullable 설정에 따른 조인 전략
//@JoinColumn(nullable = true): NULL 허용(기본값), 외부 조인 사용
//@JoinColumn(nullable = false): NULL 허용하지 않음, 내부 조인 사용
}
//또는 다음처럼 @ManyToOne.optional = false로 설정해도 내부 조인을 사용한다.
@Entity
public class Member {
//...
@ManyToOne (fetch = FetchType.EAGER, optional = false)
@JoinColumn (name = "TEAM_ID")
private Team team;
//...
}
출처
김영한, "자바 ORM 표준 JPA 프로그래밍(2015)", 에이콘출판사
'Database' 카테고리의 다른 글
JPA - 영속성 전이 CASCADE, 고아객체 (0) | 2024.08.03 |
---|---|
JPA 고급 - 지연로딩의 활용 (0) | 2024.08.02 |
JPA - 프록시 개념과 활용 방법 (0) | 2024.07.28 |
연관관계 매핑 기초 (0) | 2024.02.04 |
엔티티 매핑 (1) | 2024.01.24 |

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!