1:1 대화를 시작하려고 할 때 아래 API를 호출하게 된다.
@Override
@PostMapping("/chat-room/opposite/{oppositeId}")
public ApiResponse<SuccessBody<RoomResponse>> joinDirectRoom(
@Member MemberInfo memberInfo, @PathVariable("oppositeId") Long oppositeId) {
RoomResponse response = roomService.createDirectRoom(memberInfo.getMemberId(), oppositeId);
return ApiResponseGenerator.success(
response, HttpStatus.OK, RoomSuccessMessageCode.CREATE_ROOM);
}
두 유저 사이에는 채팅방이 존재하지 않기 때문에
두 유저가 각각 채팅방을 생성하였고
그 결과 서로가 다른 채팅방을 구독하고 있는 상황이 발생하였다.
(서로에게 메시지를 보내도 메시지를 받지 못 하는 상황...)
문제점은 만약 유저 1, 유저 2 사이에 채팅이 이루어지지 않는다면
채팅방을 생성할 필요가 없지만 이 방식대로 하면 채팅방이 생성이 된다.
즉 불필요한 채팅방까지 생성하게 되는 문제가 생긴다.
만약 유저가 n명 있을 때, 각 유저 쌍 사이에 채팅방을 생성한다고 가정하면, 총 생성되는 채팅방의 개수는 n^2개가 된다.
지금 우리 프로젝트에서는 유저의 수가 적지만 더 많은 유저의 수를 가지는 더 큰 규모의 프로젝트에서는 적용하지 못 할 것 같다는 생각이 든다.
그럼 아예 비관적 락을 사용해서 한 유저만 채팅방을 생성하도록 하자! 라는 생각을 했다.
즉 여기서는 findCommonChatRoom에서 공통된 채팅방이 있는지 조회 할 때 비관적 락을 사용하였다.
@Transactional
public RoomResponse createDirectRoom(Long senderId, Long oppositeId) {
ChatRoom commonChatRoom =
chatRoomRepository.findCommonChatRoom(senderId, oppositeId, RoomType.DIRECT);
if (commonChatRoom == null) {
Long chatRoomId = createNewChatRoom(senderId, oppositeId);
return RoomResponse.of(chatRoomId);
}
return RoomResponse.of(commonChatRoom.getId());
}
(테스트 코드 실행 시간 첨부)
두 요청이 들어왔다고 해보자
이때 비관적 락의 경우 Request2는 Request1이 다 처리가 될 때까지 block되고 기다려야 한다.
사실상 여기서 lock을 걸었던 이유는 두 유저 사이의 채팅방이 2개 이상이 생성되지 않도록 막기 위함이였는데
다른 유저 사이의 채팅방을 조회하는 케이스도 block된다.
따라서 두 유저의 정보를 바탕으로 named lock을 사용하면
의 경우에는 Request1, Request2 모두 동시에 처리가 가능하고
의 경우에는 Request2는 Request1의 처리가 다 끝날 때까지 block이 되므로 원하는 비즈니스 로직을 만들 수 있을 뿐더러 서비스의 처리 속도 또한 좋아질 것이라고 생각했다.
@Service
@RequiredArgsConstructor
public class FacadeRoomService {
private final RoomService roomService;
private final ChatRoomRepository chatRoomRepository;
public RoomResponse createDirectRoom(Long senderId, Long oppositeId) {
Long chatRoomId = 0L;
ChatRoom commonChatRoom =
chatRoomRepository.findCommonChatRoom(senderId, oppositeId, RoomType.DIRECT);
if (commonChatRoom != null) {
return RoomResponse.of(commonChatRoom.getId());
}
String lockName = getNamedLockName(senderId, oppositeId);
try {
chatRoomRepository.getLock(lockName, 60);
ChatRoom exist = chatRoomRepository.findCommonChatRoom(senderId, oppositeId, RoomType.DIRECT);
if (exist != null) {
return RoomResponse.of(exist.getId());
}
chatRoomId = roomService.createDirectRoom(senderId, oppositeId);
} finally {
chatRoomRepository.releaseLock(lockName);
}
return RoomResponse.of(chatRoomId);
}
private String getNamedLockName(Long senderId, Long oppositeId) {
return "direct_room_lock_"
+ Math.min(senderId, oppositeId)
+ "_"
+ Math.max(senderId, oppositeId);
}
}
(테스트 코드 실행 속도 첨부하기)
사실 내가 고려한 상황은 정말 특수한 상황이다.
위 2가지 조건이 만족해야만 두 유저 사이의 채팅방이 2개가 생성되어 서로 다른 채팅방을 subscribe하는 문제가 발생한다.
사실 위 케이스가 프론트 팀원과 채팅 기능이 제대로 되는지 확인하는 과정에서 발생하기는 했다.
그러나 정말 특수한 상황이기에 해당 이슈를 발견한 시점에는 해결하지 않았고 팀 안에서 다른 기능을 구현할 필요가 없을 뿐더러 다른 이슈를 해결할 필요가 없었기에 해당 이슈를 해결했다.
두 유저가 동시에 서로 사이의 채팅방을 동시에 생성하는 상황이 생기더라도 결국 두 유저의 목적은 '동일'하다.
두 유저의 값을 기반으로 채팅방에 고유한 값을 부여하였고
해당 값은 UNIQUE column으로 설정하였다.
그 결과 두 유저 사이의 채팅방이 2개 이상 생성되지 않았고 단 1개만 생성되는 요구사항을 만족하였다.
@Transactional
public RoomResponse createDirectRoom(Long senderId, Long oppositeId) {
String roomUUID = getRoomUUID(senderId, oppositeId);
ChatRoom commonChatRoom = chatRoomRepository.findByRoomUUID(roomUUID);
if (commonChatRoom != null) {
return RoomResponse.of(commonChatRoom.getId());
}
Long savedRoomId = saveChatRoom(senderId, oppositeId);
return RoomResponse.of(savedRoomId);
}
private String getRoomUUID(Long senderId, Long oppositeId) {
String combinedString = Math.min(senderId, oppositeId) + "_" + Math.max(senderId, oppositeId);
return UUID.nameUUIDFromBytes(combinedString.getBytes()).toString();
}
나는 코딩을 이용해서 "세상의 문제를 해결하고자 하는 사람이다"
근데 이번 케이스에는 동시성 이슈 발생했네? lock 이용해야겠다 라는 사고흐름으로 이어졌다.
즉 기술에 너무 의존한 나머지 다른 케이스를 생각하지 못 한것이다.
기술에 의존하지 말자. 나는 문제를 해결하고자 하는 사람이지 기술을 사용하는 사람이 아니다.