티스토리 뷰

728x90
반응형

Spring에서 Transaction Propagation은 접파 옵션을 의미합니다. 전파 옵션이라는 것은 트랜잭션을 시작하거나 기존 트랜잭션에 참여하는 방법에 대해 결정하는 속성값이라고 생각하면 됩니다. 즉 트랜잭션의 흐름을 컨트롤하는 속성값입니다.

 

@Transactional 전파 옵션의 종류

하나씩 차근차근 살펴보겠습니다. 👊

public enum Propagation {

	REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),

	SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

	MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),

	REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),

	NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),

	NEVER(TransactionDefinition.PROPAGATION_NEVER),

	NESTED(TransactionDefinition.PROPAGATION_NESTED);

	private final int value;

	Propagation(int value) {
		this.value = value;
	}

	public int value() {
		return this.value;
	}
}

 

💡REQUIRED(default)

  • 이미 시작된 트랜잭션이 있으면 참여하고 없으면 새로운 트랜잭션을 시작합니다.

💡REQUIRES_NEW

  • 항상 새로운 트랜잭션을 시작합니다. 이미 진행중인 트랜잭션이 있으면 해당 트랜잭션을 잠시 보류시킵니다.

💡SUPPORTS

  • 이미 시작된 트랜잭션이 있으면 참여하고 없으면 트랜잭션 없이 진행합니다.
  • 현재 트랜잭션을 유지하고, 진행중인 트랜잭션이 없으면 트랜잭션을 만들지 않고 진행합니다. 부모 메서드에서 트랜잭션을 시작하였다면 트랜잭션이 이어지지만 없다면 트랜잭션 없이 진행됩니다. 자식 메서드에서 Exception이 발생한다면 부모 메서드에서 정의한 트랜잭션에 의해 롤백 여부가 결정됩니다.

💡NESTED

  • 부모 트랜잭션의 커밋과 롤백으로 인해 자식 트랜잭션에 영향을 미치지만 반대로 자식 트랜잭션의 커밋과 롤백에 대해서는 부모 트랜잭션에게 영향을 주지 않습니다.

💡MANDATORY

  • REQUIRED와 비슷하게 이미 시작된 트랜잭션이 있으면 참여합니다. 반면에 트랜잭션이 시작된게 없으면 새로 시작하지 않고 예외를 발생시킵니다. 혼자서는 독립적으로 트랜잭션을 진행하면 안되는 경우 사용합니다.

💡NOT_SUPPORTED

  • 트랜잭션을 사용하지 않게 합니다. 이미 진행중인 트랜잭션이 있다면 해당 트랜잭션을 보류시킵니다.

💡NEVER

  • 트랜잭션을 사용하지 않도록 강제합니다. 이미 진행중인 트랜잭션이 존재한다면 해당 트랜잭션이 존재하면 안된다라는 예외를 발생시킵니다.

 

REQUIRED(default)에 대해


@Transactional REQUIRED인 경우

💡자식에서 문제가 발생한 경우

  • 부모에서 예외가 발생하지 않고 자식에서 예외가 발생한 경우 
  • 자식에서 예외가 발생했더라도 부모의 트랜잭션을 같이 사용하므로 둘 다 롤백이 됩니다.
@Service
@RequiredArgsConstructor
public class ParentService {

    private final ParentRepository parentRepository;
    private final ChildService childService;

    @Transactional
    public void parentSave() {
        Parent parent = new Parent("홍길동");
        parentRepository.save(parent);
        childService.childSave(parent);
    }
}

@Service
@RequiredArgsConstructor
public class ChildService {

    private final ChildRepository childRepository;
    
    @Transactional(propagation = Propagation.REQUIRED) // 주목
    public void childSave(Parent parent) {
        Child child = new Child("이순신");
        child.setParent(parent);
        childRepository.save(child);

        throw new RuntimeException("자식 저장 중 예외 발생!");
    }
}

 

💡부모에서 문제가 발생한 경우

  • 부모에서 예외가 발생하더라도 자식에서는 부모의 트랜잭션을 공유하므로 둘 다 롤백이 됩니다.
@Service
@RequiredArgsConstructor
public class ParentService {

    private final ParentRepository parentRepository;
    private final ChildService childService;

    @Transactional
    public void parentSave() {
        Parent parent = new Parent("홍길동");
        parentRepository.save(parent);
        childService.childSave(parent);
        throw new RuntimeException("부모 저장 중 예외 발생!");
    }
}

@Service
@RequiredArgsConstructor
public class ChildService {

    private final ChildRepository childRepository;
    
    @Transactional(propagation = Propagation.REQUIRED)
    public void childSave(Parent parent) {
        Child child = new Child("이순신");
        child.setParent(parent);
        childRepository.save(child);
    }
}

 

REQUIRES_NEW에 대해


@Transactional REQUIRES_NEW인 경우

💡자식에서 문제가 발생한 경우

  • REQUIRES_NEW의 트랜잭션은 항상 새로운 트랜잭션을 시작합니다. 그리고 기존 실행중인 트랜잭션이 있다면 보류를 시킵니다.
  • 여기서 자식에서 예외가 발생하여도 부모는 정상적으로 커밋이 됩니다.
  • 하지만 아래 코드는 부모, 자식 모두 롤백이 됩니다. 왜 그럴까요?
  • 해당 이유는 자식에서 Unchecked Exception을 throw하고 있기 때문입니다. Unchecked Exception throw -> parentSave()로 넘겨서 결국 parentSave() 메서드에서도 Unchecked Exception이 발생하기 때문에 부모도 롤백이 됩니다.
@Service
@RequiredArgsConstructor
public class ParentService {

    private final ParentRepository parentRepository;
    private final ChildService childService;

    @Transactional
    public void parentSave() {
        parentRepository.save(new Parent("홍길동"));
        childService.childSave();
    }
}

@Service
@RequiredArgsConstructor
public class ChildService {

    private final ChildRepository childRepository;
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void childSave() {
        Child child = new Child("이순신");
        childRepository.save(child);
        throw new RuntimeException("자식 저장 중 예외 발생!");
    }
}

 

💡자식에서 문제가 발생하고 부모에서 try-catch한 경우

  • 자식에서 Unchecked Exception을 throw하고 자식을 호출한 부모에서 try-catch로 throw된 Exception을 잡고 있는 경우에는 자식에서 발생한 트랜잭션이 롤백의 대상이라고 마킹이되며 부모는 정상적으로 커밋이 되고 자식은 롤백이 됩니다.
@Service
@RequiredArgsConstructor
public class ParentService {

    private final ParentRepository parentRepository;
    private final ChildService childService;

    @Transactional(rollbackFor = Exception.class)
    public void parentSave() {

        try {
            parentRepository.save(new Parent("부모"));
            childService.childSave();
        } catch (RuntimeException e) {
            e.printStackTrace();
        }
    }
}

@Service
@RequiredArgsConstructor
public class ChildService {

    private final ChildRepository childRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void childSave() {
        Child child = new Child("자식");
        childRepository.save(child);
        throw new RuntimeException("자식 저장 중 예외 발생!");
    }
}

 

💡자식에서 문제가 발생하고 자식에서 try-catch한 경우

  • 자식에서 발생한 Unchecked Exception을 try-catch를 사용하여 잡고 있으므로 자식에서도 정상적으로 커밋이 되고 호출한 부모도 커밋이 이루어집니다.
@Service
@RequiredArgsConstructor
public class ParentService {

    private final ParentRepository parentRepository;
    private final ChildService childService;

    @Transactional(rollbackFor = Exception.class)
    public void parentSave() {
        parentRepository.save(new Parent("부모"));
        childService.childSave();
    }
}
@Service
@RequiredArgsConstructor
public class ChildService {

    private final ChildRepository childRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void childSave() {
        try {
            Child child = new Child("자식");
            childRepository.save(child);
            throw new RuntimeException("자식 저장 중 예외 발생!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

💡부모에서 문제가 발생한 경우

  • 자식에서는 정상적으로 커밋이 되지만 부모에서는 롤백이 됩니다.
@Service
@RequiredArgsConstructor
public class ParentService {

    private final ParentRepository parentRepository;
    private final ChildService childService;

    @Transactional(rollbackFor = Exception.class)
    public void parentSave() {
        parentRepository.save(new Parent("부모"));
        childService.childSave();
        throw new RuntimeException("부모 저장 중 예외 발생!");
    }
}

@Service
@RequiredArgsConstructor
public class ChildService {

    private final ChildRepository childRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void childSave() {
        Child child = new Child("자식");
        childRepository.save(child);
    }
}

 

SUPPORTS에 대해


💡자식에서 문제가 발생한 경우

  • 자식에서 Unchecked Exception을 throw하는 경우 부모의 트랜잭션 정책에 의해 롤백이 결정됩니다. @Transactional은 예상치 못한(Unchecked Exception) 예외에 대해서는 롤백을 하므로 해당 예제는 롤백의 대상이 됩니다.
@Service
@RequiredArgsConstructor
public class ParentService {

    private final ParentRepository parentRepository;
    private final ChildService childService;

    @Transactional(rollbackFor = Exception.class)
    public void parentSave() {
        parentRepository.save(new Parent("부모"));
        childService.childSave();
    }
}

@Service
@RequiredArgsConstructor
public class ChildService {

    private final ChildRepository childRepository;

    @Transactional(propagation = Propagation.SUPPORTS, rollbackFor = Exception.class)
    public void childSave() {
        Child child = new Child("자식");
        childRepository.save(child);
        throw new RuntimeException("자식 저장 중 예외 발생!");
    }
}

 

💡자식에서 문제가 발생하고 부모에서 try-catch한 경우

  • 자식에서 발생한 Unchecked Exception을 부모의 try-catch문에서 잡을거 같지만 생각지 못한 예외가 발생하게 됩니다.
  • Transaction silently rolled back because it has been marked as rollback-only 발생!!
@Service
@RequiredArgsConstructor
public class ParentService {

    private final ParentRepository parentRepository;
    private final ChildService childService;

    @Transactional(rollbackFor = Exception.class)
    public void parentSave() {
        try {
            parentRepository.save(new Parent("부모"));
            childService.childSave();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

@Service
@RequiredArgsConstructor
public class ChildService {

    private final ChildRepository childRepository;

    @Transactional(propagation = Propagation.SUPPORTS, rollbackFor = Exception.class)
    public void childSave() {
        Child child = new Child("자식");
        childRepository.save(child);
        throw new RuntimeException("자식 저장 중 예외 발생!");
    }
}

 

MANDATORY에 대해


💡부모에서 @Transactional 어노테이션을 사용하지 않은 경우

  • No existing transaction found for transaction marked with propagation 'mandatory' 예외가 발생하면서 기존 트랜잭션을 찾을 수 없다고 알려줍니다.
  • 하지만 부모의 데이터는 롤백이 되지않고 커밋이 됩니다...?
@Service
@RequiredArgsConstructor
public class ParentService {

    private final ParentRepository parentRepository;
    private final ChildService childService;

    public void parentSave() {
        parentRepository.save(new Parent("부모"));
        childService.childSave();
    }
}

@Service
@RequiredArgsConstructor
public class ChildService {

    private final ChildRepository childRepository;

    @Transactional(propagation = Propagation.MANDATORY, rollbackFor = Exception.class)
    public void childSave() {
        Child child = new Child("자식");
        childRepository.save(child);
    }
}

 

💡자식에서 문제가 발생한 경우

  • 자식은 부모의 트랜잭션을 사용하다가 Unchecked Exception이 발생했고 부모는 자신의 트랜잭션에서 예외가 발생했으므로 롤백의 대상이 됩니다.
@Service
@RequiredArgsConstructor
public class ParentService {

    private final ParentRepository parentRepository;
    private final ChildService childService;

    @Transactional(rollbackFor = Exception.class)
    public void parentSave() {
        parentRepository.save(new Parent("부모"));
        childService.childSave();
    }
}

@Service
@RequiredArgsConstructor
public class ChildService {

    private final ChildRepository childRepository;

    @Transactional(propagation = Propagation.MANDATORY, rollbackFor = Exception.class)
    public void childSave() {
        Child child = new Child("자식");
        childRepository.save(child);
        throw new RuntimeException("자식 저장 중 예외 발생!");
    }
}

 

 

 

지금까지 다양한 트랜잭션 전파에 대해서 알아보았습니다. 

단순히 @Transactional 어노테이션이 궁금하여 찾아보다가 이런 중요한 내용이 있었고 트랜잭션과 예외처리에 대해서는 조금 더 깊게 알 필요성을 느끼게 되었습니다.

 

 

 

 

참고 자료)

https://oingdaddy.tistory.com/28

https://woodcock.tistory.com/40

https://sup2is.github.io/2021/03/04/java-exceptions-and-spring-transactional.html

https://techblog.woowahan.com/2606/

 

728x90
반응형