프로젝트를 진행하면서 발생했던
좋아요 기능에 대한 동시성 이슈에 대해서 알아보자!!
먼저 내 프로젝트의 좋아요 기능은
좋아요 토글 기능으로 만들어져 있다
(즉 좋아요 추가 API, 좋아요 삭제 API를 분리한 것이 아닌 토글 형식으로 좋아요 유무를 통해 판단)
발생했던 문제
일단 문제를 말하기 전에
웹 어플리케이션 서버가 어떤식으로 동작하는지에 대해서 간단하게 알아보자!
기본적으로 WAS는 스레드를 사용하여 여러 요청을 동시에 처리하는데
이유는 사용자의 요청을 병렬로 처리하면 성능적인 면에서도 이점을 보기 때문이다
이는 당연하다
한 사람이 모든 주문을 처리하는 레스토랑
여러 명의 직원이 동시에 주문을 처리하는 레스토랑은
성능적인 면에서 이득을 볼 수 있다.


사실 위의 그림은 매우 간단하게 만들어서 완벽한 아키텍쳐의 구성은 아니다 ㅋㅎ
위의 그림처럼 스레드 풀을 사용하여 여러 요청을 동시에 처리하는데 미리 준비된 스레드를 가지고
재활용하는 방식을 이용하여 요청을 처리한다.
저 스레드의 풀의 크기와 정책을 제어 해서
성능과 안전성을 확보하는데 집중한다고 한다!
그럼 이제 어디서 문제가 발생하는지 알아보자!



분명히 100명의 사용자가 좋아요 100개의 요청을 보내면
이 트랙 객체의 좋아요 개수는 100개가 나와야하는데 알 수 없는 값을 반환..



좋아요 개수가 11개로 반환값이 나왔다!
심지어 Like 객체도 100개가 생성되어져야 하는데
데이터의 정합성이 맞지 않는다..
문제가 뭐지?
로그를 살펴보자

데드락이 발생했다?
데드락이 발생하는 조건 4가지 중
자원은 한 번에 하나의 프로세스나 스레드만 점유할 수 있는 상호배제 조건을 지키지 못한 결말
show engine innodb status
위의 쿼리를 통해 정확한 DB 상태를 알아보자

매우 자세하게 분석할 수는 없지만 하나의 트랜잭션에서
3개의 행에 잠금이 걸려 있고
LOCK WAIT
트랜잭션이 다른 트랜잭션에 의해 점유된 자원을 기다리고 있다? 라는 의미 인거 같다.
즉 트랜잭션끼리 교착 상태에 빠져서 데드락 상황에 빠져 있다는 것을 의미한다.
likeCount 변수는
모든 스레드가 접근할 수 있는 Heap에 저장되므로,
이를 여러 스레드가 동시에 사용할려고 해서 충돌이 발생하고
데드락이 걸린 문제이다!
해결방안
synchronized 키워드 사용
synchronized란 여러개의 스레드가 한개의 자원을 사용하고자 할 때
현재 자원을 사용하고 있는 스레드를 제외한
나머지 스레드들은 데이터에 접근 할 수 없도록 막는 개념

왜 여전히 문제가 해결되지 않지?
1. synchronized 의 문제인가?

로그를 찍어서 임계영역의 실행 시간을 찍어보았지만 알 수가 없었다.
2. @Transaction 문제인가에 대해서 알아보자

JPA와 스프링 트랜잭션 로깅 레벨 DEBUG로 활성화 후
로그 내용이 복잡해서 간단하게 결과 내용을 설명을 하자면
로그 해석
- 요청 1이 트랜잭션 시작
- 요청 1의 트랜잭션이 시작
- 요청 1이 임계영역에 진입
- 요청 1이 synchronized로 보호된 메소드의 임계영역에 진입.
- 이는 요청 1만 메소드에 진입할 수 있고,
- 다른 요청은 대기해야 함을 의미
- 요청 1 처리 중
- 요청 1이 실제로 작업을 수행 중.
- 이 시점에서는 요청 2가 synchronized 메소드에 진입할 수 없어야 한다.
- 요청 2 트랜잭션 시작??
- 여기서 문제가 발생
- 트랜잭션이 @Transactional로 시작되었다는 것은 요청 2가 이미 likeTrack 메소드에 접근했음을 암시.
@Transactional의 미스터리
Spring의 @Transactional은 AOP를 기반으로 동작하는데
스프링 AOP는 프록시 객체를 기반으로 구현되어져 있다고 한다.
이 프록시 객체는 실제 객체의 메서드를 호출하기 전에
트랜잭션을 시작하고 호출이 끝난 뒤 트랜잭션을 커밋하거나 롤백하는 역할을 한다.
프록시 객체가 어떻게 만들어지는지 알아볼까?
프록시 객체는 실제 객체를 상속 받아서 만들어진다.
다만 여기서 문제가 되는건
synchronized
위의 키워드는 상속을 할 때 메서드 시그니처에 포함되지 않는다..
즉 동기화 문제를 해결 하는 키워드인 synchronized가 동작되지 않는다..
다시한번 왜 같이 쓰면 문제가 생기는지에 대해 천천히 알아보자
- @Transaction 프록시 객체는 트랜잭션을 시작한 후에 호출
- 이 과정에서 트랜잭션이 시작되기 전 다른 스레드가 들어올 여지가 생김
- ( 왜? synchronized는 실제 객체에만 적용이 되기 때문에)
- synchronized는 이 트랜잭션 경계를 보호하지 못함
- 동시성 문제가 발생 ㅜㅜ

드디어 해결이 되었는가...
스프링의 선언적 트랜잭션이 문제였으면 사용하지만 않으면 되는거 아닌가?
TransactionStatus status = transactionManager
.getTransaction(new DefaultTransactionDefinition());
수동으로 트랜잭션을 제어하면 문제를 해결할 수 있다..
해결하지 못한 숙제
위의 방법은 좋지 않은 단점이 하나 존재한다..
서버를 scale-out시 위의 방법은 모두 의미가 없어진다..
서버 A에서 synchronized가 동작하고 있어도
서버 B에서는 자유롭게 접근하여 데이터 충돌이 발생할 수 있기 때문이다.
synchronized는 JVM 단위로만 작동되기 때문에 ..
데이터 무결성을 보장하기 위한 DB 차원의 Lock에 대해서 다음 포스팅에서 알아보자!!!
2024.12.02 - [프로젝트] - 좋아요 버튼의 숨겨진 딜레마: DB Lock 관련

'프로젝트' 카테고리의 다른 글
| 좋아요 버튼의 숨겨진 딜레마: DB Lock 관련 (1) | 2024.12.02 |
|---|---|
| 안전한 파일 업로드에 대한 처리란 무엇일까? chapter 2 (0) | 2024.11.27 |
| 동적 쿼리와 배치 사이즈 최적화로 1+N 문제 해결 및 성능 개선 (0) | 2024.11.23 |
| Offset 페이징에서 효율적인 대댓글 처리 방법 (0) | 2024.11.23 |
| 안전한 파일 업로드에 대한 처리란 무엇일까? chapter 1 (0) | 2024.11.10 |