Transactional Rollback 문제가 왜 발생했을까

Mando·2024년 8월 18일
1

에러 발생

프론트에서 500에러가 터진다고 해서 로그를 확인해보니 Transactional Rollback문제가 발생하고 있음을 확인했다.

Transaction rolled back because it has been marked as rollback-only
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

확인 결과 getAccessibility(해당 팀빌딩에 접근가능한지 여부)를 확인하는 API를 호출할 때 해당 문제가 발생했다.

하지만, 로직상의 문제는 없었다.

	@Override
    @Transcational(readyOnly=true)
	public Accessibility getAccessibility(Long memberId) {
		try {
			TeamBuildingModel model = findProgressTeamBuilding();
			queryTeamBuildingTargetService.getTarget(memberId, model.getId());
			return Accessibility.JOINABLE;
		} catch (EndTeamBuildingException | NotFoundTargetTeamBuildingException ex) {
			return Accessibility.NONJOINABLE;
		}
	}
	@Transcational(readyOnly=true)
	public TeamBuildingTargetModel getTarget(Long memberId, Long teamBuildingId) {
		return teamBuildingTargetRepository
				.findByTeamBuildingIdAndMemberId(teamBuildingId, memberId)
				.map(entityConverter::from)
				.orElseThrow(NotFoundTargetTeamBuildingException::new);
	}

내가 의도한 바는 자식 메서드 안에서 RuntimeException이 발생하여 트랜젝션을 롤백시키겠지만 부모 메서드에서 해당 에러를 잡기 때문에 롤백이 없이 커밋이 될 것이라고 생각했다.

기존 트랜젝션에 참여한다는 의미

@Transactional의 기본 전파(propagation)속성은 PROPAGATION_REQUIRED이다.
즉, 기존에 존재하는 트랜젝션에 참여한다는 의미이다.

나는 여기까지로만 생각을 했다.
즉 트랜젝션이 걸려있는 최상단 부모 메서드가 트랜젝션을 시작하고 해당 메서드가 종료되면 트랜젝션 또한 종료될 것이라고 생각했다.(단 한번의 시작과 단 한번의 종료)

기존 트랜젝션에 참여한다는 의미는
각 트랜젝션을 시작하고 완료하지만, commit, rollback과 같은 최종완료 처리만 최초 트랜젝션이 일어날 때 일어난다는 것이였다.

문제 원인

  • 자식 메서드에서 RuntimeException 에러가 터지고 해당 트랜젝션은 완료처리가 된다.
  • RuntimeException 때문에 트랜젝션을 롤백할지 결정하는 규칙을 적용한다. 하지만 별도로 지정된 규칙이 없어 default 규칙을 적용한다.
  • default 규칙에 의해 참여한 트랜잭션 실패를 선언하고 rollback-only 마킹을 한다
  • 부모 메서드에서 자식 메서드에서 발생한 예외를 잡았다.
  • 부모 트랜젝션(최초 트랜젝션 메서드)가 완료처리된다.
  • 부모 트랜젝션 메서드에서 별 문제가 없어 해당 트랜젝션에서 commit을 하려고 한다.
  • 하지만 roll-back only가 마킹을 확인하고 롤백을 한다.

해결책

별도의 트랜젝션으로 분리하여 rollback여부가 true가 되어도 부모 트랜젝션이 영향을 받지 않도록 하는 방식으로 해결했다.

[BE/FIX] 별도의 트랜젝션으로 실행하도록 트랜젝션 분리 #282

	@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
	public TeamBuildingTargetModel getTarget(Long memberId, Long teamBuildingId) {
		return teamBuildingTargetRepository
				.findByTeamBuildingIdAndMemberId(teamBuildingId, memberId)
				.map(entityConverter::from)
				.orElseThrow(NotFoundTargetTeamBuildingException::new);
	}

0개의 댓글