
안녕하세요, 하원입니다.
이번에는 제가 겪었던 transaction silently rolled back because it has been marked as rollback-only 오류에 대해 소개해 보겠습니다.
롤백 오류
프로젝트를 진행하면서 서비스 단 코드에서 transaction silently rolled back because it has been marked as rollback-only 오류를 겪었는데요, 오류 원인을 찾는데 꽤.. 오랜 시간이 걸렸습니다..!
// 포스터 전체 조회 (서비스 단)
public Page<PosterDto> getPosters(Status status, int page, int size) {
// 페이징 조건 추가
Pageable pageable = createPageable(page, size);
Member member;
try {
// Member 정보 조회 -> 유저가 없을 경우 GuestUserException throw
member = memberService.findMemberByEmail();
// 조건에 맞는 Poster 가져오기
return posterRepository.findByStatus(status, pageable)
.map(poster -> {
Boolean isLike = posterLikeRepository.findByMemberIdAndPosterId(member.getMemberId(), poster.getPosterId()).isPresent();
return new PosterDto(poster, isLike);
});
} catch (GuestUserException e) {
// GuestUserException에 대한 처리
// 게스트용 조회
return posterRepository.findByStatus(status, pageable)
.map(poster -> {
Boolean isLike = false;
return new PosterDto(poster, isLike);
});
}
}
memberService.findMemberByEmail()을 통해 유저를 조회 (유저가 없다면 GuestUserException throw)GuestUserException이 발생하면 catch문으로 잡아 게스트용 포스터 조회{
"message": "Transaction silently rolled back because it has been marked as rollback-only"
}
위와 같은 플로우를 예상하며 서비스 단에서 커스텀 Exception과 catch문을 사용하여 코드를 작성했는데 결과는.. 위와 같았습니다.
원인
// 커스텀 Exception
public class GuestUserException extends RuntimeException{
public GuestUserException(String message) {
super(message);
}
}
원인은 제가 커스텀 Exception에서 상속 받았던 RuntimeException이 Unchecked Exception으로 예외 발생 시 트랜잭션을 롤백(Rollback) 처리해야 한다는 특징이 있었습니다.
결국, catch문에서 GuestUserException을 잡아도 트랜잭션이 롤백 처리되면서 해당 트랜잭션은 실패 상태로 마치게 된 겁니다. 그래서 catch문에 있던 조회가 동작하지 않았던 것 같습니다.
그러면 Unchecked Exception이 무엇인지 간단하게 소개해 보겠습니다.
Checked Exception & Unchecked Exception
IOException, FileNotFoundException, SQLException, AWTExceptionRuntimeException, NullPointerException, AssertionError, ThreadDeath위의 내용처럼 RuntimeException은 Unchecked Exception에 해당하며, 예외 발생 시 트랜잭션을 롤백 처리해 버립니다.
저는 일부로 예외를 발생시켜서 따로 처리하려고 한 건데, 그러면 어떻게 처리해야 할까요?
예외 처리
// 포스터 전체 조회 (서비스 단)
public Page<PosterDto> getPosters(Status status, int page, int size) {
// 페이징 조건 추가
Pageable pageable = createPageable(page, size);
// Member 정보 -> 유저가 없을 경우 GuestUserException throw
Member member = memberService.findMemberByEmail();
// 조건에 맞는 Poster 가져오기
return posterRepository.findByStatus(status, pageable)
.map(poster -> {
Boolean isLike = posterLikeRepository.findByMemberIdAndPosterId(member.getMemberId(), poster.getPosterId()).isPresent();
return new PosterDto(poster, isLike);
});
}
위 코드처럼 서비스 단 코드에서 예외처리를 하지 않고, 그대로 컨트롤러 단에 전달해 주었습니다.
@GetMapping
public ApiResponse<GetPostersResponse> getPosters(
@RequestParam(name = "status", defaultValue = "ACTIVE") String status,
@RequestParam(name = "page", defaultValue = "0") int page,
@RequestParam(name = "size", defaultValue = "10") int size
) {
Status findStatus = Status.valueOf(status);
try {
// 포스터 전체 조회 실행
List<PosterDto> posters = posterService.getPosters(findStatus, page, size).getContent();
GetPostersResponse result = new GetPostersResponse(posters);
return new ApiResponse<>(ResponseMessage.POSTER_SUCCESS.getCode(), ResponseMessage.POSTER_SUCCESS.getMessage(), result);
} catch (GuestUserException e) {
// 게스트용 포스터 전체 조회 실행
List<PosterDto> posters = posterService.getGuestPosters(findStatus, page, size).getContent();
GetPostersResponse result = new GetPostersResponse(posters);
return new ApiResponse<>(ResponseMessage.POSTER_SUCCESS.getCode(), ResponseMessage.POSTER_SUCCESS.getMessage(), result);
}
}
서비스 단으로부터 전달받은 GuestUserException을 컨트롤러 단에서 catch문으로 처리해 주면 됩니다. 이때는 transaction silently rolled back because it has been marked as rollback-only 오류가 발생하지 않습니다.
서비스 단에서 컨트롤러 단으로 GuestUserException이 전달되면서 트랜잭션이 종료되었기 때문에, 컨트롤러의 catch문에서 새로운 트랜잭션을 열어 서비스 단의 메서드를 실행할 수 있게 됩니다.
트랜잭션 종료
- 서비스 단의 메서드가 완료되면 트랜잭션이 커밋 또는 롤백 처리 되면서 트랜잭션이 종료됩니다.
반성
저는 트랜잭션이 Service 계층과 Repository 계층에서 유지되며, Controller 계층으로 넘어오면 종료된다는 정도로만 알고 있었습니다.
하지만 이번 오류를 통해 Checked Exception과 Unchecked Exception 사이에 트랜잭션 롤백이라는 차이점이 존재한다는 것을 처음 알게 되었습니다. 오류 덕분에 트랜잭션의 동작 원리와 흐름을 자세히 학습하게 되었습니다. 트랜잭션을 가볍게 생각한 저를 반성하게 하는 것 같습니다.
마무리
역시 실제 프로젝트를 진행해야 많은 변수를 만나게 되는 것 같다. 이론 공부를 하면서 겪지 못했던 다양한 상황을 접하게 되면서 경험이 하나씩 쌓이고 있다.
현재 1개의 프로젝트를 대략 5개월 넘게 진행하고 있는데도, 여전히 생소한 오류 발생이 많다.
이런 오류들을 만날수록 성장하고 있다는 뜻 아닐까..?