- Service에서 @Transactional의 적용
- 트랜잭션이란 무엇인가?
- @Transactional을 CRUD 중 어디에 적용해야 할까?
- 프로젝트 Service 코드에 적용한다면?
트랜잭션은 DB의 상태를 변화시키기 위해 수행하는 작업의 단위
DB에서는 데이터의 읽기, 쓰기, 수정 작업이 동시에 수행된다. 이 때, 서로의 작업 결과가 데이터에 영향을 미쳐 의도하지 않은 데이터가 생길 수 있다.
Ex) 두 사용자가 동시에 동일한 계좌에서 돈을 이체하려는 상황을 가정한다.
- 사용자 1과 사용자 2가 각각 100만원을 계좌 A에서 출금하려고 한다.
- 계좌 A의 잔액은 200만원이다.
이 두 작업이 동시에 처리될 때, 다음과 같은 문제가 발생할 수 있다.
- 사용자 1과 2는 계좌 A에 200만원이 있는 것을 확인하고 출금을 시도한다.
- 사용자 1은 계좌 A에서 100만원 출금 요청을 보낸다.
- 사용자 2도 계좌 A에서 100만원 출금 요청을 보낸다.
- 사용자 1과 2의 이체가 동시에 이루어지면서, 계좌에서 100만원씩 빠져나가면 총 200만원이 출금되어 잔액이 없어야 하지만, 실제 계좌에는 100만원이 남게 된다.
위 예시에서, 두 작업 모두 잔액에서 100만원을 차감하는 수정이 이루어 질 것이다. 하지만 두 작업 모두 수행 시점에서 잔액이 200만원이었기 때문에, 결국 잔액이 100만원이 되는 데이터 불일치가 발생한다.

이러한 문제를 해결하기 위해 DB에서는 트랜잭션을 사용한다. 트랜잭션은 ACID라는 속성을 가지고 있다.
- 원자성(Atomicity)
- 트랜잭션 내의 작업이 모두 성공하면 Commit되어 DB에 반영되고, 하나라도 실패하면 Rollback되어 이전 상태로 돌아간다.
- 일관성(Consistency)
- 트랜잭션의 작업 처리 결과가 항상 일관된 데이터베이스 상태를 보존하며, 데이터베이스의 모든 무결성 제약조건을 만족한다.
- 격리성(Isolation)
- 여러 개의 트랜잭션이 동시에 실행될 때, 트랜잭션은 서로를 참조하거나 영향을 미칠 수 없다.
- 동시에 실행되는 트랜잭션들이 서로 격리되어 마치 순차적으로 실행되는 것처럼 보인다.
- 지속성(Durability)
- 트랜잭션이 commit되면 그 데이터는 DB에 영구적으로 반영된다.
위의 예시에서는 격리성(Isolation)이 문제를 해결하는 데 도움을 준다.

트랜잭션은 데이터의 무결성과 일관성을 보장하고, 동시성 문제를 해결하는 데 도움을 준다.
Spring에서는 @Transactional 어노테이션을 통해 메서드 내의 DB 작업들이 하나의 트랜잭션으로 묶이도록 만들어 준다.
Repository의 메서드들은 @Transactional이 자체 적용되어 있다. 그렇다면 왜 @Transactional 어노테이션을 적용해야 할까? 현재 프로젝트에서는 영속성 컨텍스트를 유지시켜 변경 감지(Dirty Checking)를 사용하기 위해 사용했다.
public void updateUser(Long id, String newName) {
User user = userRepository.findById(id).get();
// findById는 내부적으로 트랜잭션이 있지만
user.setName(newName);
// 이 시점에서는 트랜잭션이 이미 종료되어
// 변경 감지가 동작하지 않음
}
@Transactional을 지정하지 않으면 변경 감지가 일어나지 않는다. @Transactional이 지정되어 있어야 메서드가 종료될 때 까지 영속성 컨텍스트가 유지되기 때문이다.
그렇다면 update 메서드에만 @Transactional을 적용하면 될까? 아래 내용은 이와 관련한 피드백이다.
@Transactional(readOnly=true)옵션을 두고, create, update, delete 메서드에만 @Transactional을 지정@Transactional을 지정하는 범위는 우아한형제들 개발자와 @Transactional 컨벤션을 어떻게할지 의논해서 결정한 내용이라고 한다.
@Transactional(readOnly = true)
//@어노테이션 ...
public class 000Service {
//필드 ...
public 읽기() {
}
@Transactional
public 쓰기() {
}
@Transactional
public 업데이트() {
}
@Transactional
public 삭제() {
}
}
현재는 update가 발생하는 메서드에서 변경 감지(Dirty Checking)를 사용하기 위해 update와 delete 메서드에 @Transactional 어노테이션을 사용하고 있다. (delete 메서드는 soft delete를 위해 DB의 delete_date와 delete_by 필드를 업데이트 하므로 업데이트라고 판단했다.)
@Transactional
public UsernameResponseDto updateUser(@Valid UpdateUserRequestDto requestDto, String username) {
User user = userRepository.findById(username)
.orElseThrow(() -> new NullPointerException(ExceptionMessage.USER_NOT_FOUND.getMessage()));
checkDeletedUser(user);
user.update(requestDto, passwordEncoder);
userRepository.save(user);
return new UsernameResponseDto(user.getUsername());
}
피드백 내용을 바탕으로, 클래스에 @Transactional(readOnly=true)을 지정하고, create메서드에도 @Transactional을 지정하는 방향으로 리팩토링을 진행할 예정이다.