실시간 다중 채팅 서비스 프로젝트를 진행하며 겪었던 문제와 해결과정을 기록합니다.
public class SimpleChatRoomService {
...
@Override
@Transactional
public Room enterRoom(식별자_자료형 roomId, 요청_모델 request) {
1. 데이터베이스로부터 채팅 방 엔티티를 불러온다. 없다면 예외 발생.
2. 채팅 방에 입장할 수 있는지 검사 수행.
2.1 수용인원 검증
2.2 비밀번호 검증
3. 채팅 방 입장 처리
3.1 room.attending++;
2.3 persis(new 참여자_엔티티));
}
...
}

이상현상(Lost Update)이 발생합니다.synchronized block을 설정해 최초 접근 쓰레드만 로직 수행을 허용하고 나머지 쓰레드는 대기하도록 만들 수 있습니다.public class SimpleChatRoomService {
...
@Override
@Transactional
public synchronized Room enterRoom(식별자_자료형 roomId, 요청_모델 request) {
... 로직 수행 ...
}
...
}
sychronized block으로임계영역을 설정한다고 해도 문제를 해결 할 수 없는데 원인은 다음과 같습니다.@Transactional 프록시의 상속 대상 범위 문제
- @Transactional 어노테이션이 붙은 메서드를 Spring AOP를 이용한 동작 방식이 적용됩니다. 이 때, 동작 시퀀스를 의사코드로 나타내면 다음과 같습니다.
public class SimpleChatRoomServiceProxy extends SimpleChatRoomService{ ... @Override publci Room enterRoom(식별자_자료형 roomId, 요청_모델 request) { try { 1. 트랜잭션 시작 2. super.enterRoom(roomId, request); // 임계 영역 3. 트랜잭션 커밋 } catch (예외 e) { 1. 예외 처리 및 트랜잭션 롤백 } } ... }- SimpleChatRoomService.enterRoom()을 상속받은 프록시를 이용해 동작하는데 synchronized 키워드는 메서드 시그니처가 아니라 상속대상이 아닙니다.
- 따라서 트랜잭션이 반영되기 전 임계영역을 벗어나 다른 트랜잭션이 시작될 수 있습니다.
요청을 수행하는 API 서버가 다중화 됐을 때
- 임계영역의 범위를
@Transactional어노테이션으로 적용된 프록시 메서드보다 크게 적용해도 현재 실행중인 프로세스의 범위를 벗어날 수 없습니다.- 요청을 수행하는 서버가 2대 이상인 경우 다른 서버 프로세스에서 수행하는 동시 요청에 대해 동시성 처리가 불가능합니다.


MySQL 공식문서 참조: https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html
데드락이 발생합니다.@Version, @Lock 어노테이션으로 적용할 수 있습니다.// 엔티티
public class Room {
...
@Version
private Long version;
...
}
---
// 레포지토리
public interface ChatRoomRepository extends JpaRepository<Room, Long> {
...
@Lock(value = LockModeType.OPTIMISTIC)
@Query("select room" +
" from Room room" +
" join fetch room.participants participant" +
" where room.id = :id" +
" and room.removedAt is null" +
" and participant.removedAt is null")
Optional<Room> findByIdFetchJoinParticipantWithOptimisticLock(Long roomId);
...
}
public class SimpleChatRoomService extends ChatRoomService {
...
@Override
@Transactional
public Room enterRoom(식별자_자료형 roomId, 요청_모델 request) {
while (true) {
try {
... 동작 수행 및 응답 반환 ...
} catch (ObjectOptimisticLockingFailureException exception) {
... 일정 시간 대기 or 예외 로직 수행 ...
}
}
}
...
}
@Lock 어노테이션으로 간단히 적용할 수 있었습니다.public interface ChatRoomRepository extends JpaRepository<Room, Long> {
...
@Lock(value = LockModeType.PESSIMISTIC_WRITE)
@Query("select room" +
" from Room room" +
" join fetch room.participants participant" +
" where room.id = :id" +
" and room.removedAt is null" +
" and participant.removedAt is null")
Optional<Room> findByIdFetchJoinParticipantWithOptimisticLock(Long roomId);
...
}
단일 데이터베이스를 운용하는 경우 강력한 동시성 처리 수단이 될 수 있습니다.유휴/대기 상태의 스레드가 많아져 처리량을 저해하는 문제가 발생할 수 있습니다.비관적 락을 적용했습니다.Redis나 다른 Lock Storage를 도입해 어플리케이션 레벨에서 락을 잡고 원자적으로 처리하는 방법도 있었습니다.