JVM/Spring

[SPRING] synchronized와 @Transactional 을 동시에 사용 시 문제점

Hyo Kim 2021. 8. 16. 17:27
728x90
반응형

😗서론

@Transactional 어노테이션과 synchronized을 동시에 사용하고 싶은 경우가 있을 수 있다.

트랜잭션 격리수준과 별개로 해당 메소드를 동기화를 적용시키고 싶을 때.

하지만, 한 메소드 위에 해당 Transactional, Synchronized를 동시에 적용 시

원하는대로 작동하지 않을 수 있다.

 

그 이유를 알아보자.


😎본론

아래 예시는 스택오버플로우를 번역하여 풀어서 정리했습니다.

 

🤨뭐가 문제지?

일단 Synchronized를 사용하는 이유는 해당 메소드를 한 쓰레드에서만 돌리기 위해서다.

하지만, 트랜잭션이 같이 정의가 되어있다면 첫 번째 쓰레드가 끝나기 전 두 번째 쓰레드가 발동할 수도 있다.

그 이유를 살펴보자.

@Transactional
public synchronized void onRequest(Request request) {

    if (request.shouldAddBook()) {
        if (database.getByName(request.getBook().getName()) == null) {
            database.add(request.getBook());
        } else {
            throw new Exception("Cannot add book - book already exist");
        }
    } else if (request.shouldRemoveBook()) {
        if (database.getByName(request.getBook().getName()) != null) {
            removeBook();
        } else {
            throw new Exception("Cannot remove book - book doesn't exist");
        }
    }
}

(해당 메소드는 예를 보여주기 위한 코드이며, 완벽하지 않은 코드입니다.)

 

위 메소드는 동작 방식은 이렇다.

1. 책을 추가하는가?
1-1. 해당 책이름이 db에 없는가?
1-2. 없으면 추가
1-3. 있다면 Exception

2. 책을 삭제하는가?
2-1. 해당 책이름이 db에 있는가?
2-2. 있다면 삭제
2-3. 없다면 Exception

😅예제

동일한 제목의 책을 제거하고 추가하는 순서로 해당 메소드를 호출한다고 가정하고 진행해 보겠다.

1. Transactional, Synchronized 키워드가 없다면 ?

Thread1: |---remove book--->

Thread2: |---add book--->

이는 삭제가 되기 전에 거의 동시에 추가 메서드가 발동되므로

동일한 이름의 book이 db에 존재하거나 Exception이 발생할 수 있다.

 

2. Synchronized를 추가하면 어떻게 동작하는가 ?

Thread1: |---remove book---> Thread2: |---add book--->

위와 같이 삭제가 끝난 후 다음 쓰레드를 통해서 책을 추가하게 된다.

우리가 원하는 방식이다.

하지만, 우리에겐 트랜잭션이 필요하다.

 

3. Transactional을 적용한다면 ?

@Transactional 어노테이션은 AOP다. 고로 새로운 프록시를 생성하게 된다.

 

begin Transaction ---> method ---> commit Transaction

위와 같이 메소드를 감싸 메소드 실행 전, 후로 새로운 코드를 호출하게 된다.

 

그렇다면 제거, 추가를 진행할 때 어떻게 어떻게 동작을 하게 될까?

 

T1: |--Spring begins Transaction---> ---remove book---> ---Spring commits Transaction--->

T2: |--Spring begins Transaction---> ---add book---> ---Spring commits Transaction--->

트랜잭션을 적용하게 되면 이러한 방식으로 거의 동시에 진행되게 된다.

앞으로 요약하여 아래와 같이 표시하겠습니다.
T1: |--B--|--R--|--C-->
T1: |--B--|--A--|--C-->

 

트랜잭션은 동일한 entity만을 동시에 수정하지 못하게 한다.

새로 add되는 book은 다른 entity를 추가하는 것이기 때문에 정상적으로 작동이 될 것이다.

하지만, 동일한 이름의 book이 동시에 db에 저장되어 있는 시간이 존재하거나 Exception이 발생할 수 있다.

 

4. Synchronized과 Transactional을 동시에 적용하게 되면 ?

여기서 문제가 발생한다.

Spring에서 추가하는 Transactional 코드는 동기화로 감싸진 코드가 아닌 별도의 코드라는 점이다.

그로 인해 동기화된 메소드가 종료된 후 commit 이 되기 전 시점에서 두 번째 쓰레드가 진행될 수 있다.

 

T1: |--B--|--R--|--C-->

T2: |--B---------|--A--|--C-->

 

위와 같이 remove 메소드가 끝난 시점에서 add 메소드가 동작하게 되면

commits이 이루어지기 전에 add 메소드가 진행되기 때문에 Exception이 발생할 수 있다.

 

🤔어떻게 해결하지?

현재 사용하고 있는 방식은 @Transactional을 호출하기 전에 먼저 Synchronized를 호출하는 방식이다.

public class SynchronizedService() {

	@Autowired
	private TransactionService transactionService;
    
	public synchronized void onRequest(Request request) {
		transactionService.onRequest(request);
	}
}

public class TransactionService() {

  @Transactional
  public void onRequest(Request request) {
      if (request.shouldAddBook()) {
          if (database.getByName(request.getBook().getName()) == null) {
              database.add(request.getBook());
          } else {
              throw new Exception("Cannot add book - book already exist");
          }
      } else if (request.shouldRemoveBook()) {
          if (database.getByName(request.getBook().getName()) != null) {
              removeBook();
          } else {
              throw new Exception("Cannot remove book - book doesn't exist");
          }
      }
  }
}

위와 같이 synchronized 안에서 @Transactional 메소드를 호출하게 되면

우리가 원하는 방향으로 진행된다.

T1: |--B--|--R--|--C-->T2: |--B--|--A--|--C-->


검색해본 결과 격리수준으로 해결하라는 글을 몇몇 접하긴 하였다.

하지만, 트랜잭션 격리수준으로 조절하는 것은 데이터의 무결성을 위해서이다.

이는 데이터의 격리수준을 조절하는 것이지 synchronized와는 전혀 다른 접근이라고 생각한다.

어느정도 격리수준으로 데이터를 잠궈 비슷한 효과를 낼 수 있겠지만,

이는 내가 원하는 방향과는 다른 방향이라고 생각하고 완벽하게 같은 동작을 이뤄내지 않는다고 생각한다.

 

 

더 좋은 방식이나 잘못된 점이 있다면 알려주시면 감사하겠습니다. (__)

 

참조

https://stackoverflow.com/questions/41767860/spring-transactional-with-synchronized-keyword-doesnt-work
https://stackoverflow.com/questions/6479283/logical-comparison-of-java-synchronized-keyword-and-spring-transactional-annota
https://github.com/Gluewine/Gluewine/issues/25
728x90
반응형