개요
iOS 어플리케이션의 알림 파트를 맡게 되면서, 앱 내 알림과 푸시 알림에 대해서 iOS 푸시 알림 구현하기 구현편 - Spring, AWS SQS, Lambda, Firebase 로 정리한 적이 있었다.
한편, 이후에 서비스가 커지는 것을 기대해보았을 때 어플리케이션 서버에서 전부 처리하는 것이 아닌, 이벤트만 발행하고 해당 이벤트를 구독하는 별도의 알림 서버로 작성하면 어떨까 하는 생각이 들었다.
사실 Lambda와 SQS만으로도 충분할 수 있으나, 별도의 작은 서버로 분리해서 어떠한 방식으로 기존 서버와 통신할 수 있을지에 대해서도 공부해보고 싶었고, 특히 코틀린을 이용해서 작성해보고 싶은 욕심도 있었다.
이를 위해 @dongglee 님과 함께 구조를 설계해보고, 의논하면서 구현해보았다.
SQS, Lambda, Firebase와 같은 인프라 및 외부 서비스에 대한 정보는 iOS 푸시 알림 구현하기 세팅편 - AWS SQS, Lambda, Firebase에 적어놓았다.
기존의 알림 처리 구조
기존에 작성한 구조는 위와 같다.
1.
WAS(서버) 자체에서, 서비스에서 정책에 따라 알림 이벤트를 발행한다. (예를 들면 새 팔로워)
2.
앱 내 알림 저장 && 푸시 알림 구분 및 해당 이벤트 발행한다.
3.
이벤트 핸들링을 통해 SQS(메시지 큐)에 메시지 발행한다.
4.
SQS가 메시지를 받으면, Lambda를 트리거한다.
5.
Lambda에서 해당 메시지를 consume하고 FCM의 인터페이스에 맞게 해당 데이터를 전달한다.
6.
FCM에서 전달받은 데이터를 기반으로 클라이언트 기기를 식별, 푸시 알림 발송한다.
어플리케이션에서 제공하는 알림의 구현사항은 다음과 같다.
•
앱 단(프론트)에서는 DB를 조회(API)하여 앱 내 알림 데이터를 조회할 수 있다.
•
앱 서버에서는 내부적으로 앱 내 알림과 푸시 알림을 구분하여 저장/송신할 수 있어야 한다.
이 구조에서 어떻게 하면 ‘알림과 관련한 모든 로직을 가지는 서버’로 분리할 수 있을지 고민해보았다.
접근 전략
어떤 구조로 구현해볼지 다음과 같이 논의했다.
•
notice(앱 내 알림 엔티티), device_token(푸시 알림을 위한 기기 식별 토큰)은 모두 애플리케이션의 DB에 둔다.
→ 현재 개발 단계에서 굳이 DB를 분리해서 다르게 두는 것보다 어플리케이션의 것을 참조하는 것이 낫다고 생각했다 - 단순히 언제든지 datasource의 변경으로 갈아끼워주면 된다고 생각했다.
•
어플리케이션 서버와는 HTTP 통신을 한다. (gRPC등은 복잡해서 기각)
◦
보안과 관련해서는 JWT Token 같은 것은 사용하지 않기로 하고, 특정 네트워크만 허용하는 방식으로 관리하기로 했다.
◦
앱(프론트) 단에서 조회를 요청하는 경우 어플리케이션에서 알림 서버에 요청, 결과를 포워딩해주는 방식으로 생각했다. (물론 현재 DB 구조 상으로는 불필요한 HTTP 뎁스를 한 번 더 타는 것과 같지만, 분리하는 것을 고려하면 이렇게 작성해야 할 것 같았다)
•
알림 이벤트는 Message Queue를 이용한다.
◦
Message Queue는 SQS로 하기로 했다.
→ RabbitMQ와 같은 별도의 프로그램을 이용, 알림 서버 인스턴스에서 구성하는 것을 고려했으나 이미 이전에 SQS와 관련하여 구현된 부분이 있었고, 부수적인 작업을 덜어내는 것을 고려해서 SQS를 그대로 가져가기로 결정했다.
근본적으로 인터페이스를 정의하고, 메시지 큐를 이용하여 서버 분리를 해보는 것에 의의를 두고 작은 사이드 프로젝트로 구성한 것이어서, 이후의 개발에 차질이 생길 수 있는 러닝커브를 가진 부분들은 우선 제하는 방식으로 구상했다.
구상한 구조
초기 예상안
이렇게 되면, 알림 서버는 두 가지 방식의 통신을 하는데, 하나는 WAS와의 HTTP 통신, SQS와의 통신이다.
•
WAS와의 HTTP 통신은 알림과 관련한 CRUD를 할 때 쓰기.
•
알림 이벤트 처리 - 메시지 큐를 이용한 방식으로 WAS에서 발생하는 알림 이벤트들에 대한 처리를 담당.
시나리오
알림 이벤트 발행
WAS에서 서비스 레이어에서 실행되는 메서드에서, ApplicationEventPublisher를 통해 단순히 정보만 담은 이벤트 객체를 publish하기로 결정했다.
이전의 방식과 피드백
→ 해당 Event에 대해 Listener(핸들러)에서 SqsMessageEvent로 convert한다.
•
receiverId - 해당 알림을 수신하는 member의 id
•
noticeAttribute - 메시지의 메타데이터. 푸시알림, 앱내 알림 여부 등의 정보가 들어간다. 기존에는 SQS 메시지의 attribute를 사용하려 했으나, 여러가지 케이스에 대한 일종의 배열처럼 사용되므로 message의 body에 포함하기로 결정했다.
•
noticeParameters - WAS의 SqsMessageEvent 자체에서는 List<NoticeParameter>로 관리되지만, 메시지로 변경될 때에는 String JSON의 형태로 message body에 포함된다.
// 아래와 같은 방식의 text block은 java 15 이후 및 코틀린에서 지원된다.
message.body = """
{
"receiverId": "123",
"title": "title",
"content": "content",
"attributes" : ["PUSH_NOTICE", "INNER_APP_NOTICE"],
"parameters": [
{"key1": "value1"},
{"key2": "value2"}
],
"createdAt": "2021-01-01T00:00:00Z"
}
""".trimIndent()
Java
복사
앱 내 알림
푸시 알림
•
유저 가입 및 토큰 변경 시에 해당 기기의 device_token을 저장, 변경하는 것은 WAS에서 담당한다.
→ device_token 자체는 member와 연관관계가 있는 데이터여서 어플리케이션이 갖는 DB에 두기로 하였다.