@Async 적용 시 트랜잭션 간의 예외 경계 실험

최인준·2024년 1월 9일
4
post-thumbnail

본 글은 @Async 자체에 대한 설명은 없습니다.

서론

스프링의 도움을 받아 개발을 한다면 @Async라는 강력한 기능을 제공해주는 어노테이션을 만나게 될 것이다.

@Async는 어노테이션만 붙이는 쉬운 작업을 통해 작업을 비동기로 수행할 수 있게 해준다.(매우 간편..)

실제로 비동기로 실행해야 되는 로직들이 많다면 이 어노테이션을 많이 사용하게 된다.

하지만 모든 어노테이션이 그렇듯 강력한 기능을 쉽게 사용할 수 있는 것들은 제대로 알고 사용하지 않으면 빨간 줄을 많이 만날 수 있고 그 문제를 해결하는데에도 많은 시간을 투자해야 할 것이다,,,

나는 문제를 만나고 공부하기 보단 미리 자세히 알아봐 예방을 하기로 했다. 개념상으로의 공부는 했지만 실제 코드를 통해 느끼는 것이 확실히 기억될 것이라 생각해 관련 실험을 하게 되었다.

실험할 내용은 간략히 다음과 같다.

  • 비동기 트랜잭션과 비동기를 호출한 트랜잭션 사이 서로간의 예외 전파
  • @Async 어노테이션 사용 안할 시의 서로간의 예외 전파

실험 환경

member 트랜잭션(MemberService의 memberMethod())

@Transactional
public void memberMethod() {
    Member member = new Member("I'm member", "member@gamil.com");
    asyncService.asyncMethod();
    memberRepository.save(member);
}

async 트랜잭션(AsyncService의 asyncMethod())

@Transactional
@Async
public void asyncMethod(){
    Member member = new Member("I'm async", "async@gamil.com");
    memberRepository.save(member);
}

실행할 테스트

@Test
@Rollback(value = false)
void test(){
    memberService.memberMethod();
}

각 트랜잭션에서 수행되는 일은 간단히 서로 다른 이름으로 Member를 생성해 저장하는 것이다.

테스트는 부모 트랜잭션을 호출하는 테스트 코드이다.

참고로 @Transactional의 기본 전파 속성은 REQUIRED 인데 이는 부모 트랜잭션 내에서 실행되고 부모 트랜잭션이 없다면 새로 트랜잭션을 만든다.

두 메소드는 서로 다른 스레드에서 실행되기 때문에 async가 member를 부모 트랜잭션이라고 생각하지도 않고 member 트랜잭션의 정보를 알 수도 없다.

그래서 부모 트랜잭션이 없기 때문에 async에서는 새로운 트랜잭션이 열린다.


가설

한쪽 트랜잭션에서 발생한 예외는 다른쪽의 트랜잭션에 영향을 미치지 않는다.

즉, 어떤 async의 예외발생은 member에게 영향이 없고 member의 예외발생도 async에 영향이 없을 것이라는 가설이다.

서로 다른 두개의 트랜잭션이기 때문에 이런 가설을 세웠다!


첫번째 예외 발생

먼저 member측에서 예외를 발생시켜 보았다. 이론대로라면 async에 대한 작업 결과만 저장될 것이다.

결과는 예상대로 async에 대한 작업만 수행되고 member측의 작업은 롤백되어 저장 되지 않았다.

반대로 async측에서 예외를 발생 시켜보았다. 이번에는 async에 대한 작업만 롤백이 일어날 것이다.

반대도 마찬가지로 예상대로 결과가 나온것을 볼 수 있다.

참고 : @Async가 없다면?

@Async 어노테이션이 없고 호출되어지는 쪽의 트랜잭션에 별다른 옵션이 없다면 어디쪽에서 예외가 발생하든 예외가 전파되어 양쪽 다 롤백이 발생할 것이다! 결과도 다음과 같다.


그럼 비동기로 실행할 생각은 없지만 한쪽의 예외가 다른 한쪽에게 영향을 미치지 않게 하려면 어떻게 해야할까?

@Transactional의 전파 옵션 중 REQUIRES_NEW를 적용하면 새로운 트랜잭션을 만들어 실행하므로 롤백이 별개로 실행될 것이다. 코드를 다음과 같이 적용해봤다.

member 트랜잭션

@Transactional
public void memberMethod() {
    Member member = new Member("I'm member", "member@gamil.com");
    asyncService.asyncMethod();
    memberRepository.save(member);
    throw new RuntimeException();
}

async 트랜잭션 (REQUIRES_NEW 옵션 적용)

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void asyncMethod(){
    Member member = new Member("I'm async", "async@gamil.com");
    memberRepository.save(member);
}

호출하는 쪽에서 예외를 발생시키는 상황이다. 호출 되어지는 쪽에선 새 트랜잭션을 시작하는 옵션을 걸었으니

asyncMethod()의 작업은 롤백되지 않을 것이다!

결과도 예상대로 되었다😃


결론

위 실험 하나만으로도 @Async를 사용할 때 제일 중요한 점을 알 수 있다.

각각의 트랜잭션에서 발생한 예외는 서로에게 영향을 끼치지 않아 롤백은 예외가 발생한 트랜잭션에서만 수행된다

@Async 어노테이션을 사용할 때는 비동기 수행을 간편하게 해주어서 잘 알지 못하고 막 사용하는 경우가 많이 있을 것이라고 생각한다.

위에서 봤던 예외 전파에 대해 잘 알지 못하면 단순히 두 트랜잭션의 관계를 부모-자식이라고 생각할 수 있다.

이렇게 되면 한쪽에서만 롤백이 발생하는 예상과는 다른 결과를 얻고 띠용할 수도 있기 때문에 잘 알고 사용하자!

  • @Async 어노테이션 사용하는 트랜잭션은 호출하는 쪽의 트랜잭션과 별개이기 때문에 롤백도 별개로 실행된다.
  • 반대로 비동기로 실행되지 않는다면 예외 전파가 되기 때문에 새 트랜잭션을 만들어 별개의 트랜잭션에서 실행될 수 있는 옵션을 적용해야 한다.

(생각했던 것보다 실험이 짧게 끝났지만 이 주제 포스팅에선 더 할 것이 없다,, 또 다른 포스팅에서 범위를 넓혀서 테스트 해볼 예정이다~)

1개의 댓글

comment-user-thumbnail
2024년 1월 10일

AOP 배경 사진이 2개가 보여요 !

답글 달기