Transaction Propagation은 한 트랜잭션이 동작하고 있을 때, 다른 트랜잭션이 동작하는 경우 어떻게 전파될지를 결정하는 속성입니다. Spring Framework에서 @Transaction 애노테이션을 사용하여 트랜잭션을 선언할 때 설정할 수 있으며, 트랜잭션 경계 설정과 실행 컨텍스트 관리를 유연하게 합니다.
Transaction Propagation의 주요 사용 시점
- 복잡한 비즈니스 로직 분리:
- 하나의 서비스 계층에서 여러 하위 서비스 계층을 호출할 때 각 계층의 트랜잭션 범위를 다르게 설정해야 하는 경우.
- 재사용성 향상:
- 공통 메서드를 다른 서비스에서 재사용하지만, 트랜잭션 전파 방식을 상황에 따라 다르게 적용해야 할 때.
- 롤백 처리 제어:
- 특정 하위 작업은 상위 트랜잭션과 독립적으로 처리되거나 롤백되기를 원하는 경우.
Transaction Propagation 옵션과 설명
REQUIRED | 기존 트랜잭션이 있으면 사용하고, 없으면 새 트랜잭션을 시작합니다. (기본값) | 예시: 상위 메서드에서 트랜잭션이 열려 있으면 이를 사용하고, 그렇지 않으면 새로 생성. |
SUPPORTS | 트랜잭션이 있으면 참여하지만, 없어도 트랜잭션 없이 실행됩니다. | 예시: 읽기 전용 서비스 메서드. |
MANDATORY | 반드시 상위 트랜잭션이 있어야 하며, 없으면 예외가 발생합니다. | 예시: 상위 트랜잭션에서만 호출해야 하는 메서드. |
REQUIRES_NEW | 항상 새로운 트랜잭션을 시작하고, 기존 트랜잭션을 일시 중단합니다. | 예시: 로깅, 감사 로그 저장 등 독립적으로 처리해야 하는 작업. |
NOT_SUPPORTED | 트랜잭션 없이 메서드를 실행하고, 기존 트랜잭션을 일시 중단합니다. | 예시: 트랜잭션 컨텍스트가 불필요한 대량 데이터 처리 작업. |
NEVER | 트랜잭션이 있으면 예외를 발생시키고, 없을 때만 실행됩니다. | 예시: 트랜잭션 환경에서 실행하면 안 되는 작업. |
NESTED | 기존 트랜잭션 안에서 별도의 중첩된 트랜잭션을 시작합니다. | 예시: 특정 하위 작업만 독립적으로 롤백 가능해야 하는 경우. |
REQUIRES_NEW 에 대하여
요청사항:
```매니저 등록 요청 시 로그를 남기고 싶어요! @Transactional의 옵션 중 하나를 활용하여 매니저 등록과 로그 기록이 각각 독립적으로 처리될 수 있도록 해봅시다. 매니저 등록과는 별개로 로그 테이블에는 항상 요청 로그가 남아야 해요. 매니저 등록은 실패할 수 있지만, 로그는 반드시 저장되어야 합니다.```
위 요구 사항을 충족하기 위해 요청사항을 만족하기 위해, 엔티티를 만들었고, 서비스와 레포지토리도 만들었습니다. 그 중 서비스만 기재합니다.
- 로그 서비스
@Service
@RequiredArgsConstructor
public class LogService {
private final LogRepository logRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logRequest(String action, String message, Long userId) {
Log log = new Log();
log.setRequestTime(LocalDateTime.now());
log.setAction(action);
log.setMessage(message);
log.setUserId(userId);
logRepository.save(log);
}
}
- 매니저 서비스
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ManagerService {
private final ManagerRepository managerRepository;
private final UserRepository userRepository;
private final TodoRepository todoRepository;
private final LogService logService;
@Transactional
public ManagerSaveResponse saveManager(UserDetailsImpl authUser, long todoId, ManagerSaveRequest managerSaveRequest) {
User user = authUser.getUser();
// 매니저 등록 로깅
logService.logRequest("saveManager", "Logging Manager", user.getId());
Todo todo = todoRepository.findById(todoId)
.orElseThrow(() -> new InvalidRequestException("Todo not found"));
if (todo.getUser() == null || !ObjectUtils.nullSafeEquals(user.getId(), todo.getUser().getId())) {
throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 유효하지 않거나, 일정을 만든 유저가 아닙니다.");
}
User managerUser = userRepository.findById(managerSaveRequest.getManagerUserId())
.orElseThrow(() -> new InvalidRequestException("등록하려고 하는 담당자 유저가 존재하지 않습니다."));
if (ObjectUtils.nullSafeEquals(user.getId(), managerUser.getId())) {
throw new InvalidRequestException("일정 작성자는 본인을 담당자로 등록할 수 없습니다.");
}
Manager newManagerUser = new Manager(managerUser, todo);
Manager savedManagerUser = managerRepository.save(newManagerUser);
return new ManagerSaveResponse(
savedManagerUser.getId(),
new UserResponse(managerUser.getId(), managerUser.getEmail())
);
}
//...
}
REQUIRES_NESTED 에 대하여
REQUIRES_NESTED 옵션을 활용해 보려고도 했지만
JPA의 구현체(예: Hibernate) 또는 데이터베이스 설정에서 **저장점(savepoint)**을 지원하지 않는다길래 포기했습니다.
***매니저 등록과 로그 등록 전부 성공하는 케이스를 테스트한 결과, 위와 같은 메시지가 나오며 매니저 등록은 되지만 로그 등록은 되지 않았습니다.
로그 등록은 성공, 매니저 등록은 실패하는 케이스를 테스트 했을 때도, 위와 같은 메시지가 나오며 로그 등록이 되지 않았습니다.***
NESTED 대신 REQUIRES_NEW 사용이 권장되는 이유
- REQUIRES_NEW는 저장점이 아닌 새로운 커넥션과 트랜잭션을 열기 때문에 JPA와 데이터베이스의 저장점 지원 여부에 의존하지 않습니다.
- 부모 트랜잭션의 상태와 관계없이 독립적으로 커밋됩니다.
'Spring > Spring 문법' 카테고리의 다른 글
N + 1 문제 해결, JPQL과 QueryDsl에서 JOIN FETCH (0) | 2024.11.26 |
---|---|
AWS S3 버킷 사용하기 (0) | 2024.11.25 |
QueryDsl - Projections 의 4가지 방식 (0) | 2024.11.21 |
Spring Security 적용 (0) | 2024.11.14 |
QueryDSL이란 무엇일까?? (0) | 2024.11.14 |