Home [football] 비동기 방식으로 처리되는 메시지 및 푸시알림 전송 로직 구현
Post
Cancel

[football] 비동기 방식으로 처리되는 메시지 및 푸시알림 전송 로직 구현

# 문제점


채팅 서비스가 동작하기 위해 필요한 기능 중 핵심은 하나의 채팅방에서 특정 사용자가 메시지를 작성해 전송한 경우, 채팅방에 접속해 있는 수신 대상자에게는 메시지가 직접 전송되어야 하며, 그렇지 않은 대상자에겐 푸시알림 으로 메세지 내용이 전송되어야 하는 것입니다. 하지만 채팅방에 초대된 사용자가 많으면 많을수록 해당 로직을 수행하는 시간을 길어질테고 이는 채팅 기능의 성능적인 측면에서 매우 불안한 요소를 제공하는 로직이라 판단되어 해결해 볼 필요가 있다고 생각했습니다.

# 해결방안


@Async

@Async Annotation은 Spring에서 제공하는 Thread Pool을 활용하는 비동기 메소드 지원 Annotation입니다. @Async를 활용한다면 간단하게 Spring 내에서 비동기 처리 메소드를 구성할 수 있고, 이전에 비동기 처리를 위해 구성해야하는 동일한 코드의 반복을 줄일 수 있었습니다.

적용 코드

ChatServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class ChatServiceImpl implements ChatService {

  @Override
  @Transactional
  public void sendMessage(int channelId, int sendUserId, String content) {

    Channel channel = channelRepository.findById(channelId)
        .orElseThrow(() -> new RuntimeException("채팅방 정보가 존재하지 않습니다."));

    User user = userRepository.findById(sendUserId)
        .orElseThrow(() -> new RuntimeException("회원 정보가 존재하지 않습니다."));

    Message message = Message.builder()
        .type(Type.MESSAGE)
        .content(content)
        .createAt(LocalDateTime.now())
        .channel(channel)
        .user(user)
        .build();

    messageRepository.save(message);

    
    List<Integer> userIdList = findMessageReceivers(channelId);

    // 조회된 수신자 리스트에 포함된 사용자들에게 한명씩 푸시 message 메소드를 실행합니다.
    for (int receiveUserId : userIdList) {

      if (receiveUserId != sendUserId) {

        PushMessageCommand command = PushMessageCommand.builder()
            .channelId(channelId)
            .sendUserId(sendUserId)
            .receiveUserId(receiveUserId)
            .content(content)
            .build();

        // 푸시 message 메소드, 반복적으로 호출
        chatPushService.pushMessage(command);

      }

    }

  }
  
}

위 코드를 보면, 만약 하나의 채팅방에 메세지를 전송해야 하는 수신자 대상이 매우 많을 경우 해당 메소드가 처리되는 시간이 딜레이될 수록 성능이 저하될 가능성이 매우 높습니다.

ChatPushServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ChatPushServiceImpl implements ChatPushService {

  private final RedisService redisService;

  private final RestTemplate restTemplate;
  
  @Async // 비동기 처리
  @Override
  public void pushMessage(PushMessageCommand command) {

    // Redis에서 수신자 정보를 통해 웹소켓에 연결되어 있는지 여부를 파악합니다.
    String address = redisService.getWebSocketSession(
        PREFIX_KEY + command.getReceiveUserId()
    );

    if (address == null) {

      // 웹소켓에 접속중이지 않은 경우는 FCM을 통해 푸시 알림을 보냅니다.
      log.info(command.getReceiveUserId() + "님에게 푸시 알림을 보냅니다.");

    } else {

      String uri = "http://" + address + "/ws/send/message";

      restTemplate.postForObject(uri, command, ResponseDto.class);

      log.info(command.getReceiveUserId() + "님에게 메세지 전송이 완료되었습니다.");

    }

  }

}

그래서 수신자 수만큼 반복적으로 호출되는 ChatPushService.pushMessage()에 @Async를 선언해 메소드가 처리되지 않더라도 다음 로직을 처리할 수 있도록 구성했습니다.

# 마치며


이처럼 메시지 전송 뿐만 아니라 푸시 알림까지 전부 동작이 완료되어야만 요청 처리에 대한 응답 객체가 클라이언트에게 전달되는 것이 아니라 사용자가 이미 보낸 메시지에 대한 전송 처리가 비동기 방식으로 동작해 채팅 기능을 수행하는 자원의 활용을 효율적으로 동작하게끔 구현했습니다.

# 참고자료


  • https://velog.io/@gillog/Spring-Async-Annotation%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%A9%94%EC%86%8C%EB%93%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0
This post is licensed under CC BY 4.0 by the author.

[football] Scale Out을 고려한 아키텍처 설계

[football] 웹소켓을 직접 활용한 채팅 서비스 구현