[Spring/JPA] @Transactional을 사용하는 이유에 대하여 (부제. JPA Dirty Checking)

resilient

·

2023. 3. 2. 16:22

728x90
반응형

이번 포스팅에서는 JPA를 사용한다면 한 번쯤은 사용해 봤을 법한 @Transactional 어노테이션에 대해서 알아보려고 합니다.

 

0. Transaction이란?

 

그럼 먼저 Transaction에 대해서 간단하게 짚고 넘어가 보겠습니다.

 

Transaction이란 한 문장으로 정의해 보면 데이터베이스의 상태를 변화시키기 위해서 수행하는 작업의 단위를 의미합니다. 일반적으로 알고 있는 SELECT, INSERT, UPDATE, DELETE을 사용해서 데이터베이스를 사용하곤 하는데요. 데이터베이스를 사용할 때 한번 접근해서 수행하는 작업의 단위라고 할 수 있죠.

 

트랜잭션의 특징으로는 안전성을 보장하기 위해 필요한 4가지 성질이 있습니다. (ACID 성질)

 

1. 원자성(Atomicity) : 트랜잭션이 한번 실행될 때, 데이터베이스에 모두 반영되던가, 모두 반영되지 않아야 합니다.

2. 일관성(Consistency) : 트랜잭션의 작업 처리 결과는 항상 일관성이 있어야 합니다.

3. 독립성(Isolation) : 둘 이상의 트랜잭션이 동시에 실행될 때, 다른 트랜잭션의 연산에 끼어들 수 없습니다.

4. 지속성(Durability) : 트랜잭션이 성공적으로 완료되었을 경우, 결과는 영구적으로 반영이 되어야만 합니다.

 

1. JPA 더티 체킹 (Dirty Checking) 이란?

 

부제와 연관된 내용이기도 합니다.

 

먼저 아래 update를 하는 메소드를 살펴보겠습니다.

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;

    @Override
    public UserDTO.UserSimpleInfoResponse updateUser(Long userId, UserDTO.UpdateUserRequest request) {
        Users user = userRepository.findById(userId).orElseThrow(() -> new EntityNotFoundException("존재하지 않는 사용자 입니다."));
        user.setNickname(request.getNickname());
        user.setProfileImage(request.getProfileImage());
//        userRepository.save(user);
        return new UserDTO.UserSimpleInfoResponse(user);
    }
}

 

user에 setter로 setNickname과 setProfileImage를 이용해서 값을 넣어준 뒤, save를 해야 일반적으로 생각하는 update가 완성이 됩니다. 물론 여기서도 update를 안 쓰고 set으로 update 기능을 구현할 수 있는 이유는 JPA 더티 체킹 때문입니다.

 

위 update 기능의 메소드를 보면 별도로 데이터베이스에 save를 하지 않습니다. 이유가 바로 Dirty Checking 때문입니다.

 

Dirty란 부정적인 의미가 아닌 '상태의 변화가 생겼다' 정도의 의미로 이해하시면 됩니다. 그렇다면 Dirty Checking은? '상태 변경 검사' 겠죠.

 

JPA에서는 트랜잭션이 끝나는 시점에 변화가 생긴 모든 엔티티들을 데이터베이스에 자동으로 반영해 줍니다.

 

JPA에서 영속성 콘텍스트가 관리하고 있는 엔티티를 조회하면 해당 엔티티의 조회 상태로 '스냅샷'을 만들어놓고, 트랜잭션이 끝나는 시점에 스냅샷과 비교해서 변화가 생긴다면 update를 해서 데이터베이스로 전달하게 되죠.

 

2. @Transactional을 사용했을 때 save가 필요 없는 이유

 

위에서 Dirty checking에 대해서 살펴봤는데요. 그러면 위의 updateUser 메소드에서도 당연히 save가 필요하지 않겠죠? 그래서 save를 주석 처리하고 실행시키면 데이터베이스에 반영이 되지 않습니다.

 

이유는 바로 언제 Dirty Checking을 하느냐와 관련이 있는데요. Dirty Checking은 transaction이 commit 될 때 작동합니다. 그렇다면 transaction이 실행되도록 해줘야 하는데 여기서 @Transactional 어노테이션을 사용하면 됩니다.

 

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;

    @Override
    @Transactional
    public UserDTO.UserSimpleInfoResponse updateUser(Long userId, UserDTO.UpdateUserRequest request) {
        Users user = userRepository.findById(userId).orElseThrow(() -> new EntityNotFoundException("존재하지 않는 사용자 입니다."));
        user.setNickname(request.getNickname());
        user.setProfileImage(request.getProfileImage());
        return new UserDTO.UserSimpleInfoResponse(user);
    }
}

 

위 코드와 같이 Transactional 어노테이션을 붙여주면 Dirty Checking을 하게 되고, 데이터베이스에 commit을 해서 수정된 사항을 save 없이도 반영할 수 있도록 합니다.

 

3. @Transactional의 롤백 기능

 

update를 할 경우를 제외하고 save 기능을 구현할 때도 Transactional 어노테이션을 사용하는 경우가 종종 있습니다. 이러한 경우에는 위 Transaction 안전성 보장 성질 중 원자성 성질 때문이라고 할 수 있습니다. 

 

보통 save를 할 때는 연관관계가 복잡하게 얽혀 있는 경우가 대부분일 텐데요. 

 

연관관계에 의해 다른 엔티티들도 save 나 update를 해야 하는 등의 경우에서 transaction이 완료되면 전체가 반영되어야 하고 실패하면 전체를 취소시켜서 이전 상황으로 롤백을 꼭 해줘야 합니다.

 

@Transactional 어노테이션을 사용한 트랜잭션 안에서 예외가 발생했을 경우, 해당 예외가 런타임 예외일 경우, 자동적으로 롤백이 발생하지만 아닐 경우 롤백이 되지 않습니다. 이러한 경우에는 @Transactional 어노테이션에서의 rollbackFor 옵션을 사용해서 해당 체크 예외를 적어 줘야 합니다. 아래와 같이 코드를 작성하면 됩니다.

@Transactional(rollbackFor=CustomException.class)
public void updateuser(UserDTO dto) throws CustomException {
	// 로직 구현
}

 

4. @Transactional의 활용법 및 사용 위치

 

트랜잭션을 중구난방으로 메소드마다 덕지덕지 붙여놓은 것은 좋지 않습니다. 일반적으로는 비즈니스 로직이 담겨있는 서비스 레이어에서 트랜잭션 처리를 많이 합니다. 이유는 데이터 저장을 하는 레포지토리 레이어에서 읽어온 데이터들을 읽기, 수정, 저장 등의 작업을 하는 곳이 서비스 레이어이기 때문이죠. 아래와 같이 클래스에 @Transactional을 붙여주면 메소드까지 모두 적용이 됩니다.

 

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;

    @Override
    public UserDTO.UserSimpleInfoResponse updateUser(Long userId, UserDTO.UpdateUserRequest request) {
        Users user = userRepository.findById(userId).orElseThrow(() -> new EntityNotFoundException("존재하지 않는 사용자 입니다."));
        user.setNickname(request.getNickname());
        user.setProfileImage(request.getProfileImage());
        return new UserDTO.UserSimpleInfoResponse(user);
    }
}

 

위 코드에서 @Transactional(readOnly = true)를 사용했는데 아래 게시물에 사용하는 이유에 대해서 정리를 해놨습니다.

 

[Spring / TIL] @Transactional(readOnly=true) 가 꼭 필요한가?

Spring 학습을 위해 프로젝트를 진행하던 도중 조회한 값을 return 해주는 메소드에 당연하게 @Transactional(readOnly=true) 어노테이션을 사용했습니다. 막연하게 롤백(rollback) 때 사용하니까 @Transactional

resilient-923.tistory.com

 

4-1. @Transactional(readOnly = true)

 

추가적으로 @Transactional(readOnly = true)를 공통으로 사용해야 하는 중요한 이유를 한 가지 더 알게 되었습니다.

 

바로 트랜잭션 어노테이션이 없을 경우 @OneToMany, @ManyToMany 등 Lazy loding(지연로딩)을 Default로 사용하는 엔티티들을 정상적으로 조회할 수 없습니다.

 

이유는 지연 로딩의 동작 방식을 생각해 보면 알 수 있습니다.

 

먼저 JPA 구현체들은 프록시 패턴을 통해서 객체를 조회할 때 연관된 객체를 바로 조회하지 않고 실제로 사용할 때만 조회를 합니다.

프록시를 사용해서 조회할 경우에는 해당 객체에 접근 할 때 조회 하겠다고 요청을 해야 하죠. 이것이 바로 Lazy loading(지연로딩) 인데요.

 

Transaction이 붙어있지 않을 경우, 준영속 상태에 있는 엔티티들은 지연로딩을 할 수 없습니다. 지연로딩(Lazy loading)을 사용해서 프록시 객체로 존재했을 때, 해당 객체에서 실제로 값을 뽑으려고 하는 행위는 불가능한 것이죠.

 

따라서 트랜잭션이 있어야 지연로딩(Lazy loading)이 필요한 엔티티들을 정상 조회 할 수 있습니다.

 

위 모든 상황들을 고려했을 때, 클래스 레벨에는 공통적으로 많이 사용하는 읽기 전용 트랜잭션을 추가해 주고, 수정 및 삭제, 저장 기능이 있는 메소드에 별도로 @Transactional 어노테이션을 추가해 주는 게 효율적이라고 볼 수 있습니다.

 

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserServiceImpl implements UserService {

    private final UserRepository userRepository;

    @Override
    @Transactional
    public UserDTO.UserSimpleInfoResponse updateUser(Long userId, UserDTO.UpdateUserRequest request) {
        Users user = userRepository.findById(userId).orElseThrow(() -> new EntityNotFoundException("존재하지 않는 사용자 입니다."));
        user.setNickname(request.getNickname());
        user.setProfileImage(request.getProfileImage());
        return new UserDTO.UserSimpleInfoResponse(user);
    }
}

 

5. 정리

 

이번 포스팅에서는 제가 약간은 무지성으로 사용했던 @Transactional 어노테이션을 왜 사용해야 하는지에 대해서 약간은 구체적으로 살펴봤습니다. @Transactionl이 꼭 필요한 메소드에만 제공하는 옵션들을 효율적으로 사용해서 최선의 퍼포먼스를 내는 연습을 많이 해봐야겠습니다.

 

긴 글 읽어주셔서 감사합니다.

 

 

반응형