본 포스팅에서는 JPA에서 많이 부딛히는 문제인 영속성과 관련한 내용을 담았다. JPA의 트랜잭션 환경을 이해하지 못하면 계속해서 발생되는 문제기 때문에 기본으로 장착해두자!!
Spring boot에서 사용하는 @Transactional의 성질은 쿼리 실행 중 발생하는 오류에 대해 롤백 기능을 제공하고 한 트랜잭션 안에 일어나는 작업에 대해 일관성을 유지하기 위해 모두 성공했을 때만 정상적으로 결과가 저장된다.
(* 참조: https://joojimin.tistory.com/25)
또한 트랜잭션 사용 조건에 따라 영속성(Persistence) 문제가 나타날 수 있는데, 여기서 영속성이란 애플리케이션과 데이터베이스 사이에 객체를 유지 및 관리하는 개념이다. 객체와 객체간의 관계를 유지해나가려는 성질을 가지고 있다.
실질적으로 데이터 객체를 저장한다고 가정했을 때 영속성을 통한 저장 순서는 다음과 같다.
[영속성 컨텍스트 저장 순서]
1. 1차 캐시에서 엔티티를 찾는다
2. 있으면 메모리에 있는 1차 캐시에서 엔티티를 조회한다.
3. 없으면 데이터베이스에서 조회한다.
4. 조회한 데이터로 엔티티를 생성해 1차 캐시에 저장한다. (엔티티를 영속상태로 만든다)
5. 조회한 엔티티를 반환한다.
위 처럼 영속성은 캐시를 통한 빠른 접근성을 자랑하며, 쓰기 지연(transactional write-behind)이라는 특성을 가지고 있어 "쓰기 지연 SQL 저장소"라는 곳에 쿼리를 모아 두었다가 커밋할 때 즉, 함수가 종료되는 시점에 데이터베이스로 전송한다. 이는 데이터 변경 및 객체 일관성 유지 관리에 유리하다!
JPA를 처음 다룰 때 영문도 모르게 가장 많이 일어나는 오류는 "LazyInitalizationException" 이었다. 정확히 repository.save()를 했는데도 불구하고 오류가 나는 이유는 뭘까?
아래 코드를 살펴보자
@Entity
public class User {
@Id
...
@OneToMany(fetch = FetchType.LAZY)
private List<Order> orders;
}
...
public List<User> getUsers(String username) {
var user = this.userRepository.findByUsername(username);
var orders = users.getOrders();
...
return users;
}
위 코드에서는 정확하게 users.getOrders() 부분에서 LazyInitalizationException 오류가 나타난다. 이유인즉슨, 영속성의 특징에서 보았듯이 1차 캐시 또는 데이터베이스에서 객체를 조회하여 가져올 것이다. 그리고 나서 JPA의 연속성 세션은 종료된다. 여기서 관계가 설정된 엔티티(OneToMany)를 참조하려고 하면서 오류가 발생하는 것이다.
사실 User 엔티티에서 orders는 엄연히 다른 엔티티이며 User 엔티티는 OneToMany를 통해 마킹만 해두었을 뿐 가져오질 않는다.