멘토 신청 기능 동시성 이슈 문제 해결기 #2 - 애플리케이션 Level 에서 해결

kms·2023년 12월 24일
0

애플리케이션 Level 에서 동시성 문제 해결하기

자바에서는 synchronized 라고 하는 키워드를 사용해서 특정 메서드 하나의 스레드만 접근할 수 있도록 할 수 있습니다. 기본적으로 락(Lock) 메커니즘을 사용하여 동시성을 제어하게 됩니다.

Java에서 모든 객체는 내부적으로 락을 가지고 있습니다. synchronized 키워드가 사용되면, 현재 스레드는 해당 객체의 락을 획득해야만 그 객체의 synchronized 블록이나 메소드에 접근할 수 있습니다. 락을 획득한 스레드만이 그 구역의 코드를 실행할 수 있으며, 다른 스레드들은 락이 해제될 때까지 대기해야 합니다.

여기서는 특정 스레드가 request 메서드에 접근했을 때 락을 흭득하고 해당 메서드를 통과하고 나서야 해제하는 형태입니다.

과연 이제 테스트를 돌려보면 통과할까요?

테스트에 통과하지 못했습니다. 이는 @Transational 어노테이션의 동작 방식 때문인데요. 스프링에서는 @Transational 어노테이션이 붙여져 있는 경우에는 실제 클래스를 래핑(wrapping)한 클래스를 새롭게 만들어서 실행합니다.

@Transational 어노테이션이 붙은 메서드가 실행될 때, UserService 를 필드로 가지는 UserService Proxy 객체를 생성합니다. 그리고 실제 로직을 실행하기 전과 후에 트랜잭션을 열고 닫는 코드를 실행하게 됩니다. 데이터베이스 트랜잭션을 열고 닫는 건 Real UserService에서 하는 것이 아닌 새롭게 생성된 proxy 객체에서 이루지는 것 입니다.

바로 여기서 문제가 발생하는데요.

class UserServiceProxy{
   UserService userService; // 진짜 UserService
   
	실행(){
		open tx
		// <-- 다른 스레드가 들어올 수 있다! 
		userService.exeute(); <- 이 메서드에 대해서만, synconrizied 는 여기에 걸려있다.
		// <-- 다른 스레드가 들어올 수 있다!
		close tx
	}
}

실제로 데이터 베이스에 쿼리가 나가는 시점은 실제 실행하는 메서드를 빠져나가서 트랜잭션을 종료(close tx) 했을 경우 입니다.

그림을 그려서 설명하자만 아래와 같습니다.

즉, 이는 애플리케이션 레벨에서 격리를 하기위해 synchronized 를 사용했지만, 여러개의 트랜잭션이 실행되어 결국 같은 문제가 야기된것입니다. 이를 위해서는 프록시 방식으로 동작하도록 하는 @Transationl 어노테이션을 제거하고 syncronized 선언된 메서드 안에서 commit 을 하도록 코드를 수정해보겠습니다.


변경한 코드입니다. mentorInfoRepositorysaveAndFlush() 를 호출하도록 해서 강제로 flush 를 하도록 했습니다.

실제 saveAndFlush() 의 구현부는 아래와 같이 되어있습니다.
1. @Transational : save() 와 flush() 전/후로 트랜잭션을 열고 닫습니다.
2. flush() : 코드에는 없지만, 결국 내부에는 em.flush(); 를 호출하고 있습니다.
연속성 컨텍스트에 있는 정보를 flush 하고 디비에 쿼리를 날리게 됩니다.

드디어 테스트를 통과했습니다.

문제점

Java 의 Synchronized 는 하나의 프로세스 안에서 보장된다. (즉, 서버가 여러 대일 경우 문제가 된다.)

Java 의 Syncronized 는 하나의 프로세스 안에서 보장됩니다. 그렇기 때문에 서버가 1대일 경우에는 데이터의 접근을 한대의 서버만 하기 때문에 여러개의 스레드가 동시에 같은 자원에 접근하는 일은 없습니다. ( 즉, RaceCondition 이 발생하지 않습니다.)

하지만 서버가 여러 대인 경우 어떻게 될까요. 각기 서버 프로세스에서 Syncronized를 통해 하나의 스레드만 접근할 수 있도록 만들어도 결국 최종적으로 데이터베이스에 접근할 때는 여러개의 스레드가 동시에 DB에 접근할 수 있는 상황이 생길 수 있습니다.

가령 두 개의 서버에서 멘토 요청을 한다고 했을 때 아래와 같은 상황이 생길 수 있습니다.
즉, Server 1 이 데이터를 업데이트 하기위해 멘토 정보를 가져왔을 때는 mentee 는 0 이였습니다. 그리고 가져온 멘티의 값에서 1을 추가하여 업데이트 쿼리를 날립니다. 하지만 그 전에 다른 Server2 에서 업데이트 반영되기 전에 멘토 요청이 들어와 멘토정보를 조회하고 말았습니다.

Server1에 의해 이미 멘티의 수가 1이 되었음에도 불구하고 이전의 mentee 정보(mentee:0) 를 토대로 업데이트 쿼리를 날리면서 문제가 발생했습니다. 의도한 건 mentee 가 최종적으로 2가 되는 것입니다.

따라서 만약 서버를 여러대로 운영하는 경우에는 치명적일 수 있습니다. 결국 애플리케이션에서 해결한다고 해도 DB에 라고 하는 하나의 자원에 여러개의 서버 (여기서는 어떻게 보면 여러개의 프로세스 라고 볼 수 있습니다.)가 접근 하여 생기는 문제는 해결하지 못합니다.

똑같은 원리로 결국 데이터베이스에 Synchronized 와 같은 락(Lock)을 적용하여 여러개의 프로세스(서버)가 접근하더라도 하나의 프로세스만 와서 처리하도록 하면 됩니다.

3편에서는 데이터베이스의 락 기능을 활용하여 동시성 이슈를 해결하는 포스팅을 다룹니다.

0개의 댓글