스프링 트랜잭션 옵션과 전파(propagation)

2023. 3. 15. 18:40카테고리 없음

반응형

기본적으로 스프링에서 사용되는 선언적 트랜잭션에는 옵션과 Default값이 적용되어 있는데..

가장 많이 사용되는 것은 readOnly(기본값 false)옵션일 듯 하다.

그리고 CheckException의 경우에는 발생하더라도 롤백처리가 되지 않는데.. 롤백하기를 원한다면 옵션으로 설정이 가능하다..!(기본적으로 RuntimeException이 발생하였을때만 자동 롤백처리됨.)

@Transactional(readOnly = true)
@Transactional(rollbackFor = Exception.class)

isolation와 timeout또한 옵션으로 설정 가능하지만..DB에 따라 지원 가능 할 수도 있고 아닐수도 있다고 한다..! 필요하다면 사용하고 적용이 되는지 제대로 확인이 필요하다..!

그리고 트랜잭션 전파에 대해서…!

트랜잭션 전파의 경우 아래와 같이 설정이 가능한데.. 해당 부분을 들어가면 default값으로 REQUIRED로 명시되어 있다.

Propagation propagation() default Propagation.*REQUIRED*;

@Transactional(propagation = Propagation.REQUIRES_NEW)
@Transactional(propagation = Propagation.REQUIRED)

개인적인 공부를 정리하고 있어서 일단 간단하게 정리해보자면…

Spring에서 제공하는 트랜잭션 전파 옵션

  1. REQUIRED : 기존 트랜잭션이 존재하면 해당 트랜잭션을 사용하고, 없으면 새로운 트랜잭션을 생성합니다.
  2. SUPPORTS : 기존 트랜잭션이 존재하면 해당 트랜잭션을 사용하고, 없으면 트랜잭션 없이 실행됩니다.
  3. MANDATORY : 기존 트랜잭션이 존재하면 해당 트랜잭션을 사용하고, 없으면 예외가 발생합니다.
  4. REQUIRES_NEW : 기존 트랜잭션을 일시 중단하고, 새로운 트랜잭션을 생성합니다.
  5. NOT_SUPPORTED : 트랜잭션 없이 실행됩니다. 기존 트랜잭션을 일시 중단합니다.
  6. NEVER : 트랜잭션 없이 실행됩니다. 기존 트랜잭션이 존재하면 예외가 발생합니다.
  7. NESTED : 기존 트랜잭션을 사용하면서, 해당 메소드에서 새로운 트랜잭션을 생성합니다. 내부 트랜잭션이 외부 트랜잭션의 savepoint를 만들고 롤백할 수 있습니다.

 

스프링 공식문서 주소(https://docs.spring.io/spring-framework/docs/current/reference/html/index.html)

 

Spring Framework Documentation

Overview History, Design Philosophy, Feedback, Getting Started. Core IoC Container, Events, Resources, i18n, Validation, Data Binding, Type Conversion, SpEL, AOP, AOT. Testing Mock Objects, TestContext Framework, Spring MVC Test, WebTestClient. Data Access

docs.spring.io

전파의 옵션의 경우 다양한 종류가 있지만 주로 사용되는 것은 아래 2가지 인듯 하다.

기본옵션의 경우(Propagation.REQUIRED) 상위 트랜잭션이 존재한다면 해당 트랜잭션에 같이 속해지게 된다. (하나의 트랜잭션을 사용)
New의 경우!(Propagation.REQUIRES_NEW) 상위에서 선언된 트랜잭션이 존재하더라도 물리적으로 분리되어 별도의 트랜잭션 커넥션을 받아와서 사용하게 된다.

2개의 차이점으로 보자면.. 오류가 발생하여 롤백하여야 하는 경우 어떻게 처리 되는가가 달라지게 된다.

예를들어 아래와 같은 로직이 있다고 한다면

  1. 회원가입
  2. 로그등록

비즈니스 로직 요청사항 회원가입과 로그는 어느것이 실패하더라도 동일하게 처리되어야한다.(정합성 보장필요)

회원가입 수행 도중 실패시—> 실패시 롤백

회원가입 성공, 로그등록 도중 실패시 —> 실패시 둘다 롤백

회원가입 성공, 로그 등록 성공 —> 커밋

Propagation.REQUIRED로 적용되는 경우 모두 하나의 트랜잭션으로 적용되기때문에…!

어느것이 실패하더라도 동일하게 보장된다.

그런데 만약 아래와 같이 요청사항이 변경된다면..?

회원가입과 로그는 별개로 봐야하고 로그작성이 실패하여도 가입은 정상적으로 처리되어야 한다.(정합성 보장 불필요함.. 로그는 그냥 확인용 이기때문에…)

이 경우에는 Propagation.REQUIRED에는 하나의 트랜잭션으로 묶여있기 때문에 문제가 된다.

  1. 아예 각각의 서비스를 따로 호출하여 로직을 분리하던가
  2. Propagation.REQUIRES_NEW 옵션을 주어서 아예 서로 다른 트랜잭션을 사용하게 해야한다.

회원가입 수행 도중 실패시—> 실패시 롤백

회원가입 성공, 로그등록 도중 실패시 —> 실패시 로그만 롤백

회원가입 성공, 로그 등록 성공 —> 둘다 커밋

하지만 항상 중요한건 해결방법에 따른 리스크가 존재한다는 것인듯 하다.

별개의 트랜잭션을 사용하기 때문에 1번 호출에 대해 트랜잭션을 2개를 사용하게 되고 이 경우 많은 요청이 올 경우 커넥션 풀에서 커넥션이 모자라서 제대로 응답하지 못하게 될 수 있다…!!!!

 

가장 중요한 주의사항 2가지!!!

1번 직접 호출 하는 경우 문제 발생

스프링에서 내부 메서드를 직접 호출하는경우 트랜잭션 Proxy가 적용되지 않는다…!
A라는 service가 아래와 같이 구성되어있다고 할 때.. 직접 호출하게되면 문제가 된다.

public class AService {


    public void test1() {
        test2();
    }

    @Transactional
    public void test2() {
    }
}



test1에는 트랜잭션이 없지만 test2에 트랜잭션이 되어있어 보기에는 트랜잭션이 적용 될 듯 보이지만 최초 진입시 test1을 호출하게되면 트랜잭션 선언이 없기 때문에 트랜잭션을 생성하지 않고 시작되고 내부에서 직접 호출하게 되므로 스프링 트랜잭션 AOP가 적용된 프록시를 사용하지 않는다. this.test2(); 와 같은형태

@RequiredArgsConstructor
public class AService {

	  private final BService bService;

    public void test1() {
        bService.test2();
    }

}

public class BService {

   @Transactional
   public void test2() {
   }

}

이 경우에는 원하는 대로 트랜잭션이 동작하지 않게 되기에 아예 서로다른 서비스로 분리하여 직접호출을 하지 않는 형태로 변경하면된다.

2번 에러가 발생하였을때 처리 후 문제 발생.

아래와 같이 로직이 수행된다고 가정할 때 원하는 결과는 2번의 결과에 관계없이 1번 로직에 대한 커밋 수행.

  1. 회원가입 — 성공
  2. 로그등록 — 실패시 RuntimeException을 반환함.

1번 정상 커밋, 2번 롤백수행을 기대한다.

이 경우 memberRepository, logRepository내 존재하는 save에 트랜잭션이 @Transactional 어노테이션이 명시되어 있고 오류 처리도 try_catch 문으로 에러 처리를 하여 비즈니스 로직의 경우에는 정상적으로 동작하게 된다…!

Member member = new Member(username);
Log logMessage = new Log(username);

memberRepository.save(member);

try {
    logRepository.save(logMessage);
} catch (RuntimeException e) {
    log.info("log저장에 실패했습니다. logMessage = {}", logMessage);
    log.info("정상흐름 반환");
}

하지만 기본값인 Propagation.REQUIRED 를 사용하기 때문에문제가 된다.

//기본 Default 전파 옵션
@Transactional(propagation = Propagation.REQUIRED)

2번 로직에서 RuntimeException이 발생하는 경우 해당 트랜잭션에서 하나라도 오류가 발생하는 경우 rollback-only 마크를 남기게 되고 최종적인 트랜잭션에서 이미 rollback 마크가 되어 있기 때문에 전체를 rollback하게 된다.

실패의 모습

 

@Transactional(propagation = Propagation.REQUIRES_NEW)

각각 서로 다른 트랜잭션을 사용하게 한다면 비즈니스 로직상 에러가 발생했을때 try catch에서 해당 로그를 남겨서 처리할 부분은 처리하고 원하는 방향으로 흘러가게된다…

여기서 2번에서의 주의사항이 또 존재한다.

아래와 같이 try_catch를 제거한다면…?

  1. 회원가입 — 성공
  2. 로그등록 — 실패시 RuntimeException을 반환함.

각각 서로다른 트랜잭션을 적용하였기 때문에 동일하게 1번 정상 커밋, 2번 롤백수행을 기대한다.

//logRepository내에 메서드에는 각각 별도의 트랜잭션 사용을 위해 new로 선언되어있다.
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(Log logMessage) {
    log.info("log 저장 ");
    em.persist(logMessage);

    if (logMessage.getMessage().contains("로그예외")) {
        log.info("log 저장시 예외 상황 발생");
        throw new RuntimeException("예외발생");
    }
}

//Member Service 메서드 
@Transactional
public void join(String username) {
	Member member = new Member(username);
	Log logMessage = new Log(username);
	
	memberRepository.save(member);
	
	//아래 save에서 RuntimeException이 발생한다. 각각 별도의 트랜잭션이라고 생각하여
	//try catch 문 제거
	logRepository.save(logMessage);
}

하지만 결과는 모두 롤백처리 되게 된다.

이유는 RuntimeException이 log.repository에서 발생하였으나 처리 로직이 없어서 Serivce까지 Exception이 그대로 올라가게 되고

memberRepository.save(member); —> 정상

logRepository.save(logMessage); —> exception발생

MemberService —> exception 발생

최종적으로 Exception이 발생했다고 판단되어 전체 Rollback처리를 하게된다..!!!

 

자세한 내용은 모두 김영한님 인프런강의 중 스프링 DB 2편을 보시면 될 듯 합니다...!

 

반응형