티스토리 뷰

(이 글은 외로운 우테코 5기 취준생 “김동욱”, “이건회” aka “그레이”, “하마드”가 작성했습니다.)

사건의 발단

때는 23년 12월 말...하반기 공채에 무참히 실패한 우테코 5기 수료생 두 남자가 만났다. 서울대입구 라멘집에서 늘 그렇듯 일상적인 개발 얘기를 하던 와중 말을 꺼냈다.

 

"피움(23년에 진행한 식물 관리 서비스 프로젝트)의 알림 기능이 문제가 많아 보인다. 프로젝트 막판이라 생각없이 짠 코드가 너무 많다."

처음에 그 말을 꺼냈을 때는 별 관심이 없어 보였다.

 

"이미 끝난 프로젝트이기도 하고...알림? 그거 내가 짠 기능 아닌데? 굳이 신경써야 하나? 그냥 다른 프로젝트 하면 안돼?ㅎㅎ"

하마드는 대충 이런 생각이었다. 하지만 식사 후 서울대입구역 집무실에서 내가 그에게 이 프로젝트의 당위성을 역설한 뒤, 생각이 바뀌었다.

 

지금이야 사용자가 많지 않으면 별 문제가 없어 보이지만, 만약 사용자가 많이 늘어난다면, 그래서 한 번에 보내야 하는 알림 갯수가 10만개,,,20만개,,,주욱 늘어난다면?

 

그렇게 된다면 문제가 있었다.


그래서 뭐가 문제였나?

간단하게 앞서 우리가 만들었던 알림 기능에 대해 소개하도록 하겠다. 우리 서비스는 개인 맞춤형 식물 관리 서비스다.

그리고 알림 기능은 사용자가 당일 관리(물주기)해야하는 식물이 있을 경우, 당일 07시에 스케줄러를 통해 그날 관리할 식물이 있는 사용자 전체에게 동시에 모든 알림 요청을 보낸다.

 

알림 기능을 도입한 시기는 우테코 수료 직전 막바지였다. 급하게 도입하다 보니, 단일 알림이 정상적으로 오고 있는지만 테스트했지, 실제로 07시에 알림이 발생하는지, 그리고 해당 시간에 사용자에게 알림이 제때 도착하는지 제대로 확인도 하지 않았다. 이제 느긋한 마음으로 찬찬히 코드를 뜯어고쳐보니, 문제가 속속들이 드러나기 시작했다.

 

근본적 문제, 알림 속도가 드럽게 느리다

일단 알림 갯수가 늘어날 때 현재 서비스에서 어떻게 처리될지 확인하기 위해, 알림 갯수를 동시에 500개, 1000개, 5000...최종적으로 10만개씩 보내며 어떻게 처리되는지 확인해보고자 했다.

알림 갯수가 많아질수록 소요되는 시간이 답도 없이 늘어난다.

 

위는 실제로 5000개까지 테스트를 수행해본 결과다. 처음 300개의 요청을 보냈을 때, 처리까지 약 2분이 소요되는 것을 보고 그려려니 했다. 그러나 1000개의 요청을 보냈을 때 전체 알림 전송 시간이 4분까지 늘어나는 것을 보고 뭔가 느리지 않나? 라는 생각이 팍 들었다. 이후 갯수를 확 늘려 5000개를 보내니 정확히 다섯배 늘어난 시간인 20분이 걸렸다. 다음에는 1만 개를 보내려 했지만, 이거 계속 늘려가면서 10만개까지 보내다가는 알림 시간 테스트하는데만 며칠 쓸 것 같았다.

 

우리가 만들려는 알림 기능의 생명은 사용자에게 리마인드를 해야 하는 07시에 최대한 가깝게 보내야 하는 것이다. 그러나 오천개를 보내는데도 20분이 걸린다? 만약 10만 개를 보내야 한다면? 산술적으로 400분이 넘는 시간이 걸린다. 그럼 07시에 알림을 받아야 하는 누군가의 사용자는 운이 나쁘면 점심밥을 먹고 나서야 물을 줘야 하는 알림을 받아보게 된다.


또 다른 문제, 하나의 알림 요청만 실패해도 뒤의 모든 알림 요청이 실패한다

심지어 적은 갯수의 알림을 보내는 상황에서도 문제가 생겼다. 5000개의 알림을 보내는 도중, 중간에 FCM 관련 문제(혹은 우리의 잘못된 요청)로 런타임 에러가 발생하면 뒤에 있는 모든 알림 요청이 실패하고 취소된다.

따라서 운이 좋은 사용자 몇 명은 알림을 받아 보겠지만, 운 나쁜 사용자 몇 명은 알림을 보내려는 시도조차 하지 못한 채 취소된다. 극단적으로 말해서 5000개를 보내야 하는데 두 번째 알림 요청이 실패하면 1명은 성공, 4999명은 그대로 실패다.

@Component
@RequiredArgsConstructor
public class NotificationEventListener {

    private final NotificationService notificationService;

    @EventListener
    @Async
    public void handleNotificationEvents(NotificationEvents notificationEvents) {
        for (NotificationEvent event : notificationEvents.getNotificationEvents()) {
            notificationService.sendNotification(event.getDeviceToken(), event.getTitle(), event.getBody());
        }
    }
}

위 코드는 우리가 실제로 알림 이벤트가 발생했을 때, 알림 처리 로직을 수행하는 코드다. 보다보면 뭔가 이상함을 느낄 것이다.

@Async 어노테이션을 통해 알림 이벤트를 호출하는 스레드와 알림 이벤트를 수행하는 스레드가 분리돼 있지만, 결국 전체 알림은 하나의 스레드가 반복문을 통해 동기적으로 모든 알림을 보낸다. 그니까 알림 갯수가 늘어나면 늘어날 수록 하나의 스레드가 처리해야 할 양, 그리고 처리 시간이 선형적으로 쭈우욱 늘어나고, 중간에 하나만 에러가 터져도 나머지 이벤트들은 빛을 보지 못하고 장렬히 전사하게 된다.

 

코드 리뷰 과정에서 왜 캐치하지 못했을까? 치명적인 결함이다.

"헤헤...@Async 달았으니까 비동기로 빠르게 처리되겠지...?"

라는 단순무식한 생각으로 바보같이 넘어갔다. 이런 정신상태로 개발자가 된다고 말할 수 없을 것 같았다.

 

그래서 우리는 결심했다. 10만개의 알림 요청을 최대한 빠르게 줄여보자. 우리가 할 수 있는 만큼.

 

정리

  • 백수생활 중에 밥먹다가 피움 알림 서비스 고치자고 제안함
  • 알림 10만 개가 동시에 일어난다고 가정했을 때, 총 처리시간이 400분 걸림
  • 알림 하나가 실패하면 모든 알림 실패함
  • 이렇게 생각없이 코딩하면 개발자 못할거같음
댓글