Spring Data JPA - LazyInitializationException
개요
요즘 Spring Boot 저장소에 있는 Spring Data JPA 샘플 프로젝트를 살펴 보고 있습니다.
테스트 코드를 작성하면서 하나하나 파악하고 있는데, OneToMany로 엮인 다른 엔티티의 목록을 가져오려고 하면 아래와 같은 오류가 발생합니다.
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 org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:575)
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:214)
at org.hibernate.collection.internal.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:155)
at org.hibernate.collection.internal.PersistentBag.size(PersistentBag.java:278)
at org.hamcrest.collection.IsCollectionWithSize.featureValueOf(IsCollectionWithSize.java:21)
at org.hamcrest.collection.IsCollectionWithSize.featureValueOf(IsCollectionWithSize.java:14)
at org.hamcrest.FeatureMatcher.matchesSafely(FeatureMatcher.java:40)
at org.hamcrest.TypeSafeDiagnosingMatcher.matches(TypeSafeDiagnosingMatcher.java:55)
at org.hamcrest.MatcherAssert.assertThat(MatcherAssert.java:12)
at org.junit.Assert.assertThat(Assert.java:956)
at org.junit.Assert.assertThat(Assert.java:923)
at kr.co.javaworld.jpa.service.PersonRepositoryIntegrationTests.testFindCars(PersonRepositoryIntegrationTests.java:46)
..............
.... 중략 ....
..............
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
늦은 초기화(Lazy initialization)에 관련된 예외 상황인데, 이에 대한 해결 방법을 정리해 봅니다.
예외가 발생하는 코드
아래의 테스트 코드를 실행하던 중 예외가 발생했습니다. 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"));
}
}
Person
엔티티의 코드입니다.
cars
는 @OneToMany
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
private List<Car> cars;
..............
.... 중략 ....
..............
}
해결 방법
@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;
..............
.... 중략 ....
..............
}
다른 해결 방법
앞서의 해결 방법은 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"));
}
}
EOF
'Java' 카테고리의 다른 글
WildFly (JBoss)가 자동 적용하는 라이브러리 배제 방법 (0) | 2017.08.07 |
---|---|
Spring Boot : auto-configuration 그리고 debug (0) | 2015.04.17 |
Spring Shell Command 구현 (0) | 2015.01.07 |
Spring Shell 소개 (0) | 2015.01.02 |
logback.xml - No grammar constraints (DTD or XML Schema) referenced in the document (0) | 2012.05.10 |