JPA 공부를 하다가 아래와 같은 오류를 만났다.
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.study.chapter05.entity.Ch05Team.memberList, could not initialize proxy - no Session
팀과 회원 객체의 양방향 연관관계 테스트를 하다가 만난 오류인데 팀을 조회할때 팀에 소속된 회원 리스트까지 함께 조회를 하는 과정에서 발생하였다.
// 팀 엔티티
@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "Ch05_TEAM")
public class Ch05Team {
...
@OneToMany(mappedBy = "team") // 양방향 연관관계 설정
private List<Ch05Member> memberList = new ArrayList<>();
...
}
해결 방법을 찾아보니 지연 로딩에 관한 사전 학습이 먼저 필요한 듯 했다. 그래서 오늘은 JPA의 프록시와 지연로딩의 기초적인 내용을 공부하려고 한다.
프록시
엔티티를 조회할 때 연관된 엔티티들이 항상 사용되는 것은 아니다 .
예를 들어 회원 객체를 조회하면 연관관계 매핑을 한 팀 정보도 함께 조회된다.
아래 코드를 예시로 보자
<회원 클래스>
package com.study.chapter05.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "Ch05_MEMBER")
public class Ch05Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Ch05Team team; // 연관관계 매핑
public Ch05Member(String id, String username) {
this.id = id;
this.username = username;
}
}
<회원 정보 조회>
@Test
@DisplayName("조회")
void selectTest() {
Optional<Ch05Member> optionalCh05Member = memberRepository.findById("member1");
if (optionalCh05Member.isPresent()) {
Ch05Member member = optionalCh05Member.get();
} else {
log.debug("조회 실패!, member is null");
}
}
<조회 결과>
조회된 member 객체 안에 팀 정보도 함께 조회되어 담겨 있다.
그런데 이 team이라는 객체는 비즈니스 로직에 따라 사용될 수도, 사용되지 않을 수도 있다.
member객체를 조회한 후에 팀에 대한 정보도 사용한다면 메소드내에서 member.getTeam() 을 호출할 것이다. 하지만 member 정보만 사용하고 team은 사용하지 않는다면 연관관계 매핑이 되어있다고해서 데이터베이스에서 함께 조회해 두는 것은 효율적이지 않다.
JPA는 이런 문제를 해결하려고 엔티티가 실제 사용될 때까지 데이터베이스 조회를 지연하는 방법을 제공하는데 이것을 지연 로딩이라고 한다.
그리고 지연 로딩 기능을 사용하려면 실제 엔티티 객체 대신에 데이터베이스 조회를 지연할 수 있는 가짜 객체가 필요한데 이것을 프록시 객체라고 한다.
즉시 로딩과 지연 로딩
member1이 team1에 소속해 있다고 가정해보자.
Optional<Ch05Member> optionalCh05Member = memberRepository.findById("member1");
if (optionalCh05Member.isPresent()) {
Ch05Member member = optionalCh05Member.get(); // 회원 조회
Ch05Team team = member.getTeam(); // 팀 조회
}
}
회원 엔티티를 조회할때 연관된 팀 엔티티도 함께 조회해두는 것이 좋을까 아니면 실제 사용되는 시점에 데이터베이스에서 조회하는 것이 좋을까? JPA에서는 개발자가 조회 시점을 선택할 수 있도록 다음 두가지 방법을 제공한다.
> 즉시 로딩(EAGER LOADING): 엔티티를 조회할 때 연관된 엔티티도 함께 조회한다.
- 설정 방법: @ManyToOne(fetch = FetchType.EAGER)
- 연관된 객체까지 조회하려고 한번더 쿼리를 실행하기 보다는 JPA에서는 즉시 로딩을 최적화하기 위해 가능하면 조인 쿼리를 사용.
- @ManyToOne, @OneToOne 과 같이 연관된 엔티티가 하나인 경우에 디폴트 패치 전략이다.
> 지연 로딩(LAZY LOADING): 연관된 엔티티를 실제 사용하는 시점에 데이터베이스에서 조회한다.
- 설정 방법: @ManyToOne(fetch = FetchType.LAZY)
- 연관된 객체에 프록시 객체를 넣어둠. 실제 데이터가 필요한 순간이 되어서야 데이터베이스를 조회해서 프록시 객체를 초기화함.
- @OneToMany, @ManyToMany와 같이 연관된 엔티티가 여러개일 때 디폴트 패치 전략이다.
처음부터 연관된 모든 엔티티를 조회해서 영속성 컨텍스트에 올려놓는 것도, 필요할 때마다 쿼리를 매번 실행해서 연관된 엔티티를 지연로딩하는 것도 꼭 좋은 것만은 아니다.
🙆🏼♀️ 상황에 따라 최적화 되는 방법이 다르기 때문에 개발자가 선택하면 된다. 🙆🏻♀️
🌈다시 이슈로 돌아가서⭐️
위 에러에 대한 내용으로 다시 돌아가보자.
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.study.chapter05.entity.Ch05Team.memberList, could not initialize proxy - no Session
@OneToMany의 디폴트 패치 전략은 지연로딩이다. 따라서 member 조회시 team은 프록시 객체가 들어있었을 것이다.
프록시 객체는 실제 사용될 때 데이터베이스에서 조회하여 엔티티 객체를 생성하는데 이를 프록시 초기화라고 한다. 프록시 초기화는 영속성 컨텍스트의 도움을 받아야 가능하다. 그런데 이미 member를 조회한 후에는 영속성 컨텍스트를 종료한 후이므로 도움을 받을 수가 없어 LazyInitializationException 예외를 발생시킨다.
간단한 방법은 지연로딩을 즉시로딩으로 바꾸는 것이다.
그렇게 되면 연관된 객체들도 모두 한번에 조회된 후에 영속성 컨텍스트가 종료되므로 LazyInitializationException에러를 만나지 않게 된다.
하지만 연관된 엔티티가 컬렉션이라면 로딩할 때 비용이 많이 들고 잘못하면 너무 많은 데이터를 로딩하게 될 수도 있다.
때문에 단지 예외를 피하기 위해 즉시로딩으로 바꿔버리는 것은 위험하며 JPA를 사용할 때는 대부분에 상황에서 지연로딩을 활용하되 꼭 필요한 부분에서만 즉시로딩을 사용하여 최적화하는 것이 권고되는 사항이다.
@Transactional을 붙이자
JpaRepository를 사용하면 공통으로 제공하는 메소드(등록,수정,삭제)에 @Transactional 처리가 되어있다.
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
}
JpaRepository 공통 메소드를 사용하는 서비스 계층에서 트랜젝션을 시작하면 JpaRepository도 해당 트랜젝션을 전파받아서 그대로 사용한다. 즉, 지연조회 시점까지 세션을 유지할 수 있다.
@Test
@Transactional(readOnly = true) // 지연 조회 시점까지 세션을 유지
@DisplayName("팀에 속한 회원들 조회")
void selectTest() {
Optional<Ch05Team> optionalCh05Team = teamRepository.findById("team1");
if (optionalCh05Team.isPresent()) {
Ch05Team team = optionalCh05Team.get();
List<Ch05Member> memberList = team.getMemberList();
for (Ch05Member member : memberList) {
log.debug("회원 = {}", member.getUsername());
}
}else {
log.debug("팀 정보 is null");
}
}
'개발 > JPA' 카테고리의 다른 글
JPA에서 save할때 select 쿼리가 먼저 실행되는 이유 (6) | 2021.09.23 |
---|