[개인프로젝트] LazyInitializationException 영속성 컨텍스트 와 프록시
오늘만 에러 3개쓰는 중...하하하하하하 나 시험공부해야하는데 왜 에러나느지 해결안되면 아무것도 못하는 사람인지라...ㅎㅎ
문제상황
엔티티를 수정하면서 lazy loading해도 되겠다고 생각한 것들을 수정하고 있었다. 하면서 원래 작성했던 테스트를 돌려봤는데..
어랏...?? 안되..??어랏...??
org.hibernate.LazyInitializationException: could not initialize proxy [com.project.user.User#1] - no Session
예?? 없다구요??왜요..??
원인
나는 이 부분을 분명 공부했었다.. 그저 나의 기억력이 안좋을 뿐..
문제는 영속성컨텍스트의 생명주기와 프록시에 대한 이해다.
WalkerReserveServiceInfo serviceDate = walkerReserveServiceRepository.findByWalkerUserIdAndServiceDateTime(
walkerSave.getUserId() ,serviceReserve).get();
대충 이런 코드가 있다고 하자. 그래서 나는
serviceDate.getCustomer().getUserEmail()
serviceDate에 있는 lazyloading방식으로 유저 관련 정보를 꺼내려고 했지만...안된다.
왜냐하면 트랜잭션이 커밋된 후에는 영속성 컨텍스트가 종료되기 때문에, 트랜잭션이 종료된 이후에 lazyloading 엔터티에 접근하면 LazyInitializationException이 발생된다. 즉 영속성 컨텍스트가 끝난 상태이다.
우리는 서비스 단에서 트랜젝션이 일어나도록 하고 컨트롤러단으로 나오면 영속성 상태가 끝난다. 그래서 이런 에러가 발생할 때가 있다.
나같은 경우도 테스트코드지만 프록시를 초기화해야하는데 영속성컨텍스트가 없으므로 실제 엔티티를 조회할 수 없어 예외가 발생한 것이다.
해결방법
첫번째방법은 해당 테스트 코드에 @Transactional을 붙였다.
트랜젝션을 붙임으로써 세션을 계속 유지시킬수 있기 때문이다.
즉 해당 테스트내에서 트랜젝션범위의 영속성컨텍스트가 유지된다.
두번째 방법은 eager로 고치는 것이다.
즉시로딩을 통해 lazy예외를 없애는 것이다.
@Entity
public class WalkerReserveServiceInfo extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "walker_reserve_service_id")
private Long reserveId;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "customer_id",nullable = false)
private User customer;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "walker_id",nullable = false)
private User walker;
}
또 다른 나만의 문제점 ..?
다만 나의 테스트코드에는 저 두방법이 썩 그랬다.
첫번째 방법이 좀 그랬던 이유는 일단 트랜젝션 레벨의 차이??다
테스트코드에 트랜젝션이 있을때의 트랜젝션 로그를 보자면
//테스트를 위한 트랜젝션 시작
2023-12-18T12:38:21.178+09:00 DEBUG 4271 --- [ main] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [com.project.dogwalker.reserve.service.ReserveDistributeTest.reserveService_success]: PROPAGATION_REQUIRED,ISOLATION_READ_UNCOMMITTED
//userRepository에 user넣기 위해 트랜젝션에 참여
2023-12-18T12:40:56.907+09:00 DEBUG 4271 --- [ main] o.s.orm.jpa.JpaTransactionManager : Participating in existing transaction
2023-12-18T12:40:57.022+09:00 TRACE 4271 --- [ main] o.s.t.i.TransactionInterceptor : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
//동시성테스트를 위한 쓰레드 트랜젝션 새로 시작 및 종료
2023-12-18T12:41:30.246+09:00 DEBUG 4271 --- [ool-5-thread-19] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [com.project.dogwalker.aop.distribute.AopForTransaction.proceed]: PROPAGATION_REQUIRES_NEW,ISOLATION_DEFAULT
2023-12-18T12:41:30.251+09:00 DEBUG 4271 --- [ool-5-thread-19] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(341042985<open>)] after transaction
2023-12-18T12:41:30.534+09:00 DEBUG 4271 --- [ool-5-thread-22] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [com.project.dogwalker.aop.distribute.AopForTransaction.proceed]: PROPAGATION_REQUIRES_NEW,ISOLATION_DEFAULT
2023-12-18T12:41:30.536+09:00 DEBUG 4271 --- [ool-5-thread-22] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(43373440<open>)] after transaction
//테스트를 위한 트랜젝션 종료
2023-12-18T12:41:30.553+09:00 DEBUG 4271 --- [ main] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(594175002<open>)] after transaction
테스트를 위한 트랜젝션이 시작되고 userRepository는 테스트 트랜젝션에 참여한다. 여기서 생각해야할부분은 save된게 db에 반영된 것이 아니라 영속성컨텍스트에 반영되었다는 점..!
이후 동시성 테스트를 위한 쓰레드가 각자 새로운 트랜젝션을 시작한다.
여기서 문제가 생긴다.
새로운 트랜젝션이 시작되면 이전 트랜젝션의 영속성 컨텍스트와 별개의 영속성컨텍스트가 생긴다. 그러다보니 새로운 트랜젝션에서 userRepository.find하면 db에 없으니 null값이 나오는 것이다.
만약 테스트 코드에 트랜젝션을 빼면
테스트를 위한 트랜젝션이 생성되지 않아 userRepository는 새로운 트랜젝션이 시작되면서 commit까지 하기때문에 db에 반영되어 동시성 쓰레드가 새로운 트랜젝션으로 시작해도 db에 반영이 되어있기 때문에 조회가 되는 것이다.
2023-12-18T12:47:27.072+09:00 DEBUG 4441 --- [ main] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2023-12-18T12:47:27.112+09:00 DEBUG 4441 --- [ main] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(914571389<open>)]
2023-12-18T12:47:27.120+09:00 DEBUG 4441 --- [ main] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(914571389<open>)] after transaction
다만 이방법은 앞서 얘기했듯이 lazy 예외가 터진다.(FetchType이 lazy일때)
그럼 테스트코드에 @Transactional붙이고 userRepository saveAndFlush해봐!
이미 해봤다. 반영안된다. 여전히 트랜잭션 내부이므로 커밋되기 전까지는 실제로 반영되지 않는다.
두번째 방법이 좀 그랬던 이유
내가 db에 테스트한 데이터들이 잘 들어갔는지 확인하기 위해
findByWalkerUserIdAndServiceDateTime
위 메서드를 사용했다. 위 메서드에서 User는 lazy로딩에서 eager로 바꾸니 해당 user조회를 위해 쿼리가 N번 더 나가는 것이다. 이것이 바로 N+1문제다. 하지만 해당 메서드의 사용목적을 코드로 보니 단순히 해당 엔티티가 있는지 조회하는건데 테스트 코드를 위해서 fetchJoin까지해서 조회하는건 비효율적이라고 생각했다. 사용되지도 않는 엔티티를 가져오는 거니께...
임시방편 해결방법
해당 lazy User를 꼭 테스트에서 검증해야했다. 해당 user로 예약이 잘들어갔는지.. 사실 그럴 필요없다면 그냥 테스트 assertThat코드에서 user관련된 코드 빼면 성공이다.
그래도 나는 user관련하여 검증이 필요하니...
일단은 entityManger의 힘을 빌렸다. 바로 임시로 fetch join해서 데이터를 가져오게
WalkerReserveServiceInfo singleResult = entityManager.createQuery(
"SELECT w FROM WalkerReserveServiceInfo w "
+ "JOIN fetch w.customer " +
"WHERE w.walker = :walkerId " +
"AND w.serviceDateTime = :serviceDate" , WalkerReserveServiceInfo.class)
.setParameter("walkerId" , walkerSave)
.setParameter("serviceDate" , serviceReserve)
.getSingleResult();
SELECT
w
FROM
WalkerReserveServiceInfo w
JOIN
fetch w.customer
JOIN
fetch w.walker
WHERE
w.walker = :walkerId
AND w.serviceDateTime = :serviceDate */ select
w1_0.walker_reserve_service_id,
w1_0.walker_reserve_service_created_at,
c1_0.user_id,
c1_0.user_created_at,
c1_0.user_updated_at,
c1_0.user_email,
c1_0.user_lat,
c1_0.user_lnt,
c1_0.user_name,
c1_0.user_phone_number,
c1_0.user_role,
w1_0.pay_history_id,
w1_0.walker_service_date,
w1_0.walker_reserve_service_price,
w1_0.walker_service_status,
w1_0.walker_service_time_unit,
w1_0.walker_reserve_service_updated_at,
w2_0.user_id,
w2_0.user_created_at,
w2_0.user_updated_at,
w2_0.user_email,
w2_0.user_lat,
w2_0.user_lnt,
w2_0.user_name,
w2_0.user_phone_number,
w2_0.user_role
from
walker_reserve_service w1_0
join
users c1_0
on c1_0.user_id=w1_0.customer_id
join
users w2_0
on w2_0.user_id=w1_0.walker_id
where
w2_0.user_id=?
and w1_0.walker_service_date=?
쿼리가 길지만 한반에 조회되고 테스트를 위한 쿼리이기에 괜찮다고 생각한다..
이로서 에러 다 해결
어렵긴한테 영속성컨텍스트랑 트랜젝션 잼있네??? 이놈들 아주 재미있는 녀석들이야.. 어제부터 거의 6시간 고민끝네 entityManger가 생각나서 다행히...여기서 마무리 짓겠다. 왜냐면 오늘 제출해야하거든 프로젝트....ㅎㅎ