@Transactional(propagation = Propagation.REQUIRES_NEW)

2024. 10. 10. 17:32·Java & Spring/트러블슈팅

주요 특징:

  1. 새로운 트랜잭션 생성: 메소드가 호출될 때 기존 트랜잭션이 있더라도 그것을 무시하고 항상 새로운 트랜잭션을 생성한다.
  2. 독립적인 트랜잭션: 이 새로운 트랜잭션은 기존 트랜잭션과 완전히 독립적으로, 새로 시작된 트랜잭션이 커밋되거나 롤백되더라도, 기존 트랜잭션에는 영향을 미치지 않는다.
  3. 원래 트랜잭션 중단: 새로운 트랜잭션이 끝날 때까지 기존 트랜잭션은 중단된다. 새로운 트랜잭션이 완료된 후에 기존 트랜잭션이 다시 이어진다.
  4. 부분 커밋: 메소드 실행 중에 새로운 트랜잭션이 성공적으로 커밋되면, 그 결과는 전체 트랜잭션의 성공 여부와 상관없이 데이터베이스에 영구적으로 반영된다. 즉, 새 트랜잭션이 커밋되면 그 변경 사항은 롤백되지 않는다.

□ 문제사항

    @Transactional
    public ManagerSaveResponse saveManager(AuthUser authUser, long todoId, ManagerSaveRequest managerSaveRequest) {
        // 일정을 만든 유저
        User user = User.fromAuthUser(authUser);

        try {
            createLog(todoId, managerSaveRequest.getManagerUserId(), user.getId());
        } catch (Exception e) {
            logger.error("Failed to save log", e);
            // 예외를 던지지 않음으로써 롤백 방지
        }

        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())
        );
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void createLog(Long todoId, Long managerId, Long requestUserId) {
        Log saveLog = new Log(todoId, managerId, requestUserId);
        logRepository.save(saveLog);
    }
  • saveManager실행 시 createLog를 실행하여 Log에 대한 기록을 남기려고함
  • saveManager이 실패하게되면 롤백이되면서 Log가 DB에 저장되지못함

 

  • 트랜잭션 전파: createLog 메소드는 REQUIRES_NEW 전파 모드를 사용하므로 saveManager와는 별도의 트랜잭션에서 실행된다. 즉, saveManager 트랜잭션과는 독립적으로 로그를 저장하는 트랜잭션이 시작되고 커밋될 수 있다.
    • 하지만 이 전파 모드는 createLog 트랜잭션이 완전히 독립적으로 동작할 때만 의미가 있다.
    • 동일한 클래스 내에서 saveManager가 createLog를 호출하면, Spring은 이를 프록시 기반으로 처리하기 때문에 실제로는 별도의 트랜잭션이 생성되지 않고 하나의 트랜잭션으로 동작할 수 있다.
  • 내부 호출 문제: Spring의 AOP 트랜잭션 관리 방식은 동일한 클래스 내에서의 메소드 호출에 대해 프록시를 생성하지 않는다. 즉, saveManager가 createLog를 호출할 때 트랜잭션 전파 규칙이 적용되지 않아, REQUIRES_NEW가 제대로 작동하지 않고, 상위 트랜잭션이 롤백되면 하위 트랜잭션도 롤백된다.
  • 트랜잭션 롤백: saveManager에서 예외가 발생하면 기본적으로 @Transactional은 그 예외를 롤백 처리, 이때 createLog가 상위 트랜잭션의 영향을 받아서 롤백되므로, 로그가 데이터베이스에 저장되지 않게 된다.

 

-> 결국 해당 문제도 지난번에 겪었던 self-invocation문제와 같은상황이다.

https://djhelloworld.tistory.com/164

 

Self-invocation(자기 호출)

Self-invocation(자기 호출)이란? Self-invocation(자기 호출)은 클래스 내의 메서드가 동일 클래스의 다른 메서드를 호출하는 상황에서 발생Spring 프레임워크에서 @Transactional 어노테이션을 사용할 때,

djhelloworld.tistory.com

 

□  해결방법

LogService를 분리하여 작업을 처리함

@Transactional
    public ManagerSaveResponse saveManager(AuthUser authUser, long todoId, ManagerSaveRequest managerSaveRequest) {
        // 일정을 만든 유저
        User user = User.fromAuthUser(authUser);
        Log saveLog = logRepository.findById(logService.createLog(todoId, managerSaveRequest.getManagerUserId(), user.getId()))
                .orElseThrow(()-> new RuntimeException("해당 로그를 찾지 못했습니다."));

        try {
            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);

            saveLog.setSuccess(true);

            return new ManagerSaveResponse(
                    savedManagerUser.getId(),
                    new UserResponse(managerUser.getId(), managerUser.getEmail())
            );
        }catch (Exception e){
            throw e;
        }
    }

 

  • try-catch를 사용해서 Log에 API성공 확인을 위한 필드를 추가 후 처리

'Java & Spring > 트러블슈팅' 카테고리의 다른 글

MultipleBagFetchException(두개의 Fetch Join)  (0) 2024.10.08
Self-invocation(자기 호출)  (0) 2024.09.05
'Java & Spring/트러블슈팅' 카테고리의 다른 글
  • MultipleBagFetchException(두개의 Fetch Join)
  • Self-invocation(자기 호출)
DJ.Kang
DJ.Kang
백엔드 개발 기록 블로그
  • DJ.Kang
    DJ Hello World
    DJ.Kang
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 이론공부
        • 개념
        • 정보처리기사 필기
        • 정보처리기사 실기 기출
        • 네트워크관리사 2급
        • SQLD
      • 시스템설계
      • Java & Spring
        • TIL
        • 트러블슈팅
        • 고도화
        • 알고리즘
        • 코딩테스트
        • Java
        • Spring
        • Thymeleaf
      • 프로젝트
        • coin-trading
        • 트러블슈팅
      • Docker
      • 웹
      • git & github
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    java 멤버
    java super
    java 에라토스테네스의 체
    개발로드맵
    java two-pointer
    java arrays.copyofrnage()
    프로그래머스 java 기초 트레이닝
    java 세수의합
    자료구조
    프로그래머스 java 기초트레이닝
    java 제어자
    데이터 크기
    java
    java 유클리드 호제법
    Java 생성자
    java기초
    java 메서드
    java enhance switch
    데이터 타입
    Java this
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
DJ.Kang
@Transactional(propagation = Propagation.REQUIRES_NEW)
상단으로

티스토리툴바