알림 시스템 구축하기(상) – 유저에게 먼저 다가가는 서비스 만들기

안녕하세요! LINER에서 백엔드 개발을 맡고 있는 토니입니다.

라이너가 하이라이트 유틸리티 툴에서 커뮤니티로 본격적으로 바뀐지 벌써 4개월이 지났습니다. 커뮤니티에서는 서비스와 유저의 상호 작용 뿐만 아니라, 유저와 유저 사이의 상호 작용도 잘 구성해야합니다.

커뮤니티와 SNS에서 유저간 상호 작용을 구성하는 다양한 장치 중 중요한 자리를 차지하고 있는 알림 시스템을 어떻게 개발했는지 이야기 해볼까 합니다!

노티 탭

첫 노티 시스템의 탄생은 지금은 없어진 첫 커뮤니티 도전으로 거슬러 갑니다.
LINER의 첫 커뮤니티는 브라우저 익스텐션을 통해 인터넷 서핑 중 옆에 띄우는 툴바로 시작 되었습니다.

노티는 툴바를 열면 노티 탭이 존재해 그곳에 노티 리스트가 쌓이는 방식이였습니다. 또한 새로운 노티가 생기면 툴바에 빨간 점이 뜨는 등의 UX 등도 제안되었습니다.

이외에도 다양한 아이디어와 요구사항들이 정리되며 실제 개발에 들어갔습니다. 개발 중에 커뮤니티가 개편되어 지금의 방향으로 바뀌었지만 노티 시스템은 유지되어 그대로 개발을 이어나갈 수 있었습니다.

요구 사항

  • 받은 노티의 리스트를 볼 수 있어야합니다.
  • 노티의 내용은 형식화 되어있으며 새로운 종류의 노티 추가가 쉬워야합니다.
  • 읽은 노티와 읽지 않은 노티를 구분해야합니다.
  • 새로운 노티가 왔음을 사용자에게 알려야합니다.
  • 당장 LINER를 켜두고 있지 않은 사용자에게 새로운 노티가 왔음을 알릴 필요는 없습니다.
    (리소스 부족과 임팩트 우선순위로 인하여 빠졌습니다.)

데이터

노티는 형식이 정해져 있으며 새로운 종류의 노티 추가가 쉬워야 했습니다. 예를 들어 제 하이라이트에 누군가 댓글을 달면 아래와 같은 형태의 노티가 와야합니다.

"{senderName}이 당신의 글에 댓글을 달았습니다: {commentContent}"

그렇기 때문에 노티를 위한 템플릿과 노티에 들어가는 동적인 정보들을 따로 저장하고 관리하도록 만들었습니다.

대략적으로 위와 같은 데이터 흐름을 처음 생각하게 되었습니다. Notification Metadata가 senderName과 같은 동적인 정보를, Notification Content가 템플릿 정보를 가지고 이들을 합쳤을 때 Notifiaction이 완성이 됩니다.


서버 구성

서버의 구성도 신경써야합니다. 기존의 도메인 로직을 가진 메인 서버에 넣기에는 두 가지 정도의 우려 점이 있었습니다. 노티 시스템은 여태가지 개발된 서비스 도메인 로직들과는 완전히 다른 성격을 띄었습니다. 그런 이질적일 수 있는 코드들이 도메인 로직 곳곳에 들어가 노티를 생성해야 했기에 기존 로직들에 섞이는 것이 우려되었습니다. 또한 기존 서버와는 다른 양상의 트래픽을 받아낼 것이라고 예상 되었습니다.

이런 이유로 main 서버notification 서버로 나누게 되었습니다.

main 서버는 유저가 댓글을 다는 등의 적절한 시점에 notification 서버로 노티의 metadata를 전달합니다.

notification 서버main 서버로부터 받은 metadata를 저장하고 유저가 조회를 요청시 notification으로 만들어 전달하는 역할을 합니다. 또한 각 노티들 마다 유저가 읽었는지, 클릭했는지, 새로운 노티가 발생했는지 등을 알려주는 기능도 수행합니다.


노티 전달 방법 Pull / Push

사용자에게 정보를 전달하는 방법으로 크게 PullPush로 들 수 있을 것 같습니다. Pull은 사용자가 요청하면 정보를 주는 방법이고 Push는 사용자의 요청이 없더라도 서버에서 먼저 정보를 주는 방식입니다.

두 방법은 각각 다른 장단점을 가집니다. Pull은 기존 http api 방식을 그대로 사용할 수 있어 구현이 쉽습니다. 다만 20초 등 주기적으로 사용자 쪽에서 요청을 해야하기 때문에 성능상 비효율이 존재합니다.

Push 방식은 Pull 방식에 비해 효율적입니다. 커넥션을 길게 유지하고 메시지 오버헤드가 적기 때문입니다. 그리고 Pull 방식보다 더 실시간에 가깝게 이용할 수 있습니다. 다만 새로운 기술 스택과 커넥션 관리 등 구현과 관리가 복잡합니다.

저희도 두가지 모두 고려해봤지만 Pull 방식을 선택했습니다. 웹소켓에 익숙하지 않았고 당장 노티에서 받아내야 할 트래픽이 적었기 때문입니다. 그래서 더 빠른 개발 및 배포를 할 수 있는 Pull 방식을 선택했습니다. 실제 배포 후 기대보다 더 트래픽을 잘 견뎌주었기 때문에 알맞게 선택했다고 생각했습니다.

데이터베이스 선택

백엔드 개발을 하다보면 RDB를 많이 접하게 됩니다. 실제로 백엔드 개발에서 RDB가 많이 선택되고 join 연산과 제약 조건 등으로 데이터간 관계를 쉽게 다루고 일관성 유지에 많은 도움을 줍니다.

다만 노티 데이터의 성격은 좀 다릅니다. 노티 데이터는 다른 데이터들과 복잡한 관계를 가지지 않습니다. 대체로 노티 데이터 하나가 하나의 노티를 완성하는 것으로 끝나는 경우가 많습니다. 또한 쓰기 작업이 읽기 작업만큼이나 중요한 작업입니다. 쓰기 과정에서 제약 조건을 검사하고 여러 인덱스 트리에 값을 기록하는 RDB의 특징이 오히려 득이 되지 않을 수 있었습니다.

이런 이유로 RDB 보다는 Document DB가 더 적합해 보였습니다. Document DB는 유연한 스키마와 빠른 쓰기 성능, 대용량 데이터에 유용하다는 특징을 가지는 NoSQL DB 중 하나입니다. 대표적으로 MongoDB가 존재합니다

저희는 DB라기 보다는 검색엔진에 가까운 ElasticSearch와 대표적인 Document DB인 MongoDB 중에서 고민했고 이미 클러스터 구성이 되어 운영되는 ElasticSearch를 선택했습니다

푸시 노티

노티 탭을 포함한 초기 라이너 커뮤니티가 배포된 이후, 모바일에 맞게 커뮤니티를 개편하는 작업이 이루어졌습니다. 푸시 노티는 모바일에서 큰 임팩트를 보이기 때문에 이에 대한 개발도 필요해졌습니다.

여기서 푸시는 위에서 설명한 노티 전달 방식 중 하나로 소개했던 브라우저 푸시와는 다른 관점입니다. 사용자가 받는 경험 관점에서의 푸시이며 서비스를 당장 켜두고 있지 않은 사람들에게도 정보를 전달하는 것을 말합니다.

아래와 같이 라이너를 켜두지 않아도 노티를 보낼 수 있습니다.

푸시 노티 구현 기술

이러한 푸시 노티를 구현하는데 주로 Apple의 APNS와 Google의 FCM이 쓰입니다. APNS 서버와 FCM 서버에 사용자들에게 이런 노티를 보내줘하고 알려주면 알아서 보내주는 방식으로 동작합니다.

APNSFCM 방식이 주로 쓰이는 이유는 구현의 편의성도 있지만 요즘은 디바이스들, 특히 모바일의 보안이 중요시 되면서 디바이스, os 개발사에서 제공한 방식 외로는 개발하기 힘든 점도 있습니다.

저희는 Google의 FCM을 사용했습니다. FCM은 추가 비용이 들지 않고 APNS 서버의 구현도 되어있어 Apple 제품에 추가적인 APNS 없이도 푸시 노티를 보낼 수 있기 때문이었습니다.

서버 구성

서버의 구성은 다음과 같이 변경되었습니다

노티의 요구사항이 더 복잡해졌고, 노티와 관련된 로직과 유저에게 전달되는 방식을 구별하기 위하여 worker를 추가하였습니다.

푸시 노티를 위해 유저의 디바이스 정보를 관리해야합니다. 유저가 알림 권한을 허락하고 푸시 노티를 받을 디바이스를 등록하고 앱 삭제 등으로 더이상 유효하지 않은 디바이스를 제거하는 등의 과정이 필요합니다.

또한 매일 아침이나 매주 등으로 주기적으로 나가야하는 푸시 노티에 대한 요구 사항이 나왔으며 글로벌 서비스인 LINER에서는 유저들의 국가, 시간대 정보를 수집해야합니다. 또한 이런식으로 세분화된 노티가 추가될 가능성에 대해서 고려해야했습니다.

notification의 기능은 크게 바뀌지 않았으나 관점이 살짝 바뀌었습니다. 기존에는 노티 정보와 노티를 다루는 것이였다면 지금은 노티 정보를 ElasticSearch에 저장하고 유저에게 노티 목록을 조회하는 역할을 하는 것으로 더 제한되었습니다.

Fcm은 Google에서 제공하는 Api를 호출해 안드로이드, IOS, 웹에 푸시 노티를 보내는 역할을 합니다.

노티 생성 로직

main 서버에서 notification 서버로 worker 없이 바로 이어질 때는 main 서버에서 노티를 만들기 위해 필요한 정보를 모두 만들어서 notification 서버로 줬습니다.

처음에는 노티에 필요한 정보가 이미 비즈니스 로직 중간에 나올 것으로 예상했습니다. 하지만 늘 그렇듯 예상은 빗나가고 더 복잡한 노티 요구 사항이 나왔습니다.

A님을 포함한 N명이 당신의 글을 좋아합니다.

위와 같은 노티는 A 유저가 나의 글에 좋아요 를 했을때 발생합니다. 노티를 완성하기 위해서는 A 유저 외에 몇 명의 유저가 좋아요를 눌렀는지가 비즈니스 로직 외에서 추가로 필요합니다. 또한 좋아요를 누를 때마다가 아닌 N명 이상일 때만 노티를 보내게 하는 등 노티를 보내는 시점도 변경될 가능성이 있습니다.

이러한 로직들에 대한 책임이 main 서버와 notification 서버 관계에서는 main 서버에 존재합니다. 그렇기 때문에 비즈니스 로직이 복잡해질 것이 우려되었고 이러한 책임을 worker로 옮겼습니다.

main 서버 – worker – notification 서버 관계에서 main 서버는 노티를 만드는데 필요한 최소 정보만 제공하고 노티를 완성하는 것은 worker에서 진행하는 것으로 만들었습니다

남은 길

알림 시스템을 구현한 시간을 다 합하면 2달이 넘는 시간을 사용한 것 같습니다. 늘 그렇듯 제한된 리소스와 부족한 경험에서 개발을 이어가다보니 아쉬움이 남는 것 같습니다.

main 서버와 worker 통신은 http를 통해 이루어집니다. 그 사이에 따로 큐를 두고 있지 않아 현재는 급격히 늘어나는 트래픽이나 한 번에 많은 유저 들에게 보내야하는 노티 등을 처리하는데는 무리가 있습니다. 앞으로는 kafka와 같은 메세지 큐를 두는 것으로 개선할 수 있을 것 같습니다.

worker에서 노티를 생성하는 로직들에서 유사한 코드들이 많이 쓰여지는 것도 개선 포인트가 될 수 있고 Apns를 직접 쓰고 있지 않아 사파리 브라우저에 웹 푸시를 못 보내고 있는 것도 개선 포인트로 남겨져 있습니다.

직접적인 코드나 아키텍처 개선 외에도 노티 전달 방법에서 pull과 push를 선택하는 과정에서도 아쉬움이 남습니다. 성능에 대해 낙관적으로 바라봐 pull로도 충분할 거다 라고 생각하고 개발했고 또 그러한 결과를 보여줬지만 그 과정에서 정확한 성능을 모르는 만큼 저와 동료들에게 두려움으로 다가온 기억도 남습니다.

넷플릭스의 멋진 알림 시스템과 같은 시스템을 꿈꾸었지만 그런 것들은 한 번에 만들어지지 않는 다는 것을 이번 개발을 통해 배운 것 같습니다. 계속해서 복잡해지는 요구 사항과 변화하는 환경에 맞춰 지속적으로 개선을 이어나가다 보면 언젠가는 괜찮은 시스템으로 도달하지 않을까 라는 생각을 남기며 글을 마칩니다.