본문
LazyInitializationException
OneToMany로 엮인 다른 엔티티의 목록을 가져오려고 하면 아래와 같은 오류가 발생합니다.
이번 글에서는 늦은 초기화(Lazy initialization)에 관련된 Exception 해결 방법을 정리합니다.
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: kr.co.javaworld.jpa.domain.Person.cars, could not initialize proxy - no Session at |
💡 Exception 발생 코드
아래의 테스트 코드를 실행하던 중 예외가 발생했습니다.
testFindCars 메소드는 Person 객체를 하나 찾은 후, 다시 그 Person이 가진 Car 목록을 조회하는 테스트 코드입니다.
Person.getCars() 호출 후 목록(cars)의 크기를 얻으려고하니 (assertThat(cars, hasSize(4))) 오류가 발생했습니다.
package kr.co.javaworld.jpa.service;
import kr.co.javaworld.jpa.domain.Car;
import kr.co.javaworld.jpa.domain.Person;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import sample.data.jpa.SampleDataJpaApplication;
import javax.transaction.Transactional;
import java.util.List;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SampleDataJpaApplication.class)
public class PersonRepositoryIntegrationTests {
@Autowired
PersonRepository repository;
..............
.... 중략 ....
..............
@Test
public void testFindCars() {
Person person = repository.findOne(1L);
List<Car> cars = person.getCars();
assertThat(cars, hasSize(4));
assertThat(cars.get(0).getName(), is("Mercedes"));
}
}
💡 Solution 1
@OneToMany로 엮을 경우 JPA는 늦은 초기화를 통해 Person의 cars 목록을 가져오는게 기본 설정입니다.
즉, 위 코드의 @OneToMany annotation 선언은 @OneToMany(fetch = FetchType.LAZY)로 대체해도 동일합니다.
따라서 해결 방법은 간단합니다. 패치 타입을 FetchType.EAGER로 변경하여 늦은 초기화 대신 이른 초기화(Eager initialization)를 사용하면 됩니다.
아래 코드와 같이 @OneToMany(fetch = FetchType.EAGER)로 annotation 선언을 변경해 주면 오류가 사라집니다.
package kr.co.javaworld.jpa.domain;
import javax.persistence.*;
import java.util.List;
@Entity
@Table(name = "TB_PERSON_02837")
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "PERSON_NAME", length = 100, unique = true, nullable = false)
private String name;
@OneToMany(fetch = FetchType.EAGER)
private List<Car> cars;
..............
.... 중략 ....
..............
}
💡 Solution 2
앞서의 해결 방법은 People 엔티티를 얻어 올 때는 언제나 Car 목록도 함께 가져오도록 강제합니다. 따라서 가끔씩만 Car 목록이 필요하다면 불필요한 메모리 낭비나 서버 성능 저하 등의 문제가 발생할 수 있습니다. 늦은 초기화를 사용하면서도 문제를 해결하려면 아래와 같이 트랜잭션을 Person.getCar() 호출 후 관련 동작을 완료할 때까지 유지하면 됩니다.
아래에서는 testFindCars 메소드에 @Transactional annotation을 붙여서 메소드 전체를 하나의 트랜잭션으로 감쌌습니다. 실무에서는 서비스 클래스의 메소드 단위로 트랜잭션을 잡아 주거나, 뷰 단의 서블릿 필터에서 UserTransaction을 이용하여 요청 단위로 트랜잭션을 처리하는 방법을 사용하면 됩니다.
package kr.co.javaworld.jpa.service;
import kr.co.javaworld.jpa.domain.Car;
import kr.co.javaworld.jpa.domain.Person;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import sample.data.jpa.SampleDataJpaApplication;
import javax.transaction.Transactional;
import java.util.List;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = SampleDataJpaApplication.class)
public class PersonRepositoryIntegrationTests {
@Autowired
PersonRepository repository;
..............
.... 중략 ....
..............
@Test
@Transactional
public void testFindCars() {
Person person = repository.findOne(1L);
List<Car> cars = person.getCars();
assertThat(cars, hasSize(4));
assertThat(cars.get(0).getName(), is("Mercedes"));
}
}
댓글