팀원들과 JPA의 영속성 컨텍스트에 대해 얘기를 나누던 도중, OSIV
를 듣게 되었다.
연관 관계에서 fetch
가 Lazy
로 설정돼 있다면, 해당 메소드에서 연관 관계 엔티티를 조회할 시 트랜잭션 환경에서 진행되야 하는 것으로 알고 있다.
그 이유는, 영속성 컨텍스트를 통해 프록시 객체를 조회해야 하는데, 트랜잭션 환경이 아니라면 영속성 컨텍스트는 자기 할 일만 하고 생명 주기가 끝나버리기 때문이다.
원래는 밑의 예제 코드에서 boardRepository.findById(boardId)
를 호출하고 반환하면 영속성 컨텍스트가 끝나야 하지만, 메소드단에 @Transactional
이 걸려있기 때문에 해당 메소드가 끝나기 전까진 영속성 컨텍스트가 살아있게 된다.
@Transactional(readOnly = true)
@Override
public BoardWithCommentResponseDto getBoard(Long boardId) {
Board board = boardRepository.findById(boardId)
.orElseThrow(NotFoundBoardException::new);
List<CommentWithUserResponseDTO> comments = board.getComments(). ...
}
만약, 트랜잭션 환경이 아니라면 연관 관계 객체(프록시 객체)를 조회할 시 오류가 터진다.
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: kr.ac.brother.newsjin.board.entity.Board.comments: could not initialize proxy - no Session
at org.hibernate.collection.spi.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:635) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final]
at org.hibernate.collection.spi.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:218) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final]
at org.hibernate.collection.spi.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:615) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final]
at org.hibernate.collection.spi.AbstractPersistentCollection.read(AbstractPersistentCollection.java:136) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final]
at org.hibernate.collection.spi.PersistentBag.iterator(PersistentBag.java:371) ~[hibernate-core-6.2.13.Final.jar:6.2.13.Final]
at java.base/java.util.Spliterators$IteratorSpliterator.estimateSize(Spliterators.java:1865) ~[na:na]
at java.base/java.util.Spliterator.getExactSizeIfKnown(Spliterator.java:414) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.exactOutputSizeIfKnown(AbstractPipeline.java:470) ~[na:na]
...
그런데, OSIV
가 true
로 설정돼있다면, 위의 메소드에서 @Transactional
이 없어도 프록시 객체를 조회할 수 있었다.
@Override
public BoardWithCommentResponseDto getBoard(Long boardId) {
Board board = boardRepository.findById(boardId)
.orElseThrow(NotFoundBoardException::new);
List<CommentWithUserResponseDTO> comments = board.getComments(). ...
}
OSIV가 대체 무엇이길래 이러한 상황이 가능한 것인지 한번 알아보는 시간을 가져본다.
Open In View
, Open Session In View
, Open Entitymanager In View
등 불려지는 이름이 많지만, 관례상 OSIV
라고 많이 불린다.
OSIV는 하나의 요청에 대한 세션 연결을 응답을 할 때 까지 연결한다는 것을 의미한다. JPA에선 영속성 컨텍스트를 뷰까지 열어두는 기능을 맡는다.
이것은 개발자들이 지연 로딩을 용이하게 다룰 수 있도록 도와주는 용도로 많이 쓰인다.
Spring boot에선 다음과 같이 application.properties
에서 OSIV를 활성화시킬 수 있으며, 따로 설정을 하지 않는다면 기본적으로 활성화된 상태가 된다.
spring.jpa.open-in-view=true
OSIV를 적절히 사용하면 개발자들에게 편리한 작업 환경을 제공해줄 수 있지만, 요청의 수가 많고 처리하는 시간이 길수록 DB 자원을 빠르게 고갈시킬 수 있는 단점이 존재한다.
@Transactional
이 없어도 지연 로딩된 객체를 조회할 수 있다.@Transactional
이 있어야만 지연 로딩된 객체를 조회할 수 있다.OSIV는 상황에 따라 사용돼야 하며, 무조건적으로 안좋다고는 볼 수 없다.
다음과 같이 엔티티가 있다고 가정한다.
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue
private Long id;
private String username;
@ElementCollection
private Set<String> permissions;
// getters and setters
}
참고로, @ElementCollection
의 기본 fetch 전략은 Lazy이다.
public interface UserRepository extends JpaRepository<User, Long> {
@EntityGraph(attributePaths = "permissions")
Optional<User> findDetailedByUsername(String username);
Optional<User> findSummaryByUsername(String username);
}
@EntityGraph
를 통해, 지연 로딩 객체라도 즉시 가져올 수 있게 메소드를 설정할 수 있다.
즉시 로딩을 할 수 있는 메소드와 지연 로딩을 할 수 있는 메소드를 각각 만들어서 필요에 따라 사용한다.
select u.id, u.username, p.user_id, p.permissions from users u
left outer join user_permissions p on u.id=p.user_id where u.username=?
즉시 로딩되도록 fetch join을 한다.
JPQL
이나 QeuryDsl
을 통해 작성할 수 있다.
https://www.baeldung.com/spring-open-session-in-view#2-pattern-or-anti-pattern