프로젝트 : 개요 (22.01.15 - 22.05.24 4개월)
AWS의 Elastic Beanstalk를 사용하여 배포하고 있습니다. Github Actions를 활용하여 CI/CD를 진행 중입니다. AWS RDS(PostgreSQL)를 RDBMS로 사용하고 있고, AWS ElastiCache(Redis)를 활용해서 캐시 데이터를 사용하고 있습니다.
서버를 앱 서버, 푸시 서버 두 개로 운영하여, 하나의 서버가 장애가 나더라도, 다른 서버에 장애가 전파되지 않도록 구성했습니다. 푸시 서버의 경우 Redis 기반 큐인 Bull 큐를 활용하여 동기 처리 방식으로 진행되던 푸시 알림을 비동기 처리하도록 구성했습니다.
배포 담당과 비즈니스 로직 담당으로 나누어 프로젝트를 진행했습니다. 저는 비즈니스 로직을 담당했고, 아래 작성된 프로젝트 진행을 홀로 진행했습니다.
----
프로젝트 : FCM Topic 활용 푸시 전송 로직 설계(22.03.02 - 22.05.06 2개월)
문제
POG 서비스는 소환사를 즐겨찾기 하여 전적 변화 여부를 파악할 수 있습니다. 만약 즐겨찾기한 소환사의 전적이 변동됐다면, 소환사를 즐겨찾기 한 모든 사용자의 FCM 토큰을 조회해서 FCM 서버에 푸시 요청을 해야 했습니다. 1분마다 전적이 변동된 소환사를 즐겨찾기한 모든 사용자의 FCM 토큰을 조회하는 과정에서 DB에 부하를 발생시켰습니다.
진행 과정
결과
사용자의 FCM 토큰을 조회하는 로직을 제거하여 DB에 부하를 줄 수 있는 로직을 제거했습니다.
----
프로젝트 : Redis Queue 활용 푸시 알림 설계(22.03.02 - 22.05.06 2개월)
문제
전적이 변동된 소환사가 1만 명이라고 생각했을 때, FCM Topic을 이용해서 푸시를 보내더라도, FCM 서버로 1만 개의 푸시를 전송해야 했습니다. 그 과정에서 푸시가 동기적으로 전송됐고 푸시를 전송할 때 까지 서버가 멈춰있는 문제가 발생했습니다. 또한 서버가 멈춰있는 동안, 서버에 처리해야 할 요청이 많아질 경우 서버는 견디지 못하고 장애를 발생시켰습니다. 그 과정에서 스레드에 쌓여 있던 푸시 메시지 작업이 모두 사라지는 문제가 발생했습니다.
진행 과정
1. 동기적으로 동작하는 로직을
비동기적으로 처리하기 위해 Queue를 활용했습니다. FCM 서버에 보내야 할 푸시 정보를 Queue에 전달해서 처리함으로써 Node.js의 메인 스레드가 계속해서 멈춰있지 않도록 설계했습니다.
3. RabbitMQ를 활용하려 했으나, 제대로 활용하려면 관리 비용이 든다는 것을 파악했습니다. AWS의 SQS의 경우도 서비스 초기에는 비용이 들지 않더라도 사용자가 조금만 많아지더라도 큰 비용이 들 수 있다고 판단했습니다.
결과
동기적으로 동작하던 푸시 전송을 비동기적으로 처리함으로써 싱글 스레드인 Node.js에 부담을 줄일 수 있었습니다. 또한 queue에 푸시 알림 데이터를 저장하기에, 푸시를 보내는 과정에서 갑작스럽게 서버에 장애가 발생하더라도 다시 정상적으로 푸시 알림을 전송할 수 있습니다.
----
프로젝트 : Redis 캐싱 전략 설계(22.03.02 - 22.05.06 2개월)
문제
1. POG 서비스는 1분 마다 사용자가 즐겨찾기 한 소환사 중, 전적이 변경된 소환사를 파악해서, 전적이 변경된 소환사를 즐겨찾기한 사용자에게 푸시알림을 보냅니다. 이때 전적이 변경된 소환사를 파악하기 위해서는 DB에 저장된 소환사를 전부 조회해야 했습니다. 전체 소환사 정보를 조회하는 과정에서 DB에 부하가 갈 수 있다고 생각했습니다.
2. 소환사의 전적이 변경됐는지 파악하기 위해 특정 소환사의 정보를 받아오는 Riot API를 호출했습니다. Riot API를 통해 받은 소환사 정보와 데이터베이스에 저장된 소환사 정보를 비교했습니다. 만약 비교 값이 다르다면, 데이터베이스에 최신 전적을 저장했습니다. DB에 수정 쿼리를 요청하는 과정에서 부하가 갈 수 있다고 생각했습니다.
진행 과정
2. 현재 Redis에 저장된 소환사의 전적과 Riot이 제공하는 소환사 전적 정보를 비교해서 소환사의 전적이 갱신될 때마다 Redis의 데이터를 수정하도록 설계했습니다.
3. 만약 Redis에 장애가 발생해서 데이터가 모두 휘발됐을 때를 대비하여, Redis에 소환사의 전적을 다시 저장할 수 있는 롤백 기능을 개발했습니다.
결과
Redis의 Set 자료구조를 활용하여 모든 소환사 Id를 조회함으로써 DB에 부하를 줄일 수 있었습니다. 또한 Redis를 활용하여 소환사 전적 정보를 수정함으로써 DB에 부하를 줄일 수 있었습니다.
----
프로젝트 : 테스트 코드 설계(22.03.02 - 22.05.06 2개월)
문제
테스트 코드를 작성하지 않아서, 코드 한 줄을 바꾸더라도 코드의 변경이 어떤 문제를 일으킬지 알 수 없었습니다. 이를 통해 리팩터링의 어려움을 겪었습니다.
진행 과정
2. Github Action을 활용하여 PR시 E2E, 통합, 단위 테스트 코드를 작동시켜 코드에 문제가 생긴다면 문제를 빠르게 해결할 수 있도록 설계했습니다.
결과
테스트 코드를 작성하여 코드의 품질과 서버의 안정성을 올릴 수 있었습니다.
----
프로젝트 : DB 설계 및 쿼리 최적화(22.01.15 - 22.03.02 2개월)
문제
서비스를 개발하면서, 저는 데이터베이스를 제대로 활용하는 방법을 몰랐습니다. 만약 데이터베이스 관리를 제대로 하지 않는다면 사용자에게 좋지 못한 사용자 경험을 제공할 수 있다고 생각했습니다.
진행 과정
1. 어떤 데이터베이스를 활용하는 것이 서비스를 발전시키는 데 도움이 될 수 있을지 알기 위해
MySQL 과 PostgreSQL를 비교했습니다. 단순 CRUD 쿼리는 MySQL을 활용하는 것이 성능이 좋았지만, 복잡한 쿼리를 진행할 때는 PostgreSQL의 성능이 좋은 것을 파악했습니다. 서비스의 특성상 푸시 서비스를 개발할 때 대규모의 조회 쿼리를 빠르게 처리해야 했기에, PostgreSQL 을 활용하는 것이 더 효과적일 것으로 판단했습니다.
2. RDS PostgreSQL 9.6 버전을 활용하면서, 커버링 인덱스가 적용이 안된다는 것을 파악하여
PostgreSQL의 버전 별로 어떤 기능을 지원하는 지 분석했습니다. 그 결과 PostgreSQL의 11버전 이상부터 안전하게 인덱스 적용을 할 수 있다고 판단하여 AWS RDS의 PostgreSQL 버전을 올려서 활용했습니다.
8. 유휴 커넥션 수가 부족해서 생길 수 있는 문제를 대비하기 위해
커넥션 풀을 조정하는 방법을 알아둠으로써 장애 대비를 위해 노력했습니다.
결과
인덱스를 활용하여 1,000만 건의 데이터를 기준으로 657ms 걸리던 조회 쿼리를 0.023ms의 속도로 조회할 수 있도록 쿼리 성능을 개선했습니다.
----
프로젝트 : ExceptionFilter 및 Sentry를 활용한 에러 핸들링(22.01.15 - 22.03.02 2개월)
문제
API에서 에러가 발생할 때마다, 에러 핸들링 하는 코드를 컨트롤러에 모두 작성했습니다. 그러다 보니 에러 핸들링을 할 때, 모든 경우의 수를 코드로 작성해야 하는 불편함이 있었습니다.
진행 과정
결과
실패 처리를 상세하게 코드로 작성하지 않아도 에러 핸들러가 처리함으로써 불필요한 코드 작성을 피할 수 있었습니다.
----
프로젝트 : Swagger 활용 API 문서화(22.01.15 - 22.03.02 2개월)
문제
기존 프로젝트를 담당하던 개발자가 Postman을 활용하면서, API 명세서를 제대로 작성하지 않아 기존 API를 살펴보는 데 어려움이 있었습니다. 또한 개발자가 증가하면서, Postman을 함께 활용할 때 비용이 발생하는 문제가 있었습니다.
진행 과정
결과
Swagger 문서를 활용함으로써 API 명세서를 제대로 작성할 수 있었고, 비용 문제 또한 발생하지 않았습니다.
----
프로젝트 : 프로젝트 기본 셋팅(22.01.15 - 22.03.02 2개월)
문제
기존 프로젝트를 담당하던 개발자의 코드를 분석하면서, 계층 간의 역할과 책임이 제대로 분리되지 않았습니다. 이 때문에 하나의 로직을 추가하거나 수정하더라도 광범위한 코드를 수정해야 했고, 코드의 응집도 또한 떨어지는 문제가 발생했습니다.
진행 과정
결과
계층별로 역할과 책임이 분리되게끔 코드를 작성함으로써 코드의 응집도를 높였고, 의존성을 줄일 수 있었습니다.