개요
요즘 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
준티 address modify / delete reply
감사합니다 !
ryan address modify / delete reply
이에러 잡는데 오늘 하루 걸렷네요. 감사합니다.
우왓 address modify / delete reply
우왕 해결되었어요 감사합니다!