개발 일기

[Spring Boot] JPA의 @Query, @Modifying 그리고 @Transactional 본문

Back-End/Spring

[Spring Boot] JPA의 @Query, @Modifying 그리고 @Transactional

개발 일기장 주인 2024. 8. 16. 17:20

JPA에서의 UPDATE문

Spring Data JPA를 적용하게 되면 SELECT 문은 JPA를 사용해 쿼리를 만들 수 있지만 INSERT, UPDATE, DELETE 와 같은 DML을 특정 조건을 곁들여 사용할 경우 쿼리를 직접 작성해야 한다.

이 때 쿼리는 JPQL 로 작성하게 된다.


@Query

Spring 공식 문서 : '리포지토리 메서드에서 직접 파인더 쿼리를 선언하는 주석입니다.'

@Query Annotation은 JPA를 사용하여 UPDATE 문, DELETER 문과 같은 DML을 수행하기 위해 쿼리를 직접 작성해야 할 때 사용한다.

위에서 잠깐 언급했듯이 쿼리를 작성할 때 JPQL로 작성해야한다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("UPDATE Member m SET m.nickname = :nickname, m.phoneNumber = :phoneNumber WHERE m.id = :memberId")
    int updateMemberDetails(@Param("id") Long memberId, @Param("nickname") String nickname, @Param("phoneNumber") String phoneNumber);
}

 

Spring Data JPA에서 @Query는 기본적으로 SELECT 쿼리(읽기 전용)로 간주된다.

그러나 위 쿼리와 같이 UPDATE, DELETE와 같이 데이터베이스의 상태를 변경하는 쿼리를 실행하려면 @Modifying을 명시적으로 추가해야 합니다. 그렇지 않으면 런타임 시 에러가 발생한다. 


@Modifying

Spring 공식 문서 : '쿼리 메서드가 실행해야 하는 방식을 변경하므로 쿼리를 수정하는 것으로 간주되어야 함을 나타냅니다. 이 주석은 쿼리 주석을 통해 정의된 쿼리 메서드에 사용되는 경우에만 고려됩니다. 기본 데이터 액세스 API를 이미 제어하거나 이름으로 수정하는지 여부를 지정하므로 메서드 이름에서 파생된 사용자 지정 구현 메서드 또는 쿼리에는 적용되지 않습니다.'

사실 완벽하게 이해하지 못 했다. 내가 대략적으로 이해한 내용은 아래와 같다.

@Modifying 어노테이션은 기본적으로 @Query와 함께 사용되어 데이터베이스에서 직접 데이터 변경 작업(예: INSERT, UPDATE, DELETE)을 수행하는 메서드를 정의할 때 사용된다. 이 어노테이션이 필요한 이유는 @Query는 기본적으로 SELECT 쿼리(읽기 전용)로 간주되기 때문에, UPDATE 또는 DELETE와 같은 수정 작업을 수행하기 위해서는 @Modifying을 추가하여 Spring Data JPA에 이 쿼리가 데이터베이스의 상태를 변경할 것임을 명시해야 한다.

 

이 Modifying Annotation에는 2가지 설정을 지정할 수가 있다.

  • clearAutomatically
    : 이 속성은 @Modifying 쿼리 실행 후, 영속성 컨텍스트를 자동으로 클리어할지를 결정한다.
    영속성 컨텍스트를 클리어하는 것은 이 컨텍스트 내에서 관리되는 모든 엔티티를 제거하여, 이후의 데이터베이스 작업에서 새로운 상태를 반영하도록 한다.
    defaultValue는 false (클리어하지 않음)
    예시는 https://devhyogeon.tistory.com/4 여기 참고하면 좋을 것 같다.
     
  • flushAutomatically
    : 이 속성은 @Modifying 쿼리 실행 직전에 영속성 컨텍스트를 자동으로 플러시할지를 결정한다.
    플러시(flush)는 영속성 컨텍스트 내의 변경 사항(변경된 엔티티의 상태)을 데이터베이스에 반영하는 작업한다.
    defaultValue는 false (플러시하지 않음).
    flushAutomatically가 의미를 가지게 하려면 Hibernate의 FlushModeType의 값을 COMMIT으로 변경해야한다.
    https://devhyogeon.tistory.com/5 이 블로그를 참고하면 좋을 것 같다.

 

@modifying으로 명시된 Query를 실행할 시 바로 DB에 접근하기 때문에 default 값으로 지정하였을 시 영속성 컨텍스트에 존재하는 모든 entity가 DB에 commit 되지 않는 다는 이야기다.

이 entity와 영속성 컨텍스트에 대한 내용은 다음 포스팅에서 다루도록 하겠다.
설정을 적용하지 않는 다면 DB와 영속성 컨텍스트의 정합성이 맞지 않는 다는 것만 알아두자.

@Modifying Annotation 까지 추가한 updateCar method이다.

이것까지 적용한 후 실행을 하게 되면 Executing an update/delete query 라는 에러가 발생하게 된다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Modifying
    @Query("UPDATE Member m SET m.nickname = :nickname, m.phoneNumber = :phoneNumber WHERE m.id = :memberId")
    int updateMemberDetails(@Param("id") Long memberId, @Param("nickname") String nickname, @Param("phoneNumber") String phoneNumber);
}

@Transactional

JPA를 사용하여 UPDATE, DELETE 문을 위에서 했던 방식으로 직접 작성하여 실행할 때는 Transaction 처리가 필요하다. 그것을 도와주는 Annotation 이라고 생각하면 된다. 해당 쿼리를 실행하던 도중 에러가 발생하게 되면 Rollback 을 진행한다.
@Transactional까지 추가한 최종 코드는 아래와 같다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Modifying
    @Transactional
    @Query("UPDATE Member m SET m.nickname = :nickname, m.phoneNumber = :phoneNumber WHERE m.id = :memberId")
    int updateMemberDetails(@Param("id") Long memberId, @Param("nickname") String nickname, @Param("phoneNumber") String phoneNumber);
}

 

@Transactional을 서비스 레이어에만 적용하면 리포지토리 레이어에서 굳이 @Transactional을 추가할 필요는 없다.
개인적인 생각으로 만약 서비스 레이어에서 하나의 서비스 메소드 로직 중간에 하나라도 에러가 나면 Rollback을 해야한다면 서비스 레이어의 메소드에다가 @Transactional을 적용해야하고 서비스 로직 중간에 에러나도 실행된 쿼리를 기준으로 Rollback처리가 필요하다면 서비스 레이어의 해당 로직의 Transactional을 readOnly로 돌리고 그 로직 안에 있는 각 쿼리마다 @Transactional을 적용해주는 것이 맞는 것 같다.

정리해보면
1. 서비스 레이어에 @Transactional을 적용하여 하나의 서비스 로직 아래의 쿼리들은 전체 트랜잭션을 통해 관리하는 것이 일반적
2. 리포지토리 레이어에 @Transactional을 추가할 필요는 보통 없지만, 쿼리 단위로 트랜잭션 처리가 필요한 경우 쓰일 수도 잇을듯