@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
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 이론공부
        • 정보처리기사
      • 시스템설계
      • Java & Spring
        • TIL
        • 트러블슈팅
        • 고도화
        • 알고리즘
        • 코딩테스트
        • Java
        • Spring
        • Thymeleaf
      • 프로젝트
        • coin-trading
        • 트러블슈팅
      • Docker
      • DB
      • AWS
      • CI-CD
      • 웹
      • git & github
      • 구인공고분석
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

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

  • 최근 글

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

티스토리툴바