일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- AOP
- Transactio
- 테스트코드
- 동시성문제
- Gateway
- 오어스
- Spring cloud gateway
- 커넥션 풀
- circuitbreaker
- Kotlin
- oauth
- 우아한 테크 코스
- 톰캣
- spirng
- HikariCP
- tomcat
- 트랜잭션
- Thread
- DispatcherServlet
- Spring Batch
- MDC
- 우아한테크코스
- 우테코
- resilience4j
- redis
- 우테코 5기
- JWT
- 최종 합격
- 살아남았다.
- Elk
- Today
- Total
코딩은 내일부터
LinkedList로 글과 카테고리의 순서 구현했을 때 동시성 제어 본문
배경 상황
동글 프로젝트는 드래그 앤 드롭으로 글과 카테고리의 순서를 바꿀 수 있다.
그래서 글 과 카테고리의 순서구현을 LinkedList로 구현해서 각각의 테이블에는 next를 바라보고 있다.
(각각의 글의 순서를 1,2,3 이런 식으로 저장하고 있으면 하나의 글의 순서가 변경될 때 나머지 글의 순서를 모두 변경해야 하는 문제가 있어 LinkedList로 구현하였다.)
이러한 상황에서 ngrinder로 부하테스트를 하던 중
위와 같이 순서가 마지막(next가 null)인 글이 여러 개 생성되는 것이다..
그래서 글의 가져올 때 마지막글이 여러 개가 select 돼서 오류가 발생해서 사용자가 이러한 문제를 겪었을 때
DB에 직접 지워주지 않는 이상 그 사용자는 글을 못 가져오는 버그가 발생한다.
문제의 코드
그래서 로직을 확인해 본 결과
마지막에 글을 저장하고 원래 마지막글의 next_writing_id를 새로 저장된 마지막 글의 아이디로 바꿔주고 있는 로직이다.
그래서 동시에 여러 요청이 왔을 때 마지막임을 알리는 글을 여러 개 저장한 후
원래 마지막글은 마지막의 저장된 ID로 update가 일어나고 있었다.
해결방법
이제 문제를 알았으니 해결할 수 있는 방법을 알아보겠다.
- 복합 유니크 인덱스 + Id 생성 전략 변경(Table)
- 복합 유니크 인덱스 + 끊어짐이라는 상태를 추가
- AOP를 활용하여 사용자 별로 동시성 제어(이 방법을 프로젝트에 적용)
해결방법 1. 복합 유니크 인덱스 + Id 생성 전략 변경(Table)
먼저, 복합 유니크 인덱스를 설정하여 카테고리 별 마지막 글이 단 하나만 존재하게 하려고 했었다.
이를 위해 category_id와 next_writing_id를 결합하여 복합 유니크 인덱스를 만들었는데,
로직 실행 중 문제가 발생했습니다.
이유를 찾아본 결과, NULL은 유니크 제약조건에 영향을 받지 않는 것은 알고 있었지만,
복합 유니크 인덱스에서도 동일하게 적용된다는 것을 처음 깨달았다....
그래서 자기 참조를 하고 있는 형태에서 ID(Long)을 저장하도록 로직을 바꿨다...(울먹..)
수정을 한 후 복합 유니크 인덱스로 인해 원래의 마지막 글의 next_writing_id를 변경하고, 그다음 요청받은 글을 저장해야 했다.
그래서 생각한 게 글과 카테고리의 ID를 생성하는 전용 테이블 전략이다.
하지만... 여러 사용자가 동시에 이 테이블에 접근하여 동일한 ID의 글이나 카테고리를 생성할 수 있는 위험이 있어
이를 방지하기 위해 select... for update로 ID값을 가져와야 했다.
그래서 이 방법은 성능 저하 문제 때문에 적절하지 않다고 결론지었다.
해결방법 2. 복합 유니크 인덱스 + 끊어짐이라는 상태를 추가
또 다른 방법은 복합인덱스로 카테고리 별로 마지막임을 보장한 후
마지막이라는 상태 + 끊어짐이라는 중간 상태를 추가해서 해결해보려고 했다.
처음은 1번 방법과 똑같이 복합 인덱스를 설정하고 진행했다.
그다음으로 위에 사진과 같이 end라는 마지막과, disconnention이라는 끊어짐이라는 중간 단계를 추가했다.
로직을 살펴보면
- 먼저 next_id를 disconnention(-2)로 초기화한 writing을 save 한다.
- save 해서 나온 Id로 원래 마지막 글을 저장한 writing을 바라보게 한다.
- save 한 writing의 상태를 disconnention(-2)에서 end(-1)로 변경한다.
이렇게 3개의 단계로 로직을 변경하였다.
하지만 원래 로직은 2번의 쿼리를 날리는 반면, 변경된 로직은 3번의 쿼리를 날려서 글과 카테고리를 저장해야 한다.
그래서 이러한 방법은 1번보다는 괜찮은 방법이지만,
커넥션을 더 오래 잡고 있고, 인덱스를 사용해서 저장할 때 비용이 발생하므로 적절하지 않다고 생각했다.
구현은 했지만.....https://github.com/woowacourse-teams/2023-dong-gle/pull/417
해결방법 3. AOP를 활용하여 사용자 별로 동시성 제어
이방법을 적용해보았는데 말랑이 최범균의 유튜브를 보면 에플리케이션단에 락을 거는방법이 나온다그래서 알아보고 적용해보았다.
(고마워요 말랑!!! https://ttl-blog.tistory.com/ <-말랑의 블로그 많관부)
다시본론으로 가보면
우리 프로젝트에서는 한 명 한 명의 사용자가 관리자이다.
그래서 여러 사용자가 하나의 자원을 두고 요청을 하는 로직이 없어
사용자별로 애플리케이션 단에서 동시성을 제어해서 해결하면 되는 문제였다.
먼저 맨 위에는 커스텀 어노테이션을 만들어주었고,
밑에 noConcurrentAccess메서드의 로직을 보면
- 메서드에 입력된 memberId를 가져온다.(메소트 파라미터 1번째에 memberId가 오게한다.)
- computeIfAbsent메서드로 key(memberId)의 대한 value(Lock)을 가져온다.
- 지금 락이 걸려있다면 ConcurrentAccessException(커스텀 Exception)을 반환한다.
- 락이 걸려있지 않다면 메서드를 실행한다.
- 마지막으로 락을 해제한 후 사용자의 락을 지워준다.
한 명의 사용자가 동시에 요청을 하는 경우가 매우 적을 거 같아 위와 같이 애플리케이션 단에서 락을 걸어 해결하였다.
이러한 방법을 사용하면 사용자 간의 영향을 안 주면서 중복요청 문제를 해결할 수 있을 거 같다!!
적용한 코드
'우아한 테크 코스(우테코) > 우테코 공부' 카테고리의 다른 글
동글 프로젝트 테스트 코드 개선 및 최적화 (7) | 2023.10.01 |
---|---|
동글 프로젝트 쿼리 개선기 (0) | 2023.09.24 |
파사드 패턴으로 외부 API와 DB 트랜잭션 범위 분리하기 (0) | 2023.09.14 |
동시성 문제 해결을 위한 회원가입 기능 개선 (5) | 2023.09.11 |
전략패턴을 이용한 외부API 추상화하기 (0) | 2023.09.10 |