Spring

Transaction을 주의하여 Async 사용하기

leeheefull 2022. 2. 20. 14:13

이번에 프로젝트 구현하면서 비동기 처리에 대해 필요성을 느끼게 됐습니다. 사용할 때 발생했던 문제점에 대해서 이야기해보려고 합니다.

 

 

게시판과 게시글, 댓글의 연관 관계

게시판과 게시글, 댓글의 연관 관계

 

Board와 Post, Comment를 삭제하는 로직을 예제로 이야기해 보겠습니다.

 

 

실제 엔티티들의 데이터 관계

실제 엔티티들의 데이터 관계

 

Board를 삭제하면 Post와 Comment도 삭제되어야 하기 때문에 아래와 같은 로직을 작성하게 됐습니다.

 

 

Board 삭제 시, Post와 Comment 삭제 연쇄 작용 로직 (동기적 처리)

Board 삭제 시, Post와 Comment 삭제 연쇄 작용 로직 (동기적 처리)

 

위의 코드를 실행하면, Board를 삭제할 때 Post와 Comment 삭제 시간도 포함하여 대기해야 합니다. 게시판의 데이터가 많다고 가정하면, 응답 시간이 길어지게 되는 상황이 발생하게 됩니다.

 

당장 Board 하나를 삭제하는 응답이 중요한 것이고, Post와 Comment의 삭제 응답을 기다릴 필요가 없다고 생각했습니다.

 

개선 방법으로 Board는 동기적으로 처리하고 나머지 2가지 일을 비동기적으로 처리해준다면, Board의 삭제 응답만 기다리면 되기 때문에, 실제 보이는 처리시간이 빨라질 수 있다고 생각했습니다.

 

그렇다면, 비동기 처리 방법으로 바꾸기 위해서 동기와 비동기에 대해 간단히 이야기해 보겠습니다.

 

 

 

동기와 비동기 처리 비교

동기와 비동기 처리 비교

 

동기 (Synchronous)

동기 방식은 서버에 요청을 보냈을 때, 응답이 돌아오면 다음 동작을 수행하는 방법입니다.

 

비동기 (Asynchronous)

비동기 방식은 동기 방식과는 반대로 요청을 보내고 응답과는 상관없이 다음 동작을 수행할 수 있는 방법을 말합니다.

 

차이점

동기 방식은 코드가 직관적이고 간단합니다. 또한, 순서를 보장받을 수 있어서 명확한 데이터를 확인할 수 있다는 장점이 있습니다. 하지만, Board를 지우기 위해서 Comment와 Post가 지워질 때까지 대기해야 하는 문제점이 있습니다.

 

반면, 비동기 방식은 시간이 걸리는 동안 다른 작업을 할 수 있어서 효율적인 장점이 있습니다. 하지만, 동기 방식보다 복잡합니다. 그리고 순서가 명확하지 않아서 데이터의 값을 확실하게 판단할 수 없습니다.

 

그렇다면, 이제 비동기 처리 방법을 적용해보겠습니다.

 

 

 

@Async 적용하기

@Async을 이용하면, 간단하게 비동기 처리를 할 수 있습니다.

 

`@Async`를 활성화하기 위한 Configuration 구현

@Async를 활성화하기 위한 Configuration 구현

 

우선, @Async를 사용하기 위해서 AsyncConfigurerSupport를 상속받고 @EnableAsync로 활성화를 해주고 @Configuration을 등록해줘야 합니다. 이로써 간단한 비동기 처리를 사용할 수 있게 되었습니다. 적용해 보겠습니다.

 

 

BoardFacade의 Board, Post, Comment 삭제 메서드

BoardFacade의 Board, Post, Comment 삭제 메서드

 

BoardService의 Board 삭제 메서드

BoardService의 Board 삭제 메서드

 

PostService의 Board 관련된 Post 삭제 메서드

PostService의 Board 관련된 Post 삭제 메서드

 

CommentService의 각 Posts의 Comment 삭제 메서드

CommentService의 각 Posts의 Comment 삭제 메서드

 

위의 로직에서 비동기 처리하고 싶은 메서드를 골라서 @Async를 달아주면 됩니다. Board와 Post, Comment를 삭제하는 모든 로직에 @Async를 달았습니다. 그리고 실행했더니...

 

Board에 null 값이 넘어오면서 NPE가 발생했습니다. 우선 NPE가 발생한 것 자체도 의아했지만, DB Table을 확인해보니 Post와 Comment는 지워지지 않았고 Board만 지워지는 현상이 발생했습니다. 분명히 트랜잭션을 각각 걸어주었고 퍼사드 레이어의 메서드에도 걸어주었기 때문에 롤백이 되어야 하는 상황이라고 생각했습니다.

 

 

문제 발생 이유

문제 발생 이유

 

문제를 분석해본 결과, 트랜잭션과 Async를 동시에 모두 걸어주면서 트랜잭션이 두 가지로 나뉜다는 것을 알게 되었습니다. tx-a는 Board를 지우는 것을 완료하여 커밋이 됐고, tx-b는 Board를 가지고 Post와 Comment를 지워야 하는데, 서로 다른 트랜잭션이므로 null 값인 Board를 가지고 로직을 수행해야 했습니다. 그래서 tx-b는 롤백이 된 것입니다.

 

 

문제 해결

문제 해결

 

@Async를 달아줄 때는, 항상 트랜잭션을 잘 고려해야 한다는 것을 알게 됐습니다. 수정한 결과로 Board를 지우는 메서드는 동기로 처리하고 Post와 Comment를 지우는 메서드는 비동기로 처리해 주었습니다. 이로써 트랜잭션 하나로 로직을 수행할 수 있게 되었습니다.

 

 

기본적인 @Async 주의 사항

위와 같은 주의사항 외에도 기본적으로 Async 메서드는 같은 클래스 내에서 사용이 불가능합니다. 그렇기 때문에 접근 제한자가 private인 메서드에 사용이 불가능합니다.

 

 

 

동기와 비동기 효율 비교

Board와 Post, Comment를 지우는데 각각 5초가 걸린다고 가정하고 효율을 비교해보았습니다.

각 삭제 로직에 5초씩 sleep 걸기

각 삭제 로직에 5초씩 sleep 걸기

 

Board 지우기 효율 결과

  • 동기 방식 15.101 (sec)
  • 비동기 방식 5.025 (sec)

 

이처럼 동기 방식은 15초 동안 응답을 대기하지만, 비동기 방식은 Board를 지우는 5초만 응답을 대기합니다. 그래서 실질적으로 5초가 걸린다고 할 수 있습니다.

 

 

 

다음에는 비동기 처리를 더 효율적으로 하기 위해서 이벤트로 묶어서 처리하는 방법에 대해 작성하겠습니다.

 

이상입니다. 수정 사항이나 피드백이 있으시다면 편하게 댓글 부탁드리겠습니다. 글 읽어 주셔서 감사합니다