@EntityGraph, @Transactional와 지연로딩

크리링·2023년 2월 15일
0

오늘의 문제

목록 보기
2/9
post-thumbnail

나의 얕은 지식으로는 도메인 간의 @OneToMany@ManyToOne 관계 시에 지연로딩을 사용해야 한다는 기억이 있다. 즉시로딩이 필요한 특수한 경우에는 리파지토리에 @EntityGraph를 사용하여 값을 페치조인으로 불러온다고 알고있다.
하지만 @Transactional을 사용하면 @EntityGraph를 사용하지 않고도 failed to lazily initialize a collection of role 오류 없이 결과가 나타나는 것을 볼 수 있었다. 그렇다면 @EntityGraph@Transactional 사이에는 어떠한 공통점이 있고, 어떠한 점이 다를까 오늘 짚고 넘어가고자 한다.






@EntityGraph

연관관계가 있는 엔티티를 조회할 경우 지연 로딩으로 설정되어 있으면 연관관계에서 종속된 엔티티는 쿼리 실행 시 select 되지 않고 proxy 객체를 만들어 엔티티가 적용시킨다. 그 후 해당 프록시 객체를 호출할 때마다 select 쿼리가 실행된다.
연관관계가 지연 로딩으로 되어있을 겨우 fetch 조인을 사용하여 여러 번의 쿼리를 한번에 해셜할 수 있다. @EntityGraph는 Data JPA 에서 fetch 조인을 어노테이션으로 사용할 수 있도록 만들어 준 기능이다.

사용

  • 지연로딩의 경우 객체만 조회하고 연관된 객체는 프락시 객체가 대체되어 들어간다.
  • 리파지토리에 @Override 하여 @EntityGraph(attributePaths = {""})를 사용하여 @EntityGraph를 사용할 수 있다.

출처 및 참고 : JPA @EntityGraph 사용하기

리파지토리에서 사용해야 돼서 불편한 점이 있지 않을까 생각이 든다.
그렇다면, @Transactional은 어떤 부분 때문에 @EntityGraph를 사용하지 않아도 오류가 나지 않았던 것일까?






@Transactional

1. 트랜잭션의 성질 (ACID)

  • 원자성 : 한 트랜잭션 내에서 실행한 작업들은 하나로 간주한다. 즉, 모두 성공 또는 모두 실패
  • 일관성 : 트랜잭션은 일관성 있는 데이터베이스 상태를 유지한다.
  • 격리성 : 동시에 실행되는 트랜잭션들이 서로 영향을 미치지 않도록 격리해야한다.
  • 지속성 : 트랜잭션을 성공적으로 마치면 결과가 항상 저장되어야 한다.



2. 스프링에서 트랜잭션 처리 방법

스프링에서 트랜잭션 처리를 지원하는데 그 중 어노테이션 방식으로 @Transactional을 선언하여 사용하는 방법이 일반적이며, 선언적 트랜잭션이라 부른다.

클래스, 메서드 위에 @Transactional이 추가되면, 이 클래스에 트랜잭션 기능이 적용된 프록시 객체가 생성된다.

이 프록시 객체는 @Transactional이 포함된 메소드가 호출될 경우, PlatformTransaction을 사용하여 트랜잭션을 시작하고, 정상 여부에 따라 Commit 또는 Rollback 한다.



3. @Transactional의 작동 원리와 흐름

@Transactional이 클래스 내지 메서드에 붙을 때, Spring은 해당 메서드에 대한 프록시를 만든다. 프록시 패턴은 디자인 패턴 중 하나로, 어떤 코드를 감싸면서 추가적인 연산을 수행하도록 강제하는 방법이다.

트랜잭션의 경우, 트랜잭션의 시작과 연산 종료 시의 커밋 과정이 필요하므로, 프록시를 생성해 메서드의 앞뒤에 트랜잭션의 시작과 끝을 추가하는 것이다. 이러한 로직은 AOP에 바탕을 두고 설계되었다.

또한, 스프링 컨테이너는 트랜잭션 범위의 영속성 컨텍스트 전략을 기본으로 사용한다. 서비스 클래스에서 @Transactional을 사용할 경우, 해당 코드 내의 메서드를 호출할 때 영속성 컨텍스트가 생긴다는 뜻이다. 영속성 컨텍스트는 트랜잭션 AOP가 트랜잭션을 시작할 때 생겨나고, 메서드가 종료되어 트랜잭션 AOP가 트랜잭션을 커밋할 경우 영속성 컨텍스트가 flush되면서 해당 내용이 반영된다. 이후 영속성 컨텍스트 역시 종료되는 것이다.

이러한 방식으로 영속성 컨텍스트를 관리해 주기 때문에, @Transactional을 쓸 경우 트랜잭션의 원칙을 정확히 지킬 수 있다.

유의할 원칙

  • 만약 같은 트랜잭션 내에서 여러 EntityManager를 쓰더라도, 이는 같은 영속성 컨텍스트를 사용한다.
  • 같은 EntityManager를 쓰더라도, 트랜잭션이 다르면 다른 영속성 컨텍스트를 사용한다.


4. @Transactional과 지연로딩

조회한 엔티티가 Service/Repository 계층에서는 영속성 컨텍스트에서 관리되며 영속 상태를 유지하지만, 컨트롤러나 뷰 같은 프레젠테이션 계층에서는 준영속 상태가 된다!

준영속 상태는 엔티티가 영속성 컨텍스트에서 분리된 것을 말한다. 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다. 그 중 하나가 지연로딩이다.

프록시를 사용해서 조회할 경우, 해당 객체에 접근 시 조회 요청을 보내야 한는 것이 지연로딩이다. 영속성 컨텍스트가 이미 종료된 상태라, 엔티티와 DB를 이어줄 매개가 없기 때문에 준영속 상태에서 엔티티는 이 기능을 쓰지 못한다.

ex)

@Entity
public class Book {
	@Id @GeneratedValue
    private Long id;
    
    @ManyToOne(fetch = FetchType.LAZY) // 지연 로딩 전략
    private Author author;
    ...
}

작가와 책이 서로 연관 관계를 맺고 있으며, 지연 로딩을 쓴다.

class BookController {
    public String view(Long bookId) {
    	Book book = bookService.findBook(bookId);
        Author author = book.getAuthor();
        author.getName(); //여기서 예외 발생!!!
        ...
    }
}

findBook 메서드가 종료되면서 영속성 컨텍스트가 닫혔고, 반환된 book 엔티티가 준영속 상태가 된 것이다.
author는 지연로딩 전략을 사용했으므로 비어있는 프록시 객체로 존재했는데, 해당 객체에서 실제로 값을 뽑아 쓰려고 하니 예외가 발생한다.


5. 해결책

  1. 글로벌 페치 전략 수정
  2. JPQL 페치 조인
  3. 강제 초기화
  4. FACADE 계층 추가
  5. DTO 사용







내가 접한 문제에서는 값을 가져오고 나서 또 지연로딩 부분을 찾으려니 준영속성 컨텍스트로 넘어가 문제가 생겼던 것이었다. 그리고 @Transacational을 사용하면 영속성 컨텍스트가 끝나지 않아 해당 칼럼을 조회시에도 문제가 발생하지 않았던 것이었다.






출처 및 참고 : ([Spring] Transactional 정리 및 예제)[https://goddaehee.tistory.com/167]
(@Transactional 어노테이션의 이해
)[https://kafcamus.tistory.com/30]
(@Transactional과 Lazy Loading)[https://kafcamus.tistory.com/31]

0개의 댓글