Transactionality

2024. 10. 22. 18:41Spring Boot/Spring Data JPA

디폴트 Transaction 설정

CrudRepository에서 상속받는 메서드들은 SimpleJpaRepository에서 기본적인 트랜잭션 설정을 상속받습니다. 읽기 작업(read operation)의 경우 @Transactional 어노테이션의 readOnly 속성이 true로 설정됩니다. 반면, 그 외의 작업들은 기본적인 @Transactional 설정이 적용됩니다.

예시: CrudRepository 기본 트랜잭션 설정

public interface UserRepository extends CrudRepository<User, Long> {

    @Override
    @Transactional(timeout = 10)
    public List<User> findAll();

    // 추가적인 쿼리 메서드 선언
}

위 코드에서는 findAll() 메서드를 재정의하면서 @Transactional 어노테이션의 timeout 속성을 10초로 설정했습니다. 이로 인해 findAll() 메서드는 10초 내에 완료되지 않으면 타임아웃이 발생하며, readOnly 속성은 기본값인 false가 됩니다.

서비스 계층을 통한 트랜잭션 관리

CRUD 메서드의 트랜잭션 설정을 변경하는 방법 중 하나는 위에서 본 것처럼 repository 인터페이스에서 직접 설정하는 것이지만, 또 다른 방법은 서비스 계층을 이용하는 것입니다. 서비스 계층은 여러 repository를 묶어서 비즈니스 로직에 맞는 트랜잭션 경계를 설정하는 데 사용됩니다. 이를 통해 복잡한 비CRUD 작업을 처리할 때 트랜잭션을 관리할 수 있습니다.

예시: 서비스 계층에서 트랜잭션 관리

@Service
public class UserManagementImpl implements UserManagement {

    private final UserRepository userRepository;
    private final RoleRepository roleRepository;

    public UserManagementImpl(UserRepository userRepository,
                              RoleRepository roleRepository) {
        this.userRepository = userRepository;
        this.roleRepository = roleRepository;
    }

    @Transactional
    public void addRoleToAllUsers(String roleName) {
        Role role = roleRepository.findByName(roleName);

        for (User user : userRepository.findAll()) {
            user.addRole(role);
            userRepository.save(user);  // save 호출은 권장 사항
        }
    }
}

위 코드에서는 addRoleToAllUsers 메서드가 트랜잭션 내에서 실행됩니다. 이 메서드가 호출되면 이미 실행 중인 트랜잭션에 참여하거나, 실행 중인 트랜잭션이 없으면 새로운 트랜잭션을 생성합니다. roleRepositoryuserRepository에서 각각 데이터를 조회하고 변경하는 작업이 트랜잭션 내에서 이루어집니다.

주의사항

  • @Transactional을 이용해 트랜잭션 경계를 설정할 때, repository의 개별 메서드에 설정된 트랜잭션은 무시됩니다. 즉, 외부의 트랜잭션 설정이 우선 적용됩니다.
  • JPA 관점에서 save() 호출은 꼭 필요하지 않지만, Spring Data의 repository 추상화에 맞추기 위해 save() 호출을 포함하는 것이 좋습니다.

쿼리 메서드와 트랜잭션

쿼리 메서드(선언된 쿼리 메서드 및 디폴트 메서드)에는 기본적으로 트랜잭션 설정이 적용되지 않습니다. 만약 특정 쿼리 메서드를 트랜잭션으로 실행하고 싶다면, 해당 메서드에 @Transactional을 명시적으로 선언해야 합니다.

예시: 쿼리 메서드에서 @Transactional 사용

@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();
}

위 코드에서 findByLastname() 메서드는 데이터를 읽기만 하므로 readOnly 속성을 true로 설정해 읽기 전용 트랜잭션으로 실행되게 했습니다. 이는 성능 최적화를 위해서도 권장됩니다. 예를 들어, Hibernate에서는 readOnly 트랜잭션의 경우 Flush 모드가 NEVER로 설정되므로, 더티 체크(변경된 객체를 확인하는 작업)를 생략하여 성능을 향상시킬 수 있습니다.

deleteInactiveUsers() 메서드는 데이터베이스에서 비활성화된 사용자를 삭제하는 쿼리를 실행하는데, @Modifying 어노테이션을 추가해 수정 쿼리임을 명시합니다. 이 메서드는 @Transactional 설정을 따로 지정하여 readOnlyfalse로 설정하고, 트랜잭션 내에서 실행되게 했습니다.

readOnly 플래그의 역할

readOnly = true로 설정하면 읽기 전용 트랜잭션으로 실행됩니다. 일부 데이터베이스는 읽기 전용 트랜잭션 내에서 INSERTUPDATE 쿼리를 거부할 수 있지만, 이는 데이터베이스에 따라 다릅니다. 대부분의 경우, readOnly 플래그는 JDBC 드라이버에 hint로 전달되어 성능을 최적화하는 용도로 사용됩니다. Spring은 이를 활용해 JPA 프로바이더에서 성능 최적화를 수행합니다.

 

'Spring Boot > Spring Data JPA' 카테고리의 다른 글

PESSIMISTIC_READ, PESSIMISTIC_WRITE  (0) 2024.10.22
Locking  (0) 2024.10.22
Projections  (0) 2024.10.22
Scrolling  (0) 2024.10.20
Configuring Fetch- and LoadGraphs  (0) 2024.10.20