-
Notifications
You must be signed in to change notification settings - Fork 0
Long Polling으로 랜덤매칭
사용자가 채팅 버튼을 누르면, 랜덤으로 상대방이 매칭되어 채팅방이 생성되어야 합니다.
중요한 요구사항은 다음과 같습니다.
- 실시간으로 접속되어있는 사용자끼리 1:1 매칭할 것
- 같은 그룹의 사용자끼리 매칭할 것
- 차단한 사용자와는 매칭하지 않을 것
랜덤매칭 기능을 작업 흐름은 다음과 같습니다.
- 사용자가 랜덤채팅 요청 시 대기열에 저장
-
Redis
에 실시간으로 요청한 사용자를 저장합니다. - Map에 사용자의
DeferredResult
를 저장합니다.
-
- 대기열에서 랜덤으로 매칭 상대방 찾기
- 매칭되면 채팅방 생성 후 채팅방 id 반환합니다.
- 매칭 실패하면 422 코드 반환합니다.
- 422 코드를 받으면 클라이언트에서는 사용자에게 매칭에 실패했음을 알리고, 재요청 할 것인지 아니면 나갈 것인지 묻기
가장 먼저 고려한 방식은 SSE
입니다.
SSE
는 서버와 클라이언트가 한 번 연결을 맺고 나면, 일정시간 동안 서버에서 클라이언트로 데이터를 전송할 수 있습니다.
타임아웃 시 브라우저에서 자동으로 서버에 재연결 요청을 보내기 때문에 Long Polling
이나 Polling
보다 효율적입니다.
저희 애플리케이션에서는 매칭 실패 시 "매칭 상대방을 찾을 수 없습니다!"라는 메세지를 띄우고 사용자에게 채팅을 재요청 할 것인지 아니면 다른 화면으로 나갈 것인지 묻습니다.
이를 위해 타임아웃 시 예외코드를 반환합니다.
실패응답을 클라이언트에서 별도로 처리하지 않으면 SSE
에서는 자동으로 재연결 요청을 보내 사용자가 랜덤 매칭을 무기한으로 기다리게 됩니다.
emitter.onTimeout(() -> {
emitter.completeWithError(new CustomException());
});
타임아웃마다 클라이언트가 재요청을 한다면 Long Polling
과 비교해 SSE
의 장점이 없어지기 때문에,
Http를 사용하는 Long Polling
이 더 적합하다고 판단해 SSE
에서 Long Polling
으로 적용기술을 변경했습니다.
Long Polling
은 단방향 통신으로 클라이언트에서 요청을 해야 서버에서 응답을 보낼 수 있고,
자동으로 재요청이 가지 않기 때문에 클라이언트는 응답을 받은 뒤 다시 요청을 보내야 합니다.
Long Polling
에서는 클라이언트의 요청에 즉시 응답을 보내지 않고 일정시간동안 기다렸다가 응답을 보냅니다.
응답을 보내는 시점은 두 가지로 나눠볼 수 있습니다
- 서버의 데이터가 변경되었을 때
-
deferredresult.setResult()
호출
-
- 정해진 타임아웃 시간이 지났을 때
-
new DeferredResult(10 * 1000L)
생성자로 타임아웃 시간 설정
-
아래와 같이 Long Polling
을 사용해 랜덤 매칭 기능을 구현했을 때, 동시성 문제가 발생했습니다.
Chatcontroller.java
// 타임아웃 시 422 반환하도록 설정
DeferredResult<ResponseEntity<Map<String, Object>>> deferredResult =
new DeferredResult<>(10 * 1000L,
ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(Map.of("message", "매칭에 실패했습니다."));
// 클라이언트 요청 시 que에 deferredResult 저장
que.put(key, deferredResult);
// 랜덤매칭 로직 실행
var result = chatService.matching(groupId, memberId);
// 매칭 성공 시, 본인과 매칭 상대방 성공 응답 전송
if (result.geChatRoomId != 0L) {
deferredResult.setResult(ResponseEntity.ok().body(Map.of("chatRoomId", result.getChatRoomId)));
var partnerDeferredResult = que.get(partnerKey);
partnerDeferredResult.setResult(ResponseEntity.ok().body(Map.of("chatRoomId", result.getChatRoomId)));
}
// 콜백 시, redis와 대기 큐에서 본인과 매칭 상대 제거
deferredResult.onCompletion(() -> {
redisService.remove(key);
redisService.remove(partnerKey);
que.remove(deferredResult);
que.remove(partnerDeferredResult);
});
먼저 A가 랜덤 매칭 요청을 하고 잠시 후 이어서 B가 랜덤 매칭 요청을 합니다.
A의 요청으로 A와 B가 매칭되었을 때, A와 B의 deferredResult
의 setResult()
를 호출해 클라이언트에 응답을 보냅니다.
그리고 A의 콜백 메서드 onCompletion()
가 실행되면 대기열에서 A와 B를 제거합니다.
그런데 A의 콜백 메서드가 실행되어 두 사람이 대기열에서 빠지기 전에, B의 요청에 의해 B가 다른 사람과 매칭이 됩니다.
B는 두 사람과 중복으로 매칭이 되는 문제가 발생합니다.
Chatcontroller.java
// 요청 시 2초 기다리기
try {
Thread.sleep(2 * 1000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ChatService.java
// synchronized 사용
@Transactional
public synchronized MatchingResult matching() {
...
}