Skip to content

Long Polling으로 랜덤매칭

HeeJu Cho edited this page Oct 26, 2023 · 4 revisions

기능 요구사항

사용자가 채팅 버튼을 누르면, 랜덤으로 상대방이 매칭되어 채팅방이 생성되어야 합니다.
중요한 요구사항은 다음과 같습니다.

  • 실시간으로 접속되어있는 사용자끼리 1:1 매칭할 것
  • 같은 그룹의 사용자끼리 매칭할 것
  • 차단한 사용자와는 매칭하지 않을 것

구현방식 채택 과정

랜덤매칭 기능을 작업 흐름은 다음과 같습니다.

  1. 사용자가 랜덤채팅 요청 시 대기열에 저장
  2. 대기열에서 매칭 상대방 찾기
    1. 매칭되면 채팅방 생성 후 채팅방 id 반환
    2. 매칭 실패하면 422 코드 반환
  3. 422 코드를 받으면 클라이언트에서는 사용자에게 매칭에 실패했음을 알리고, 재요청 할 것인지 아니면 나갈 것인지 물음

1. SSE (Server Sent Events)

가장 먼저 고려한 방식은 SSE입니다.
SSE는 서버와 클라이언트가 한 번 연결을 맺고 나면, 일정시간 동안 서버에서 클라이언트로 데이터를 전송할 수 있습니다.
타임아웃 시 브라우저에서 자동으로 서버에 재연결 요청을 보내기 때문에 Long Polling이나 Polling보다 효율적입니다.

매칭 실패 시 클라이언트에서는 "매칭 상대방을 찾을 수 없습니다!"라는 메세지를 띄우고 사용자에게 재요청 할 것인지 아니면 나갈 것인지 묻습니다.
이를 위해 타임아웃 시 예외코드를 반환합니다. 실패응답을 클라이언트에서 별도로 처리하지 않으면 SSE에서는 자동으로 재연결 요청을 보내 사용자가 랜덤 매칭을 무기한으로 기다리게 됩니다.

emitter.onTimeout(() -> {
            emitter.completeWithError(new CustomException());
        });

타임아웃마다 클라이언트가 재요청을 한다면 Long Polling과 비교해 SSE의 장점이 없어지기 때문에 SSE에서 Long Polling으로 적용기술을 변경했습니다.

2. Long Polling

Long Polling에서의 동시성 문제

    private final Map<String, DeferredResult<Map<String, Object>>> que = new ConcurrentHashMap<>();

    // 타임아웃 시 422 반환
    DeferredResult<ResponseEntity<Map<String, Object>>> deferredResult =
                new DeferredResult<>(10 * 1000L, 
                        ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(Map.of("message", "매칭에 실패했습니다."));

    // 요청 시 que에 저장
    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);
    });

참고자료

Clone this wiki locally