왜? 우리만 동시성 문제가 많이 발생했지???? #837
sangwonsheep
started this conversation in
General 일반 공유
Replies: 1 comment
-
혼자쓰는데 왜 동시성이 발생하지? 이유 : 네트워크가 느릴수록 요청이 한번에 동시에 옴 이러한 순서에 맞게 추후 글 다듬겠습니다. |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
발단
태그 순서나 폴더 리스트의 순서를 어떻게 보장할까?라는 의문이 들었음.
다른 서비스는 순서를 어떻게 보장하도록 구현했는지 궁금하다.
다른 서비스를 찾아본 결과, raindrop이라는 해외 북마크 서비스를 알게 되었음.
raindrop이라는 완성된 서비스의 네트워크 요청을 참고해보기로 하였음.
네트워크 요청/응답을 살펴본 결과, 리스트 형태로 순서를 관리하는 것을 확인하였음.
리스트 형태로 순서를 보장하면, 편리하겠다! 구현에도 큰 어려움이 없을 것 같아서 참고하여 개발을 시작하기로 하였음.
API 설계부터 DB 설계까지 모두 raindrop의 순서 보장 방식을 따르게 되었음.
(수정된 부분만 보내는게 아니라 가지고 있는 리스트 전체를 주고 받자)
전개
이 방식을 사용하다 보니 많은 문제가 발생하게 되었는데...... 무슨 문제가 있었을까?
가장 많이 발생한 방식은 바로 동시성 문제였다.
왜??? 대체 왜??? 동시성이 터질만한 상황이 아닌데?
우리의 설계는 이러했다.
폴더 엔티티 내에 자식 폴더id 리스트, 자식 픽id 리스트를 column으로 가지도록 하자.
또한, 리스트를 DB에 넣을때는 JPA Converter를 이용해서 String으로 변환해서 넣어두자. 리스트에 대해 JPA 연관관계도 지어놓지 않은 상태로....
그 외에도 픽 엔티티 내에 태그id 리스트도 동일하게 구현하였다.
위기
동시성 문제가 발생한 시점은 프론트 화면이 개발이 완료된 시점부터였다.
가장 먼저 발생했던 것은 픽이 가지고 있는 태그 리스트를 수정할 때였다.....
이런 방식으로 태그를 추가, 삭제를 할 수 있다.
태그를 추가하고 삭제하는 것이라고는 하지만 실제 API 호출은
픽 수정 API
가 호출된다.이렇게 태그를 추가 혹은 삭제를 하게 되면 아래와 같은 플로우로 로직이 수행된다.
태그를 추가, 삭제하는 픽 수정 API를 빠르게 요청을 보낼 때 데드락 또는 동시성 문제가 발생하였다.
또는 빠르게 요청을 보내지 않아도 네트워크가 느릴 때 요청을 쌓아두었다가 동시에 보내는 경우도 있었다.
DB의 general_log를 확인해보고, API 요청도 살펴보며 문제가 발생하는 원인을 찾아보았다.
로그와 요청을 확인해본 결과, 여러 요청이 동시에 발생하여 같은 Pick 엔티티와 PickTag 엔티티를 수정할 때 데드락이 발생했었다.
항상 데드락이 발생하는 것은 아니었다.
데드락이 발생하지 않더라도 동시성 문제는 발생했다.
요청을 보낸 순서대로 처리가 되어야 하는데 순서대로 처리되지 않는 문제가 발생했는데 사실 이 문제가 동시성 문제였다.
예를 들어, [A, B] -> [A, B, C] -> [A, B, C, D] 순으로 처리가 되어야 하는데 [A, B] -> [A, B, C, D] -> [A, B, C] 순으로 처리가 된 것이다.
[A, B, C, D]가 먼저 저장되어 이후에 저장하려고 하는 [A, B, C]는 비즈니스 로직상 넣을 수 없기 때문에 500에러를 발생시킨다.
500 에러를 사용자에게 보여주게 되기도 하고, 500 에러가 발생하면서 서비스가 잠깐 버벅일 수 있는 문제 때문에 반드시 개선이 필요했다.
또한, 폴더 엔티티 내에 있는 하위 폴더 리스트, 하위 픽 리스트 수정 시에도 동시성 문제가 발생했다.
이로 인해 서비스가 제대로 동작하지 않는 문제도 발생했었다.
절정
그러면 이걸 어떻게 해결하지?
요청을 보낸 순서를 보장해주어야 한다.
이 순서를 보장하려면 어떻게 해야할까? 그건 바로 락을 사용해서 순서를 보장시켜주면 되는 것이다.
synchronized
자바에서 제공하는 일종의 락이라고 생각하면 된다.
이 키워드를 사용하면, 순차적으로 실행할 수 있게 해줄 수 있다.
이것의 가장 큰 문제는 메서드에 synchronizd를 걸면 모든 유저가 해당 메서드를 사용하려고 할 때 이미 사용중이라면 모두 대기해야 한다.
총 100명의 유저가 기다리고 있다고 가정하자.
1명당 처리되는 시간이 1초라고 하면, 맨 마지막 유저는 99초를 기다린 후에서야 로직을 수행할 수 있는 것이다.
성능에서 크나큰 비효율을 보이기 때문에 사용해서는 안된다.
낙관적 락
낙관적 락을 사용하면 해결이 될까?
낙관적 락을 Pick, PickTag 테이블에 사용해보았다.
Pick의 경우 하위 태그 리스트를 update 하는 부분은 제대로 동작하여 순차적으로 처리할 수 있게 되었다.
그러나, PickTag 테이블의 경우 insert, delete만 사용하기 때문에 update를 하지 않아 결국 동시성 문제는 동일하게 발생한다.
또한, retry 횟수에도 신경을 많이 써야하기 때문에 낙관적 락은 우리의 상황에 적합하지 않다.
비관적 락
비관적 락의 경우 테이블에 락을 거는 것이 아닌 row에 락을 거는 방식이다.
메서드가 실행될 때 한 트랜잭션이 점유 중이라면, 다른 트랜잭션은 점유를 하지 못하게 해야 한다.
하지만, Service에서 트랜잭션이 시작하여 DataHandler까지 트랜잭션이 이어지도록 구현이 되어 있다.
Service 메서드가 시작하는 지점에서 select for update문을 걸지 않았고, DataHanlder 내부에 비관적 락을 걸도록 구현을 했었다.
그 이유는 Service 메서드가 시작하는 지점에 모두 걸게 되면 synchronized와 비슷하여 같은 문제가 발생할 것이라 생각하였다.
이러한 이유로 비관적 락은 사용하지 않기로 하였다.
그렇다면, 마지막으로 어떤 선택을 했을까?
메서드에 락을 거는 방식보다는 유저별로 락을 거는 것은 어떨까라는 생각을 하게 되었다.
유저별로 락을 걸기 위한 방식이 무엇이 있을까? 그것은 바로 MySQL의 네임드 락을 이용하는 것이었다.
네임드 락
네임드 락은 테이블 락이나 row 락이 아닌 락에 이름을 부여하면 여러 메서드 혹은 분산 환경에서 사용할 수 있다.
특히, 우리의 서비스는 개인 서비스이기 때문에 여러 사용자가 동시에 변경을 하는 환경이 아니다.
지금까지 발생했던 문제들도 개인이 요청을 동시에 여러 번 보내서 생긴 문제였기 때문에 네임드 락을 이용하여 보낸 요청의 순서를 보장해주면 된다.
이렇게 해서 네임드 락으로 개인이 보낸 요청의 순서를 보장해줄 수 있게 되었다.
결말
네임드 락으로 어찌저찌 문제는 해결을 하였지만, 결국 락이 필요 없는 상황인데 락을 쓰게 되었다.
이러한 작은 규모의 개인 서비스에서 락은 오버 엔지니어링인 것이다.
그러면, 왜 이런 상황이 발생하였는지를 살펴보면 우리의 API 설계부터가 잘못된 것이다.
PUT과 같이 모든 리스트를 보내는 게 아니라 변경이 필요한 부분 하나만 보내는 방식으로 설계를 변경하면 락을 사용하지 않고도 해결할 수 있다.
Beta Was this translation helpful? Give feedback.
All reactions