Floney

[플로니] 데이터 동시성 제어하기

딤섬뮨 2023. 10. 8. 11:01
728x90
런칭 때가 되니..끝이라고 생각했지만, 처리해야할 게 하나둘 생긴다
오늘은 이러한 이슈를 생각해봐야한다.
만약, 동시에 가계부 내역을 고쳐서 동시성 문제가 생기면 어떻게 하나요????
우리의 가계부는 공유 가계부라 해당 동시성 문제를 매우매우 신중히 봐야한다.

 

 팀원과 미팅을 통해..일단 동시성 제어가 필요한 코드가 어디인가를 생각해보았다.

 

처음에 접근은 이랬다.

가계부 내역을 수정하는 과정중에 다른 사람이 가계부 내역이 삭제된다면? 이걸 동시성 제어를 해서, 가계부 내역을 수정할 때 다른 '쓰기'행동을 막아야하는가?

 

근데 이제 이건 아닌 것 같단 생각을했다. 왜냐하면 각 사용자의 입장에서 생각해보면 '공유'가계부 측면에서 생각하면,

뭔가 어? 왜 내가 수정했는데 수정이 안되고, 삭제 되었어!는 개발자의 실수가 맞겠지만

어? 내가 수정했는데 다른 멤버가 삭제했어! 는 개발의 실수보다는 공유 가계부의 특성인것같다.

 즉, 결국 우리가 지원하는 비즈니스와 일치하기 때문이다.

 

그래서 개발자 팀원과 어느 부분에서 동시성 제어가 들어가야할지 부터 검토했다.

기본적으로 제공하는 비즈니스 로직 말고 '예외'를 생각해보아야했다.

 

다음과 같은 리스트가 나왔다.

 

우선은 코드를 살펴본 결과 다음 리스트의 쿼리에 락을 걸어놓고 차차 운영 중에 발생하는 이슈에 추가적으로 대응하기로 했다.

 


스프링으로 개발하는 많은 사람들이 남겨준 동시성 해결 방안 게시글을 참고해 간단하게 각 장단점을 파악 후 , 우리 서비스에 맞는 가장 이상적인 방안을 찾아보자.

 

1. synchronized 이용

- synchronized를 이용하면 , 하나의 스레드만 접근이 가능하다.

- 멀티 스레드 환경에서 스레드간 데이터를 동기화 시켜주기 위해 자바에서 제공하는 키워드

- 현재 데이터를 사용하고 있는 해당 스레드를 제외하고, 나머지 스레드들은 데이터 접근을 막아 순차적으로 데이터에 접근할 수 있다.

synchronized는 클래스 인스턴스에 lock을 건다.

 

syncrhonized 문제점

- 자바의 synchronized는 하나의 프로세스 안에서만 보장이 된다.

즉, 서버가 1대일 때는 문제가 없다.

@Transactional과 사용할 때 주의사항 존재한다.

 

 syncrhonized 검토 사항

1.  현재 문제가 일어나는 부분은 DB여서 DB 락을 걸면 해결이 되기에, 애플리케이션단 까지 접근을 제어할 필요가 있을까?싶었다.

2. 또한  synchronized는 무언가 전역 변수와 같은 애플리케이션의 공유자원 제어이기에..조금은 어긋난 방식 아닐까 싶었다.

 

 

2. DB 이용하기

 

1. Pessimistic Lock

비관적 락은 Repeatable Read 또는 Serializable 정도의 격리성 수준을 제공한다.

트랜잭션이 시작될 때 Shared Lock이나 Exclusive Lock을 걸고 시작하는 방법이다.

 

즉 배타적 락을 걸게 되면 다른 Tx에서는 lock이 해제 되기전에 가져갈 수 없음. 하지만 데드락 발생 가능성이 있다.

 

2. Optimistic Lock

 

[database] 낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock) (tistory.com)

낙관적 락 : version과 같은 별도의 컬럼을 추가하여,  수정했다고 명시하여 다른 트랜잭션이 동일한 조건으로 값을 수정할 수 없게한다.

 

1, A가 table id 1번을 읽음 => version 1

2, B가 table id 1번을 읽음 => version 1

3, A 가 id 1번을 수정을 하면서 version 1 -> version2 로 업데이트

4. B는 version 1에 해당하는 id 1번을 발견 못하여 충돌

충돌이 발생했을 경우 DB가 아닌 애플리케이션 단에서 처리해야한다.

 

비관적 락 vs 낙관적 락

낙관적 락은 트랜잭션을 필요하지 않기에 성능은 좋다.

비관적 락은 데이터 자체에 락을 걸기 떄문에 동시성이 떨어지고, 데드락이 일어날 수도 있다.

 

검토 사항

충돌이 많이 발생하는 경우에는, 비관적 락은 트랜잭션을 롤백하면 끝이난다. 하지만 낙관적락은 까다로운 수동 롤백 처리는 둘째치고 성능면에서도 update를 한번 더 해줘야한다.

 

4, Redis

Redis의 분산락을 사용하여 동시성 문제를 해결하는 방법이 있다.

여러 서버에서 동기화된 데이터를 처리하기 위해 Database,redis와 같은공통된 저장소를 이용한 방법

 

lettuce 

분산락 구현을 위해 스핀락으로 구현해야함. 스핀락같은 경우 SETNX 명령어로 계속 요청을 보내야하므로 Redis와의 부하를 줘서 비효율적임. 락의 타입 아웃도 구현되어있지 않아 무한 루프에 빠질 수도있다.

 

SETNX : Set if not exist 특정 key 값이 존재하지 않을 경우 set하라는 명령어로 특정 키에 대해 value가 없을 경우에만 값을 셋팅하라는 명령어

 

redission

위 같은 단점을 보완한게 redission의 pub/sub 방식이다.

 

Redis는 메세지 브로커 역할이 가능하다 예를 들어 10개의 쓰레드가 락 휙득을 위해 경합할 때 스레드 1번이 락을 얻고 로직을 시행할 때 나머지 스레드 2~10번은 구독을 하고 있다. 그리고 로직이 처리된 후 락이 해제되면 해제된 메세지를 sub에 대기중인 스레드에 publish하고 락을 얻게한다.

 

검토 사항

Redis를 사용하면 인프라 구축에 대한 비용과 유지보수 비용이 발생한다. 

 


 

동시성 제어를 위한 다양한 방법이 있겠지만, 우리의 런칭 시기를 고려하고 애플리케이션 규모를 생각해보았을 때, 비관적락을 사용하는 DB 쪽의 제어가 맞다고 생각되었다.

 

일단 비관적 락은 JPA에서 제공하고 있다.

 

@Lock(LockModeType.PESSIMISTIC_WRITE)
List<BookUser> findAllByBookAndStatus(Book book, Status status);

@Lock의 종류

 

* PESSIMISTIC_WRITE 데이터베이스에 쓰기 락을 건다

* PESSIMISTIC_READ 반복 읽기만 하고 수정하지는 않는 용도로 락을 걸 때 사용, 다른 트랜잭션에서 읽기는 가능

* PESSIMISTIC_FORCE_INCREMENT version 정보를 사용하는 비관적 락

 

728x90