일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- redis
- 우테코
- resilience4j
- Spring Batch
- JWT
- DispatcherServlet
- Thread
- 살아남았다.
- 톰캣
- MDC
- spirng
- 우아한 테크 코스
- oauth
- Gateway
- 트랜잭션
- Transactio
- circuitbreaker
- 동시성문제
- 테스트코드
- Kotlin
- HikariCP
- Elk
- 우아한테크코스
- 최종 합격
- tomcat
- 오어스
- Spring cloud gateway
- AOP
- 커넥션 풀
- 우테코 5기
- Today
- Total
코딩은 내일부터
동시성 문제 해결을 위한 회원가입 기능 개선 본문
서론 및 버그가 일어나는 상황
동글 프로젝트를 하면서 개발서버와 실제 배포서버 2개의 서버를 사용하고 있는 상태였다.(DB서버까지 하면 총 3개다.)
회원가입 기능을 만들고 개발서버에서 1/10확률로 회원가입을 하면 똑같은 회원이 2명 생기는 거였다.
그래서 그 회원은 우리가 DB에서 1명 제거하지 않는 이상 로그인을 못하는 상황이다.
분명 OAuth로그인을 할 때 회원이 아니면 회원가입하게 로직을 구현해 놨는데
회원가입이 2번 일어나 나는 거였다.
프론트에서 말하길 react에서 api 로직을 수행하려면
useEffect라는 훅 내부에서 api 함수를 호출하는데 이 useEffect가 개발모드에서는 두 번 API가 호출된다.
그래서 이것 때문이구나 생각을 했는데 어떻게 해결해야 하는지 감이 안 왔다.
시간이 지나고 Real MySql을 읽어보고 DB관련 문제구나 알아차렸다.
그래서 이번글은 어떻게 트러블 슈팅을 했는지 적어보겠다.
문제의 코드
위에 보이는 코드로 회원가입과 로그인을 진행하였다.
코드를 설명하면 login메서드가 실행되면 DB에 member가 있는지 찾고,
없으면 save를 통해 회원을 저장하는 방식으로 로그인과 회원가입을 진행하고 있었다.
Wirte Skew 발생
문제가 발생한 상황을 예를 들어 설명하겠다.
A트랜잭션이 먼저 시작되고 잇따라 B트랜잭션도 시작한다.
A트랜잭션은 name이 비버가 있는지 먼저 찾고, 없으면 비버라는 이름으로 회원가입(저장)을 할 것이다.
하지만 select 하고 insert 하는 그 중간에 B트랜잭션이 select문으로 비버가 있는지 찾아본다.
그러면 B트랜잭션도 비버가 없으므로 회원가입을 진행을 할 것이다.
마지막으로 테이블을 확인해 보면 비버가 2 번들어가 있는 걸 볼 수 있다.
이러한 문제를 Wirte Skew라고 한다.
해결 방법
이제 문제를 알았으니 해결할 수 있는 방법을 알아보겠다.
해결 방법 1.Gap Lock(SELECT...... FOR UPDATE)
첫 번째 방법은 비관적 락인 SELECT문 뒤에 FOR UPDATE를 붙여서 쓰기락을 획득해 놓는 거다.
member라는 테이블에 위와 같이 초기 세팅을 했다.
auto commit을 꺼놓고 for update를 통해 락을 걸어봤다.
- unique제약조건이 걸려있고 쿼리의 조건이 1건의 결과를 보장하는 경우 Gap Lock은 사용되지 않고 Record Lock만 사용되는 거를 볼 수 있다.
- unique제약조건이 걸려있고 쿼리의 조건이 0건의 결과를 보장하는 경우 Gap Lock만 걸리게 된다.
즉, SELECT 했을 때 값이있는 경우는 Record Lock만, SELECT 했을때 값이 없는 경우는 Gap Lock이 사용된다.
그러면 회원가입을 할 때는 Gap Lock을 사용해서 동시성을 처리할 수 있지만 문제점이 한 가지 있다.
Gap Lock의 문제점
Gap Lock의 치명적인 문제점은 Dead Lock이 발생한다는 것이다.
예를 들어
시나리오
- A트랜잭션과 B트랜잭션에서 SELECT... FOR UPDATE문까지는 대기 없이 즉시 실행된다. (다시 말하면 두 개의 트랜잭션 모두 Gap Lock만 획득하게 되는데, Gap Lock은 항상 Shared 모드이므로 서로 충돌하지 않고 잠금 획득이 허용된다. )
- A트랜잭션이 INSERT문을 실행할 때insert intention gap lock이라고 불리는 gap lock의 일종인 lock이 필요하다.
위에 1,2번에서 문제가 발생하는데 먼저 insert intention gap lock은 gap lock 하고 호환되지 않는다.
그렇게 때문에 A트랜잭션이 insert 쿼리가 나갈 때 B트랜잭션의 gap lock이 해제되길 기다리고 있고,
B트랜잭션 역시 A트랜잭션에 gap lock이 해제되길 기다리고 무한 기다림 일명 데드락(Dead Lock)이 발생하게 된다.
데드락이 발생하기 때문에 사용할 수 없다고 판단했다.
해결 방법 2. SERIALIZABLE 격리 수준 or synchronized 설정
SERIALIZABLE격리 수준 또는 사용하는 메서드(또는 구간)를 synchronized로 설정하면
동시처리 능력이 떨어져 성능저하가 된다고 판단 해서 사용하지 않았다.
해결 방법 3. 복합 Unique INDEX 설정 및 DuplicateKeyException를 통한 예외 처리
이 방법을 프로젝트에 적용해서 동시성 문제를 해결했다.
먼저 SELECT.... FOR UPDATE를 사용하지 않고 복합 Unique INDEX 설정으로 유니크함을 보장해 주었다.
그리고 위와 같은 상황에서 똑같은 회원이 INSERT를 할 때 복합 Unique INDEX설정을 해서
DuplicateKeyException이 일어나고 코드상에서 예외를 catch 해서 커스텀 예외로 다시 반환해 준다.
이런 방법을 사용한 이유는
- 락을 사용하지 않아 데드락이 발생하지 않는다.
- 락을 사용하지 않아 동시성처리가 떨어지지 않는다.
이러한 이유로 복합 Unique INDEX + DuplicateKeyException를 통해 동시성 문제를 해결했다.
적용한 코드
https://github.com/woowacourse-teams/2023-dong-gle/pull/371
[BE] feat: 로그인 기능 추상화 및 동시성 문제 처리 by ingpyo · Pull Request #371 · woowacourse-teams/2023-dong-
🛠️ Issue close #367 ✅ Tasks 로그인이 추가될 때 중복되는 코드 제거 및 앞으로의 확장성 목적의 추상화 dev환경에서 회원가입 시 요청을 2번씩 보내는데 이 경우 회원의 정보가 2번 저장되는 에러
github.com
참고
https://medium.com/daangn/mysql-gap-lock-%EB%8B%A4%EC%8B%9C%EB%B3%B4%EA%B8%B0-7f47ea3f68bc
'우아한 테크 코스(우테코) > 우테코 공부' 카테고리의 다른 글
LinkedList로 글과 카테고리의 순서 구현했을 때 동시성 제어 (2) | 2023.09.16 |
---|---|
파사드 패턴으로 외부 API와 DB 트랜잭션 범위 분리하기 (0) | 2023.09.14 |
전략패턴을 이용한 외부API 추상화하기 (0) | 2023.09.10 |
Thread 와 톰캣 튜닝하기 (2) | 2023.09.09 |
HTTP와 톰캣은 뭘까? (6) | 2023.09.06 |