[JPA Lazy Evaluation] LazyInitializationException: could not initialize proxy – no Session
Basic Information
LazyInitializationException: could not initialize proxy - no Session
JPA 관련해서 작업을 하다보면 위와 같은 에러메시지를 만나곤 합니다. 무슨 의미일까?
JPA Session
여기서 Session 은 JPA 영속성 컨텍스트가 유지되는 하나의 세션을 의미합니다.
즉, 트랜잭션 시작 ~ 트랜잭션 끝 까지를 의미합니다.
언제 이런 예외가 발생할까??
회원가입을 예로들면, 회원가입을 구현하기 위해 1. 중복체크 2. 데이터 저장 순서로 보통 구현합니다.
아래 예에서는 중복체크를 위해 getOne을 이용해서 Lazy 엔티티를 가져왔습니다.
(getOne에 대해서는 아래 부분에서 설명합니다.)
그리고 엔티티에 필요한 작업을 하고 저장할때 이전에 가져온 Lazy 엔티티를 사용합니다.
하지만 @Transaction이 나뉘어져 있는 것에 주목합니다.
다른 세션에서 Lazy 엔티티를 전달받아 사용할 경우 이런 예외가 발생할 수 있습니다.
[Wrong code]
- A {
- @Transaction
- isDuplicate() { userEntity = getOne(id) ... }
- ...
- @Transaction
- save(userEntity);
- }
- @Transaction
- B() {
- userEntity = isDuplicate()
- save(userEntity)
- }
isDuplicate에서는 중복제크를 위해 getOne을 이용해서 데이터베이스에서 이미 값이 있는지 확인했습니다.
JPA에는 getOne, findOne 두가지 방법으로 데이터를 읽어올 수 있습니다.
getOne은 Lazy Evaluation으로 동작합니다. 즉, getOne은 껍데기인 프록시의 레퍼런스를 리턴합니다.
그리고 리턴된 객체를 save()에서 사용하는데, 다른 @Transaction 으로 묶여있기 때문에, save() 에서는 해당 레퍼런스를 알지 못합니다.
그래서 proxy를 initialize (초기화) 할 수 없습니다. 해당 프록시를 초기화하기 위한 세션이 없기 때문입니다.
그래서 LazyInitializationException: could not initialize proxy - no Session 와 같은 에러가 발생합니다.
JPA는 @Trasnaction로 묶인 곳 안(영속성 컨텍스트)에서 Entity를 관리합니다.
@Transaction은 스프링에서 트랜잭션을 관리하는 방법 중 하나입니다.
스프링에서 @Transaction은 프록시를 이용해서 동작합니다. 프록시가 Target Object를 감싸서 트랜잭션 부가기능을 제공합니다.
@Transaction은 프록시를 이용한다는 점에 주목합니다. 때문에 외부에서 접근해야 프록시가 적용됩니다. 프록시이기 때문입니다.
때문에 @Transaction이 적용된 자기 자신(this)의 다른 메소드를 그냥 호출하면 프록시를 타지않기 때문에 유효하지 않습니다.
다른 추가적인 방법이 필요합니다.
JPA는 매번 데이터베이스에 접근하는 비효율적인 계산을 방지하기 위해 영속성 컨텍스트에 엔티티를 관리합니다.
엔티티를 관리할때 Lazy Evaluation 개념이 나옵니다.
어떤 엔티티의 모든 필드가 필요한 것이 아닐 경우가 있습니다.
A가 B 엔티티를 필드로 가지고있는데, B 엔티티의 모든 필드가 당장 필요한것이 아니라, A엔티티를 반환하기 위해 참조만 필요할 경우 JPA는 Lazy Evaluation을 적용합니다.
즉, B엔티티를 데이터베이스에서 접근해서 모든 필드를 채워오는 것이 아니라, 껍데기 뿐인 프록시 객체를 생성한 뒤 참조만 A엔티티에 넘겨줍니다. 그리고 나중에 B엔티티를 사용할 필요가 있을 때만 데이터베이스에서 접근해서 껍데기 뿐이였던 프록시 객체를 채워넣습니다.
물론 영속성 컨텍스트에 보관되어있는 객체들에 다시 접근할 경우, 데이터베이스에 다시 접근하는 것이아니라 컨텍스트에 있는 객체를 재사용합니다.
다시말하자면, Lazy Evaluation을 적용하면 프록시를 이용하며 껍데기인 참조만 리턴됩니다.
이것이 findOne과 getOne의 차이입니다. findOne은 데이터를 바로 가져오고, getOne은 Lazy Evaluation이 적용된 프록시를 리턴합니다.
주석을 보면 findOne의 경우 "Retrieves an entity” 이고, getOne은 "Returns a reference to the entity”로 레퍼런스만 가져옵니다.
하고자 하는것은 간단합니다. 아래와 같이 ExampleEntity 가 ExampleAttachmentEntity 와 1:N 관계를 맺고있습니다.
DTO는 ExampleEntity는 ExampleDto DTO를 사용하고, ExampleAttachmentEntity는 ExampleAttachmentDto가 사용됩니다.
ExampleEntity 리스트를 가져와서 ExampleDto 리스트로 변환한 뒤 캐쉬에 저장합니다.
이후에 같은 요청들은 캐쉬를 통해 가져다 사용하도록 합니다. 코드는 다음과같습니다.
ExampleEntity에서는 List로 exampleAttachments을 가지고 있는 것에 주목합니다.
- @Entity(name = “ExampleEntity")
- @Table(name = “example")
- public class ExampleEntity {
- @Id
- @GeneratedValue(strategy = GenerationType.AUTO)
- @Column(name = "id")
- private Long id;
- @JsonIgnore
- @Where(clause = "deleted='N'")
- @OneToMany(mappedBy = “example", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
- private List exampleAttachments;
- …
- }
- @Entity(name = “ExampleAttachmentEntity")
- @Table(name = “example_attachment")
- public class ExampleAttachmentEntity {
- @Id
- @GeneratedValue(strategy = GenerationType.AUTO)
- @Column(name = "id")
- private Long id;
- …
- }
- public class ExampleDto {
- private Long id;
- ...
- private List exampleAttachments;
- }
- public class ExampleAttachmentDto{
- private Long id;
- ...
- }
그리고 getExamples 메소드에서는 find메소드를 이용해서 database에 접근하여 ExampleEntity 리스트를 가져옵니다.
그리고 가져온 exampleEntity 리스트를 convert 메소드를 이용해서 ExampleDto 리스트로 변환합니다.
그리고 메소드에는 캐쉬를 적용하여 DB접근을 최소화합니다.
코드는 다음과같습니다.
- @ReadThroughSingleCache(namespace = “example_cache_v1", expiration = 60)
- public List getExamples(@ParameterValueKeyProvider(order = 0) Condition condition) {
- …
- // get exampleEntities from database
- Page entities = find(condition);
- return convert(entities.getContent());
- }
getExamples 에서는 convert 메소드를 이용해서 ExampleEntity리스트를 ExampleDto 리스트로 변환합니다.
변환할때 필드가 많아서 ModelMapper를 이용합니다.
이때 ExampleEntity에 멤버변수로 있는 List<ExampleAttachmentEntity>또한 ModelMapper가 복사합니다.
이때 문제가 발생합니다.
- private ModelMapper modelMapper = new ModelMapper();
- private List convert(List exampleEntities) {
- List exampleDtos = exampleEntities.stream().map(entity-> {
- return modelMapper.map(entity, ExampleDto.class);
- }).collect(Collectors.toList());
- return exampleDtos;
- }
- message
- ...
- WARN 2018-10-10 18:33:43 [http-nio-10001-exec-16] com.google.code.ssm.aop.CacheAdvice.warn:51 - Caching on method execution(ExampleFindService.getExamples(..)) and key [example_cache_v1:N2018-10-10A00110721] aborted due to an error. com.google.code.ssm.providers.CacheException: java.lang.RuntimeException: Exception waiting for value at com.google.code.ssm.providers.spymemcached.MemcacheClientWrapper.get(MemcacheClientWrapper.java:165) ~[spymemcached-provider-3.6.0.jar:na] at com.esotericsoftware.kryo.serializers.CollectionSerializer.read(CollectionSerializer.java:134) ~[kryo-shaded-3.0.3.jar:na] at at com.google.code.ssm.providers.spymemcached.TranscoderAdapter.decode(TranscoderAdapter.java:47) ~[spymemcached-provider-3.6.0.jar:na] at net.spy.memcached.transcoders.TranscodeService$1.call(TranscodeService.java:63) ~[spymemcached-2.11.7.jar:2.11.7] at java.util.concurrent.FutureTask.run(FutureTask.java:266) ~[na:1.8.0_152] at net.spy.memcached.transcoders.TranscodeService$Task.run(TranscodeService.java:110) ~[spymemcached-2.11.7.jar:2.11.7] at net.spy.memcached.transcoders.TranscodeService$Task.get(TranscodeService.java:96) ~[spymemcached-2.11.7.jar:2.11.7] ... 116 common frames omitted Caused by: org.hibernate.LazyInitializationException: failed to lazily initialize a collection, could not initialize proxy - no Session at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:569) ~[hibernate-core-4.2.20.Final.jar:4.2.20.Final] at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:188) ~[hibernate-core-4.2.20.Final.jar:4.2.20.Final] at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:548) ~[hibernate-core-4.2.20.Final.jar:4.2.20.Final] at org.hibernate.collection.internal.AbstractPersistentCollection.write(AbstractPersistentCollection.java:373) ~[hibernate-core-4.2.20.Final.jar:4.2.20.Final] at org.hibernate.collection.internal.PersistentBag.add(PersistentBag.java:291) ~[hibernate-core-4.2.20.Final.jar:4.2.20.Final] at com.esotericsoftware.kryo.serializers.CollectionSerializer.read(CollectionSerializer.java:134) ~[kryo-shaded-3.0.3.jar:na] at com.esotericsoftware.kryo.serializers.CollectionSerializer.read(CollectionSerializer.java:40) ~[kryo-shaded-3.0.3.jar:na] at com.esotericsoftware.kryo.Kryo.readObject(Kryo.java:708) ~[kryo-shaded-3.0.3.jar:na] at com.esotericsoftware.kryo.serializers.ObjectField.read(ObjectField.java:125) ~[kryo-shaded-3.0.3.jar:na] ... 130 common frames omitted
- private List convert(List before) {
- List notices = before.stream().map(entity-> {
- return ExampleConverter.deepcopy(notice);
- }).collect(Collectors.toList());
- return notices;
- }
- public class ExampleConverter {
- public static ExampleDto deepcopy(ExampleEntity exampleEntity) {
- return ExampleDto.builder()
- .id(exampleEntity.getId())
- ...
- .exampleAttachments(ExampleAttachmentConverter.deepcopy(exampleEntity.getExampleAttachments()))
- .build();
- }
- }
- public class ExampleAttachmentConverter {
- public static List deepcopy(List exampleAttachments) {
- if (exampleAttachments == null) {
- return Lists.newArrayList();
- }
- List exampleAttachmentDtos = Lists.newArrayList();
- for (ExampleAttachmentEntity exampleAttachmentEntity : exampleAttachments) {
- exampleAttachmentDtos.add(
- ExampleAttachmentDto.builder()
- .id(exampleAttachmentEntity.getId())
- ...
- .build()
- );
- }
- return siteNoticeAttachmentDtos;
- }
- }