Develop/Spring+JPA

[JPA] @Query와 @Transactional을 꼭 같이 써야하나요?🤔

연로그 2022. 11. 17. 12:25
반응형

😄 서론

지난 번에 deleteAll과 관련해 N+1 문제가 발생했었다. 그 과정에 @Query를 이용해 직접 JPQL을 작성하여 해결했는데  (🔗관련 글 링크) 한가지 의문이 생겼다. 해당 문제를 해결하기 위해 다양한 문서들을 찾아보았는데 많은 예제에서 JPQL을 사용할 때 @Transactional 어노테이션을 붙이고 있었다. 안 붙여도 잘 동작하는데 그랬을까? 원인을 찾아보기 시작했다.

 

 

😏 Repository 구현체와 트랜잭션

우리가 JPA를 사용해서 ***Repository 클래스를 만들 때 일반적으로 Repository나 JpaRepository를 확장한다.

public interface ReminderRepository extends Repository<Reminder, Long> {
    // ...
}

 

Custom Repository을 사용하는 곳에서 디버그 해보면 SimpleJpaRepository가 proxy 형태로 주입되는 것을 확인할 수 있다.

 

그렇다면 SimpleJpaRepository에서는 트랜잭션이 어떻게 처리될까? 해당 클래스를 들어가보았더니 @Transactional을 이용하는 것을 확인할 수 있었다. 전역으로 @Transactional(readOnly = true)이 걸려있었고, CUD 관련 메서드에는 @Transactional이 걸려있었다. save(), delete() 등과 같은 기본적인 메서드들은 SimpleJpaRepository를 이용해 실행되므로 트랜잭션이 잘 적용된다.

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
    // ...
    
    @Override
    @Transactional
    @SuppressWarnings("unchecked")
    public void delete(T entity) { ... }
    
    @Override
    public Optional<T> findById(ID id) { ... }
}

 

 

🤔 그렇다면 @Query는?

그렇다면 SimpleJpaRepository에 메서드가 없는, @Query와 JPQL을 이용해 작성한 메서드들은 트랜잭션이 어떻게 적용될까? 결론부터 말하자면 트랜잭션이 적용되지 않는다. Spring Data JPA에서는 트랜잭션 내에서 사용하고 싶다면 @Transacational을 붙이는 것을 권장하고 있다. 아래는 Spring Docs에서 가져온 예제이다.

@Transactional(readOnly = true)
interface UserRepository extends JpaRepository<User, Long> {

    List<User> findByLastname(String lastname);

    @Modifying
    @Transactional
    @Query("delete from User u where u.active = false")
    void deleteInactiveUsers();
}

 

헌데 내 코드에서는 @Transactional을 붙이지 않아도 여태까지 오류가 난 적이 없다. 왜 그럴까? 정답은 코드 내에서 유일하게 Repository를 호출하고 있던 Service에 있다. @Query에 트랜잭션을 별도로 처리해주지 않아도 Service에서의 트랜잭션이 이미 존재하므로 잘 동작하고 있었던 것이다. 실제로 트랜잭션이 적용되지 않는 것을 눈으로 확인해보고 싶다면 RepositoryTest를 만들어보면 된다.

public interface ReminderRepository extends Repository<Reminder, Long> {
    // ...
    
    @Modifying
    @Query("delete from Reminder r where r in :reminders")
    void deleteInBatch(Iterable<Reminder> reminders);
}
@SpringBootTest
class ReminderRepositoryTest {
    // ...
    
    @Test
    void deleteReminder() {
        // 기본 데이터 세팅
        Reminder reminder = reminderRepository.save(new Reminder(member, message, LocalDateTime.now()));
        reminderRepository.deleteInBatch(List.of(reminder));
    }
}

 

org.springframework.dao.InvalidDataAccessApiUsageException: Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredException: Executing an update/delete query 라는 오류가 발생한다. update/delete를 하기 위해서는 트랜잭션이 필수적으로 존재해야하는데, 트랜잭션이 존재하지 않아서 발생한다.

오류 발생

 

 

😵 조회에도 @Transactional을 달아야 하나요?

@Query를 이용한 조회를 할 때도 @Transactional을 달아야하는가?에 대한 의문이 들었다. 기존에 @Query를 통한 조회 쿼리를 만들어둔 적이 있어 사용해보았다.

public interface ReminderRepository extends Repository<Reminder, Long> {

    @Query("select r from Reminder r join fetch r.member join fetch r.message WHERE r.message.id = :messageId and r.member.id = :memberId")
    Optional<Reminder> findByMessageIdAndMemberId(Long messageId, Long memberId);
}

 

이번에는 통과가 잘 된다. update query에서 발생했던 Exception을 다시 자세히 살펴보니 update/delete에서만 트랜잭션이 필수적이고 select는 해당 사항이 아닌 듯 하다.

테스트 통과

 

@Transactional(readOnly = true)를 붙이는 경우와 붙이지 않는 경우 성능 차이가 발생할지에 대해 궁금해서 추가적인 테스트를 진행해보았다. readOnly를 붙인 경우가 조금 더 빨랐다. 테스트 환경이 H2를 사용하는데 H2는 readOnly 힌트를 지원하지 않는다. 그렇다면 왜 속도 차이가 날까? JPA의 경우 readOnly를 하면 더티체킹을 생략하는데 그 과정 중에서 속도가 더 빠르게 나타난 것이라고 예측해본다.

  @Transactional(readOnly = true)가 있는 메서드 @Transactional 제거한 메서드
실행 시간 96 162

 

 

🙄 고민

우리 프로젝트에서는 Repository를 호출하는 곳은 Service에만 있다. (RepositoryTest를 별도로 만들지 않았다.) 또한 트랜잭션을 전부 Service에서 관리한다. Repository에 @Transactional을 붙여도 전파 속성이 REQUIRED이기 때문에 Service에서 호출한 트랜잭션을 가져다 사용할 것이다.

 

그렇다면 Repository에 꼭 @Transactional을 붙여야할까?에 대한 고민을 했다. 책임 분리의 관점에서 본다면, Service에서 호출하는 상황을 가정하지 않고 Repository만 호출하더라도 정상적으로 동작해야하는 코드를 만들어야하지 않는가? 라는 생각을 했다.

 

@Query와 JPQL을 이용해 정의한 메서드들에는 @Transactional을 붙이자고 건의해봐야겠다.

 


참고

반응형