[TIL] JPA - JPA에서의 OSIV, 안좋은 것인가?

phdljr·2023년 12월 1일
0

TIL

목록 보기
40/70

서문

팀원들과 JPA의 영속성 컨텍스트에 대해 얘기를 나누던 도중, OSIV를 듣게 되었다.

연관 관계에서 fetchLazy로 설정돼 있다면, 해당 메소드에서 연관 관계 엔티티를 조회할 시 트랜잭션 환경에서 진행되야 하는 것으로 알고 있다.

그 이유는, 영속성 컨텍스트를 통해 프록시 객체를 조회해야 하는데, 트랜잭션 환경이 아니라면 영속성 컨텍스트는 자기 할 일만 하고 생명 주기가 끝나버리기 때문이다.

원래는 밑의 예제 코드에서 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]
    ...

그런데, OSIVtrue로 설정돼있다면, 위의 메소드에서 @Transactional이 없어도 프록시 객체를 조회할 수 있었다.

@Override
public BoardWithCommentResponseDto getBoard(Long boardId) {
    Board board = boardRepository.findById(boardId)
        .orElseThrow(NotFoundBoardException::new);

    List<CommentWithUserResponseDTO> comments = board.getComments(). ...
}

OSIV가 대체 무엇이길래 이러한 상황이 가능한 것인지 한번 알아보는 시간을 가져본다.


Open Session In View(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 자원을 빠르게 고갈시킬 수 있는 단점이 존재한다.

OSIV를 활성화 시킨다면

  • @Transactional이 없어도 지연 로딩된 객체를 조회할 수 있다.
    • 단, 조회만 할 수 있으며 수정은 못한다.
    • 수정은 트랜잭션이 있는 계층에서만 동작한다.
  • 한 번 조회된 엔티티는 요청이 끝날 때까지 영속 상태를 유지한다.
  • 요청에 대한 응답을 반환하기까지 영속성 컨텍스트와 DB의 커넥션을 유지한다.

OSIV를 비활성화 시킨다면

  • @Transactional이 있어야만 지연 로딩된 객체를 조회할 수 있다.
  • 한 번 조회된 엔티티는 트랜잭션 환경이 끝날 때까지 영속 상태를 유지한다.

효율적인 활용 방안

OSIV는 상황에 따라 사용돼야 하며, 무조건적으로 안좋다고는 볼 수 없다.

  • 일단은 OSIV를 비활성화시킨 채로 개발해라.
    • 사용 안하다가 도중에 활성화 시키면 좋지만, 사용하는 도중에 비활성화 시키면 코드를 수정해야 할 확률이 높다.
  • 간단한 CRUD 서비스에선 활성화하는 것이 좋을 수도 있다.
  • 원격 서비스를 많이 호출하거나, 트랜잭션 컨텍스트 외부에서 많은 일이 일어나고 있는 경우는 비활성화하는 것이 좋다.
    • 그 시간동안 DB의 자원을 사용하지 않으므로, 이를 낭비시킬 필요는 없다.

대체 방안

다음과 같이 엔티티가 있다고 가정한다.

@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이다.

Entity Graphs

public interface UserRepository extends JpaRepository<User, Long> {
    @EntityGraph(attributePaths = "permissions")
    Optional<User> findDetailedByUsername(String username);

    Optional<User> findSummaryByUsername(String username);
}

@EntityGraph를 통해, 지연 로딩 객체라도 즉시 가져올 수 있게 메소드를 설정할 수 있다.

즉시 로딩을 할 수 있는 메소드와 지연 로딩을 할 수 있는 메소드를 각각 만들어서 필요에 따라 사용한다.

fetch join

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

profile
난 Java도 좋고, 다른 것들도 좋아

0개의 댓글