
이 블로그 포스트에서는 차량을 추적하는 시스템을 구축하는 방법을 탐구할 것입니다. 특히 학교 버스에 중점을 두고 있습니다. 이 시스템의 설계는 다양한 교육 기관에서 학교 버스를 모니터링할 수 있도록 합니다. 또한, 우리 시스템을 알림 기능으로 향상시킬 것입니다.
이 기능은 버스가 특정 학생의 집 근처에 다가올 때나 학생을 내려 놓을 때 부모님에게 알림을 보냅니다. 이 포스트에서는 이 시스템의 개발과 구현과 관련된 다양한 기술적 주제를 다룰 것입니다.
기능 요구 사항 이해하기
우리는 학교 버스 위치를 추적할 수 있는 시스템을 설계하고자 합니다. 이 시스템에는 여러 학교가 포함되어 있으며, 각 학교에는 여러 학생이 포함되어 있습니다. 다음은 각 학교에 대한 요구 사항입니다.
- 버스 위치의 빈번한 업데이트: 시스템은 높은 빈도로 버스의 현재 위치를 업데이트하고 표시해야 하며, 정확한 추적을 보장해야 합니다.
- 이해관계자들을 위한 시각화: 학부모와 학교 관리자가 접근할 수 있는 실시간으로 버스 위치를 지도상에 표시하는 시각화가 필수적입니다.
- 근접 알림: 버스가 어린이 집에 접근할 때, 시스템은 자동으로 부모에게 알림을 전송해야 합니다. 주요 초점은 Push 알림을 구현하는 데 있지만, SMS와 같은 추가 알림 방법을 지원하기 위해 아키텍처가 확장 가능해야 합니다.
- 학교 간 데이터 격리: 각 학교는 자체 버스 편대에 대한 정보에만 액세스해야 하며, 데이터 개인 정보 보호와 시스템 무결성을 보장합니다.
비기능 요구 사항
비즈니스 목표는 학교 버스 추적기 시스템에 대한 여러 중요한 비기능 요구 사항을 식별할 수 있는 기초를 제공합니다:
- 확장성: 이 시스템은 버스가 위치 업데이트를 전송하는 수와 학부모가 학생의 위치를 동시에 추적하는 수에 대비하여 효율적으로 처리해야 합니다.
- 고가용성: 지속적인 서비스 및 중단 없는 실시간 추적을 보장하기 위해 높은 가용성을 유지해야 합니다.
- 신뢰성: 시스템은 알림이 손실되지 않도록 보장해야 합니다. 특히 버스가 학생의 집에 가까워지고 있음을 나타내는 알림은 반드시 목표 수신자에게 도착해야 합니다.
- 보안: 위치 데이터의 민감한 성격을 감안하여, 시스템은 무단 접근 및 잠재적인 데이터 유출에 대비하기 위해 견고한 보안 조치를 도입해야 합니다.
- 개인정보: 사용자가 직접 관련된 버스의 위치 정보에만 액세스할 수 있도록 엄격한 개인 정보 보호 관리가 시행되어야 합니다. 이를 통해 어떠한 권한 없는 차량의 위치 조회를 방지할 수 있습니다.
대략적인 계산을 통한 이해
제안된 스쿨 버스 추적 시스템의 규모와 수요를 더 잘 이해하기 위해 예상 사용량을 기반으로 초기 계산을 수행해 보겠습니다:
초당 위치 업데이트 요청 (RPS)
- 총 버스 수: 2000개의 학교가 있으며 각 학교당 약 10대의 버스가 있습니다. 따라서 총 20,000대의 버스가 있습니다.
- 업데이트 빈도: 각 버스는 위치 업데이트를 2초마다 보냅니다.
RPS 계산:
- 2초 동안의 총 요청 수: 20,000대의 버스
- RPS = 20,000개의 요청 / 2초 = 10,000 RPS
따라서 매우 높은 데이터 수신율이 발생하며, 이를 효과적으로 처리하기 위해 상당한 백엔드 자원이 필요합니다.
2. 학부모 앱 사용량
- 총 학생 수: 2000개 학교 × 300학생 = 600,000명 학생
- 분당 체크 수 = 180,000명 부모 / 2분 = 분당 90,000회 체크
- 초당 체크 수 = 분당 90,000회 체크 / 60초 = 초당 1,500회 RPS
데이터 읽기 속도가 상당히 높지만, 현대 데이터베이스의 용량 안에 들어가며 주요한 도전 과제로 될 가능성은 적습니다.
고수준 디자인 제안
우리 학교 버스 추적 시스템의 고수준 설계는 몇 가지 중요한 구성 요소를 중심으로 구성됩니다. 각각에 대한 개요는 다음과 같습니다:
- API 디자인
- 학생 거주지 찾기 알고리즘
- 버스 위치 업데이트
- 데이터 모델
API 디자인
우리는 RESTful API를 사용하여 위치 업데이트 및 버스의 보기를 처리할 간단하고 강력한 백엔드를 설계할 것입니다.
/v1/buses/locations
- 인증: JWT를 헤더로 하는 "X-Authorization"을 사용하여 로그인한 사용자를 검증합니다.
- 기능: 이 엔드포인트는 사용자의 자녀에 연관된 버스를 식별하고 현재 위치를 반환합니다.
응답 본문
{
"buses": [
{
"bus_id": "e3d54bd9-bbbc-4d8e-a653-a937c7c4a215",
"driver": {
"id": "a23e80d5-679d-4041-96bd-20d3954099ff",
"name": "Rosella Huel"
},
"children": [
{
"id": "0d95d921-97ba-48a9-8f5d-73b54c4318f5",
"name": "Herminia Rippin"
}
],
"location": {
"latitude": 40.712776,
"longitude": -74.005974
}
}
]
}
WebSocket /v1/buses/'bus_id'/location
- 기능: 각 버스가 WebSocket 연결을 통해 현재 위치를 전송하여 새로운 HTTP 연결을 열 필요 없이 지속적인 데이터 흐름이 가능합니다.
요청 매개변수:
{
"latitude": ...,
"longitude": ...,
"timestamp": ...
}
학생 기숙사 위치를 찾는 알고리즘
우리는 데이터베이스에 학생 기숙사 위치의 포괄적인 목록이 포함되도록 하고자 합니다. 이러한 위치는 각 위치에 버스가 도착하기 약 2분 전에 학부모에게 알릴 수 있도록 구성되어야 합니다.
다음 그림에서 보여지는 두 종류의 지리 공간 색인 접근 방식이 있습니다:
고르게 분할된 그리드
세계를 작고 고르게 간격이 나뉜 그리드로 나누는 간단한 방법이 있습니다. 이 시스템에서 각 그리드는 여러 집을 포괄할 수 있으며, 지도상의 각 집은 한 특정 그리드에 할당됩니다.
그러나 이 방법은 주택 분포가 상당히 다양하다는 주요 문제로 인해 비즈니스 요구에 맞지 않을 수 있습니다. 우리는 추정된 통지 시간을 효과적으로 충족시키기 위해 그리드의 크기를 보다 정밀하게 제어할 수 있는 능력이 필요합니다.
Geohash
Geohash는 균등하게 나누어진 그리드 옵션보다 좋습니다. 이는 이차원 경도 및 위도 데이터를 문자와 숫자로 이루어진 일차원 문자열로 줄이는 방식으로 작동합니다. Geohash 알고리즘은 세계를 추가적인 비트마다 더 작고 더 작은 그리드로 재귀적으로 나누는 방식으로 작동합니다. 고수준에서 Geohash가 어떻게 작동하는지 살펴봅시다.
먼저, 행성을 본초자오선과 적도를 기준으로 네 개의 사분면으로 나눕니다.
- 위도 범위 [-90, 0]은 0으로 표현됩니다.
- 위도 범위 [0, 90]은 1로 표현됩니다.
- 경도 범위 [-180, 0]은 0으로 표현됩니다.
- 경도 범위 [0, 180]은 1로 표현됩니다.
두 번째로, 각 격자를 네 개의 더 작은 격자로 분할합니다. 각 격자는 경도 비트와 위도 비트를 번갈아가며 표현할 수 있습니다.
원하는 정밀도 내에 격자 크기가 되도록 이 분할을 반복합니다. Geohash는 일반적으로 base32 표현을 사용합니다. 두 가지 예시를 살펴봅시다.
Google 본부의 Geohash (길이 = 6)
1001 10110 01001 10000 11011 11010
(2진법의 base32) → 9q9hvu
(base32)
Facebook 본부의 Geohash (길이 = 6):
1001 10110 01001 10001 10000 10111
(2진법의 base32) → 9q9jhr
(base32)
Geohash에는 Table에 표시된 12개의 정밀도(레벨이라고도 함)가 있습니다. 정밀도 요소는 그리드의 크기를 결정합니다. 우리는 길이가 6부터 7인 GeoHash에만 관심이 있습니다. 8보다 길면 그리드 크기가 너무 작고, 7보다 작으면 그리드 크기가 너무 커서 이 경우에는 우리의 통지 접근 방식이 대부분 비효율적일 것입니다.
| Geohash 길이 | 그리드 너비 x 높이 |
|--------------|--------------------------|
| 1 | 5,009.4 km x 4,992.6 km |
| 2 | 1,252.3 km x 624.1 km |
| 3 | 156.5 km x 156 km |
| 4 | 39.1 km x 19.5 km |
| 5 | 4.9 km x 4.9 km |
| 6 | 1.2 km x 609.4 m |
| 7 | 152.9 m x 152.4 m |
| 8 | 38.2 m x 19 m |
| 9 | 4.8 m x 4.8 m |
| 10 | 1.2 m x 59.5 cm |
| 11 | 14.9 cm x 14.9 cm |
| 12 | 3.7 cm x 1.9 cm |
적절한 정밀도를 어떻게 선택할까요? 사용자가 정의한 반지름으로 그려진 원을 완전히 포함하는 최소한의 Geohash 길이를 찾고 싶습니다. 반지름과 Geohash의 길이 사이의 해당 관계는 아래 표에 나와 있습니다.
| 반지름 (킬로미터) | Geohash 길이 |
| - - - - - - - - - - - -| - - - - - - - - |
| 2 km (1.24 mile) | 5 |
| 1 km (0.62 mile) | 5 |
| 0.5 km (0.31 mile) | 6 |
| 0.086 km (0.05 mile) | 7 |
| 0.015 km (0.009 mile) | 8 |
GeoHash가 어떻게 작동하는지 자세히 알고 싶다면, 이 사이트를 이용해보세요.
const geohash = require('ngeohash');
// 위치를 나타내는 예시 좌표
const latitude = 40.6892; // 자유의 여신상의 위도
const longitude = -74.0445; // 자유의 여신상의 경도
// GeoHash 생성
const geohashCode = geohash.encode(latitude, longitude, 7);
console.log("GeoHash code:", geohashCode);
현재 설정에 대해 매우 효과적인 접근 방식입니다. 하지만 경계 정의와 관련된 소규모 문제가 있습니다. 그러나 이러한 문제는 기존 프레임워크에는 영향을 미치지 않을 것입니다.
Quadtree
쿼드 트리는 주로 두 차원 공간을 구획하기 위해 사용되는 데이터 구조입니다. 이 구조는 해당 공간을 네 개의 사분면(격자)으로 재귀적으로 나누는 방식으로 동작하며, 각각의 격자에 특정 기준을 충족할 때까지 계속해서 나누게 됩니다. 예를 들어, 이 기준은 해당 격자 내의 가게 수가 100개를 초과하지 않도록 계속해서 나누는 것일 수 있습니다. 이 숫자는 임의로 설정되며, 실제 숫자는 비즈니스 요구에 따라(한 격자 안의 주택 수) 결정될 수 있습니다.
쿼드 트리를 사용하면 쿼리에 응답하기 위한 메모리 내 트리 구조를 구축할 수 있습니다. 쿼드 트리는 메모리 내 데이터 구조로, 데이터베이스 솔루션이 아닙니다. 각 서버에서 작동하며, 데이터 구조는 서버가 시작될 때 만들어집니다.
다음 그림은 세계를 쿼드 트리로 나누는 개념적인 과정을 시각화한 것입니다. 세계에 2억(200백만) 가구가 있다고 가정해봅시다. (이것은 사용법을 설명하기 위한 임의의 숫자입니다)
루트 노드는 전체 세계지도를 나타냅니다. 루트 노드는 재귀적으로 4개의 사분면으로 나뉘어지고 더 이상 100개 이상의 기업이 있는 노드가 남아있지 않을 때까지 계속해서 분할됩니다.
Quadtree를 사용하여 인근 기업을 얻는 방법은 다음과 같습니다.
- 메모리에 Quadtree를 생성합니다.
- Quadtree가 생성된 후 루트부터 탐색을 시작하여 트리를 순회하고, 검색 원점이 있는 리프 노드를 찾을 때까지 진행합니다. 만약 해당 리프 노드에 기업이 100개 있다면, 해당 노드를 반환합니다. 그렇지 않으면 충분한 수의 기업이 반환될 때까지 이웃 노드에서 기업을 추가합니다.
실제로 사용되는 쿼드 트리 예시
Yext는 덴버 근처에 구성된 쿼드 트리를 보여주는 이미지를 제공했습니다. 밀집 지역에는 더 작고 세분화된 격자를, 희소 지역에는 더 큰 격자를 원합니다.
쿼드 트리에 대한 운영상의 고려 사항을 고려하지 않았을 경우, 새로운 집을 추가하는 작업이 맵에 업데이트되는 데 많은 시간이 걸리며, 맵에서 사용한 격자의 크기를 제어하지 못하므로 집의 길이에 따라 나뉘는 문제가 발생할 수 있습니다.
구글 S2
구글 S2 지오메트리 라이브러리는 이 분야에서 큰 역할을 하는 또 다른 주요 라이브러리입니다. Quadtree와 유사하게, 이는 메모리 솔루션입니다. Hilbert 곡선 (공간을 채우는 곡선)을 기반으로 구 형태를 1차원 인덱스로 매핑합니다. Hilbert 곡선에는 매우 중요한 특성이 있습니다: Hilbert 곡선 상에서 서로 가까운 두 지점은 1차원 공간에서 가깝습니다. 1차원 공간에서의 탐색은 2차원 공간에서의 탐색보다 훨씬 효율적입니다. 관심 있는 독자들은 Hilbert 곡선의 온라인 도구를 활용해 볼 수 있습니다.
S2는 복잡한 라이브러리이지만 구글, Tinder 등 회사들에서 널리 사용되고 있습니다.
- S2는 임의의 영역을 다양한 수준으로 커버할 수 있어 지오펜싱에 최적입니다. 지오펜싱을 통해 우리는 관심 영역을 둘러싼 경계를 정의하고 해당 영역에 있는 사용자들에게 알림을 보낼 수 있습니다.

- S2의 또 다른 장점은 기존의 GeoHash처럼 고정 레벨(정밀도)을 갖는 대신, S2에서는 최소 레벨, 최대 레벨 및 최대 셀을 지정할 수 있는 Region Cover 알고리즘이 있습니다. S2로 반환된 결과는 셀 크기가 유연하기 때문에 더 상세합니다. 더 알고 싶다면 S2 도구를 확인해보세요.
권고사항
마지막으로, 저희 애플리케이션에 적합한 기능을 제공하는 GeoHash와 Google S2를 사용하는 장단점을 고려하는 것이 좋습니다.
그러나 하나는 데이터베이스 솔루션이고, 다른 하나는 인메모리 솔루션으로 작동하는 것에 주목해야 합니다. 단순함을 유지하고 이 기사의 범위 내에서 GeoHash 솔루션을 진행할 것입니다.
버스 위치 업데이트
각 버스는 약 두 초마다 위치 업데이트를 전송합니다. 위치 업데이트의 쓰기 중심적 성격을 감안할 때, 데이터베이스로 직접 쓰기가 실행 가능하지만 최적이 아닐 수 있습니다. 대안적인 방법은 최신 위치를 Redis 캐시에 기록하면서 데이터베이스에 버스 위치의 히스토리를 유지하는 것입니다. 이 방법은 쓰기 작업의 확장성을 향상시킵니다.
게다가 부모 앱이 이 업데이트를 받는 방법을 고려하는 것이 중요합니다. 데이터베이스를 반복적으로 쿼리하여 버스의 위치를 새로 고치는 대신, Redis Pub/Sub를 활용하여 실시간 알림을 제공할 수 있습니다.
Redis Pub/Sub 이해하기:
Redis Pub/Sub은 효율적이고 가벼운 메시지 버스로 작동합니다. 채널은 주제로도 알려져 있으며 비용 효율적이고 쉽게 생성할 수 있습니다. 메모리 수 기가바이트로 구성된 현대적인 Redis 서버는 수백만 개의 채널을 지원할 수 있습니다.
우리의 설계에서는 WebSocket 서버를 통해 수신된 위치 업데이트가 Redis Pub/Sub 서버의 특정 버스 채널로 게시됩니다. 그런 다음 새로운 업데이트는 해당 버스의 모든 구독자에게 즉시 다시 발행되어 맵 위의 실시간 위치 업데이트를 보장합니다.
위치 업데이트 전달
각 버스에서 높은 빈도로 업데이트가 오기 때문에 RESTful API를 사용하여 이러한 업데이트를 받는 것은 비효율적일 수 있습니다. 보다 효과적인 해결책은 웹소켓을 사용하는 것입니다. 웹소켓은 지속적인 연결을 유지하며 몇 초마다 전체 TCP 핸드셰이크를 요구하지 않고 지속적인 데이터 전송을 허용합니다.
데이터 모델
이 섹션에서는 애플리케이션의 읽기/쓰기 비율 및 스키마 설계에 대해 논의합니다.
Read/write 비율
시스템은 많은 버스가 매 두 초마다 업데이트를 보내기 때문에 높은 쓰기 볼륨을 나타냅니다. 따라서 학부모가 학생들의 위치를 추적하는 쿼리 수보다 쓰기 횟수가 현저히 많습니다. 이러한 쓰기 중심의 특성을 고려할 때 NoSQL 데이터베이스가 선호됩니다. 본 블로그에서는 DynamoDB를 활용할 것입니다.
데이터베이스로서의 DynamoDB
DynamoDB는 대규모 쓰기 및 읽기를 관리하기 위해 설계된 확장 가능하고 고가용성 있는 데이터베이스입니다. 신중한 스키마 설계가 필요하지만, 우리의 유즈 케이스에 유용한 강력한 기능을 제공합니다.
우리는 세 가지 주요 테이블을 만들 것을 제안합니다:
- 부모 테이블
- 학생 테이블
- 버스 위치 이력 테이블
DynamoDB에서 테이블은 기본 키 (PK)와 선택적인 정렬 키 (SK)로 구성됩니다. PK와 SK의 고유한 조합 (또는 SK를 사용하지 않는 경우 PK만)은 중복 레코드를 방지하기 위해 필수적입니다. 쿼리는 일반적으로 이러한 키로 제한됩니다. 다른 속성을 사용하는 쿼리의 경우, 로컬 보조 인덱스 (LSI) 또는 글로벌 보조 인덱스 (GSI)를 사용할 수 있습니다.
이전 논의를 고려하면 지오해시(GeoHash)가 위치 쿼리에 사용될 것입니다. 데이터베이스가 우리의 운영 요구 사항에 최적화되도록 스키마 작성 전에 쿼리를 정확하게 정의하는 것이 중요합니다.
부모 테이블
이 테이블은 학부모의 자녀 위치를 검색할 때 쿼리 양을 최소화하기 위해 student_ids를 정규화합니다. 이 스키마는 각 부모와 관련된 버스 ID를 캐싱하여 성능을 향상시킬 수 있습니다.
- Key: parent_id
- Value: bus_ids
학생 테이블
학생이 속한 지리적 블록을 결정하는 GeoHash 필드가 포함되어 있으며, 또한 집이 위치한 위도 및 경도 필드가 있어 도착 예상 거리를 계산하는 데 사용됩니다. 또한, Geohash를 파티션 키 (PK)로, bus_id를 정렬 키 (SK)로 하는 전역 보조 인덱스 (GSI)를 설정하는 것이 극히 중요합니다. 이 GSI는 특정 지리적 영역을 대상으로하는 쿼리를 용이하게 하고, 특정 버스에 대한 부모에게 버스 도착 알림을 제공하는 데 사용됩니다. 이에 대해 "버스 위치 기록 저장" 섹션에서 자세히 살펴볼 것입니다.
버스 위치 이력 테이블
지도 상에서 버스의 위치를 정확히 표시하기 위해 위도와 경도 속성을 분리하여 부모님들의 요구 사항을 충족시킵니다.
시스템 디자인 (버스 흐름)
시스템의 각 구성 요소를 검토한 후, 이제 애플리케이션의 초기 다이어그램을 개발할 준비가 되었습니다. 시스템 아키텍처를 설계할 때 고려해야 할 주요 사항은 다음과 같습니다:
- 버스 알림: 버스는 우리 서버와의 WebSocket 연결을 통해 매 두 초마다 위치 업데이트를 전송할 것입니다.
- 데이터 처리: 우리 서버는 이러한 데이터 업데이트를 durable storage 플랫폼에 푸시할 것입니다(직접 데이터베이스에는 바로 저장하지 않음).
- 위치 캐싱: 가장 최근의 버스 위치는 Redis를 사용하여 캐싱됩니다.
- 실시간 발행: 업데이트된 위치는 버스 채널에 구독한 부모들에게 발행됩니다.
- 데이터베이스 업데이트: 마지막으로, 위치 데이터가 데이터베이스에 저장됩니다.
- Geo-위치인식: 버스가 새로운 GeoHash 위치에 들어가면 해당 지역에 거주하는 부모들에게 알림이 전송됩니다.
버스 위치 보고서
- WebSocket 게이트웨이: 우리는 AWS 관리형 WebSocket 게이트웨이를 사용하여 WebSocket 연결을 설정하고 매 두 초마다 위치 업데이트를 전송합니다.
- API 게이트웨이 및 람다: API 게이트웨이는 람다 함수를 호출하며, 각 메시지 후 데이터를 Kafka 또는 Kinesis와 같은 데이터 스트림 플랫폼으로 전달합니다.
- 데이터 내구성: 데이터는 처리될 때까지 스트림에 남아 있습니다.
앞서 언급한 아키텍처는 우리의 초기 목표를 효과적으로 해결했어요. 이제 우리는 설계에서 제시된 세 번째와 네 번째 포인트에 집중할 차례입니다: 현재 위치를 관리하고 부모들에게 알림을 제공하는 것입니다.
버스 위치 업데이트 및 사용자 알림
- Redis로 데이터 스트림: Kafka/Kinesis에서 온 데이터는 worker에 의해 처리된 후 Redis 인스턴스에 저장됩니다. 이 과정은 데이터에 대해 TTL(생존 기간)을 설정하는 것을 포함합니다.
- Redis Pub/Sub: 성공적인 캐싱 후, 새로운 위치는 해당 특정 버스에 대한 Redis Pub/Sub 채널(e.g., bus#uuid)에 게시됩니다. 이를 통해 연결된 모든 클라이언트에게 실시간 알림이 가능해집니다.
- 비동기 처리를 위한 큐: 게시한 후, 데이터는 비동기 처리 및 데이터베이스 저장을 위해 SQS로 대기열에 들어가게 됩니다.
지금까지 우리는 버스로부터 위치 업데이트를 성공적으로 받아서 현재 위치와 실시간 업데이트를 Redis Cache에 저장하는 데 성공했습니다. 이러한 업데이트는 그 후에 부모 앱으로 매끈하게 게시되어 모든 사용자가 액세스할 수 있습니다.
버스 위치 기록 저장
- SQS로부터 데이터 처리: 람다 워커를 통해 대기열에서 가져온 데이터는 버스 ID와 위도 경도 좌표와 함께 데이터베이스에 저장됩니다.
- 조건부 알림: 알림 효율성을 최적화하기 위해, 최종 관찰 위치의 GeoHash가 변경되었는지 확인하는 검사를 수행합니다. 그렇다면, 새로운 GeoHash 위치에 연결된 부모가 식별되고, 알림이 SQS로 전송됩니다.
알림 플로우 디자인
- 데이터 검색: 알림이 필요할 때 시스템은 먼저 캐시에서 고객 정보를 가져오려 시도합니다. 정보를 찾을 수 없는 경우 데이터베이스에 액세스합니다.
- 팬-아웃을 위한 SNS: SNS를 사용하여 사용자 선호도에 따라 알림을 배포합니다.
- 종단 처리: 메시지는 다른 SQS에 대기열에 들어가고 Lambda 함수에 의해 처리되어 최종 알림이 최종 사용자에게 전달됩니다.
이 시스템 디자인은 위치 기반 알림을 위한 실시간 업데이트 전파와 효과적인 데이터 관리를 보장합니다. 각 구성 요소 간 상호 작용을 완전히 이해하려면 종단 간 아키텍처 다이어그램을 주의 깊게 검토해 주세요.
시스템 디자인 (부모 앱 흐름)
부모 앱의 아키텍처는 버스 흐름과 비교하여 더 간단하며 하나의 다이어그램으로 설명할 수 있습니다.
앱을 열면 다음의 3가지 주요 작업이 수행됩니다:
- 초기 WebSocket 연결: 앱은 WebSocket Gateway와 초기 연결을 설정합니다.
- 현재 버스 ID 조회: Redis Cache에서 현재 버스 ID를 조회하여 최신 위치 정보를 가져와 지도에 표시합니다.
- 버스 채널 수신 대기: 검색된 버스 ID를 사용하여 앱은 ECS 측의 해당 채널을 구독합니다. 이를 통해 새로운 위치 업데이트가 Redis Pub/Sub을 통해 수신되고 WebSocket Gateway를 통해 사용자의 핸드폰으로 전달되도록합니다.
연결 처리를 위한 서버 측 로직
새로운 연결이 설정될 때 ECS의 동작을 이해하는 것이 중요합니다. 이는 부모 앱 아키텍처의 기초를 형성합니다.
사용자가 웹소켓 게이트웨이에 연결하면 ECS가 트리거되고 지정된 함수를 호출합니다. 이 함수는 DynamoDB에서 학생 ID 목록을 검색하고 현재 ECS 인스턴스가 이들 채널을 청취할 수 있도록 합니다.
다음은 이 프로세스를 보여주는 샘플 코드 스니펫입니다:
const redis = require('redis');
// Redis 클라이언트 생성
const subscriber = redis.createClient({
url: 'redis://localhost:6379'
});
subscriber.on('error', (err) => {
console.log('Redis Client Error', err);
});
subscriber.connect();
// 새 채널을 구독하는 함수
function subscribeToChannel(channelName) {
subscriber.subscribe(channelName, (message, channel) => {
console.log(`채널 ${channel}에서 메시지 수신: ${message}`);
});
console.log(`${channelName} 구독 완료`);
}
// 처음에는 기본 채널을 구독합니다.
subscribeToChannel('initialChannel');
// 어떤 이벤트나 조건에 기반해 동적으로 새 채널을 구독하는 예시
setTimeout(() => {
// 10초 후에 새 채널 구독 트리거 시뮬레이션
subscribeToChannel('bus#uuid1');
}, 10000);
구독된 채널의 모든 업데이트는 즉각적으로 ECS 인스턴스에 반영됩니다. AWS SDK를 사용하여 ECS는 웹소켓 게이트웨이를 통해 모바일 애플리케이션에 실시간 업데이트를 게시합니다.
성능을 최적화하기 위해, 우리는 각 ECS 인스턴스에 대해 구독된 채널을 위한 인-메모리 HashMap을 유지보수합니다. 이는 채널 구독의 효율적인 관리를 가능하게 하며, 중복된 구독을 제거합니다. 게다가, 연결된 사용자들의 WebSocket 게이트웨이 ID와 그에 해당하는 채널 ID를 메모리에 저장합니다. 이 접근 방식은 알림 프로세스를 간소화하여 빈번한 데이터베이스 쿼리가 필요하지 않도록 합니다.
게다가, 클라이언트 앱은 연결이 끊어진 경우 자동으로 WebSocket 게이트웨이에 다시 연결하는 로직을 통합하여, 서버와 모바일 애플리케이션 간의 끊김없는 통신을 보장합니다.
확장성 깊이 탐색
이제 각 애플리케이션 구성 요소에 대한 설계를 완료했으므로, 독립적인 확장성 전략을 탐색해 보겠습니다. AWS에 의해 관리되는 일부 구성 요소의 확장성 기능이 기본 제공된다는 점을 고려하는 것이 중요합니다.
WebSocket 서버
WebSocket 서버는 API Gateway WebSocket과 같은 관리형 서비스를 통해 사용량에 따라 자동으로 스케일링될 수 있습니다. AWS가 스케일링을 자동으로 처리하기 때문에 쉽게 관리할 수 있습니다. ECS 및 Lambda는 WebSocket 메시지에 대한 특정 로직을 실행하는 것을 담당하며, 쉽게 확장될 수 있습니다. 람다 함수는 동시성 설정을 구성하여 확장할 수 있으며, ECS 인스턴스는 CPU 및 메모리 사용량에 따라 확장할 수 있습니다.
DynamoDB
DynamoDB는 높은 확장성과 가용성을 제공하며, 온디맨드 및 예약 용량과 같은 두 가지 스케일링 옵션을 제공합니다. 온디맨드는 사용량에 따라 자동으로 스케일링되지만, 예약 용량은 예측 가능한 성능과 비용을 제공합니다. 우리의 사용 패턴을 고려할 때 최적의 성능을 보장하기 위해 모니터링이 중요합니다.
캐시 및 Pub/Sub (Redis)
AWS ElastiCache는 자동 스케일링 기능을 갖춘 호스팅된 Redis 서비스를 제공합니다. ElasticCache Serverless는 리소스 이용률을 지속적으로 모니터링하여 필요에 따라 스케일 아웃하고 새로운 샤드를 추가하며 데이터를 중단 없이 다시 분산하는 자동 워크로드 트래픽 수용 기능을 제공합니다.
데이터 스트리밍 (Kafka/Kinesis)
Kafka와 Kinesis는 둘 다 AWS에서 제공하는 관리형 서비스로, 적절한 구성으로 확장성을 제공합니다. 선택한 플랫폼에 관계없이 AWS가 확장성을 관리하여 운영 상의 걱정을 덜어 줍니다.
SQS (Simple Queue Service)
AWS는 FIFO 및 순서 없는 큐 두 가지 버전의 SQS를 제공합니다. 각각이 자체 할당량과 제한이 있습니다.
FIFO 큐: FIFO 큐는 API 동작(메시지 보내기, 메시지 받기 및 메시지 삭제) 당 초당 300개의 트랜잭션 할당량을 지원합니다. 배치 처리를 사용하면 FIFO 큐는 API 동작 당 초당 최대 3,000개의 메시지를 처리할 수 있으며, 300개의 API 호출이 있고, 각 호출에는 10개의 메시지 일괄 처리가 포함됩니다.
우리의 대략적인 추정을 고려하면, 예상 쓰기 속도는 초당 10,000개의 요청(RPS)이며, FIFO 큐를 사용하면서 일괄 처리를 하더라도 최적이 아닐 수 있습니다. 이러한 경우, 몇 가지 대안이 있습니다:
- 정렬되지 않은 큐: 정렬되지 않은 큐를 활용하여 응용 프로그램 논리에서 잠재적인 중복을 관리합니다.
- 데이터 스트리밍 플랫폼: SQS를 완전히 Kafka 또는 Kinesis와 같은 데이터 스트리밍 플랫폼으로 대체하여 특히 알림 부분에 사용합니다. 알림 프로세스에는 SNS와 함께 큐만 사용합니다.
- 큐의 과도한 할당: 큐를 과도하게 할당하고 일관된 해싱 로직을 구현하여 이러한 큐 사이에서 부하를 분산합니다. 이 접근 방식은 더 높은 쓰기 속도를 효율적으로 처리할 수 있도록 돕습니다.
Redis pub/sub 대체 알고리즘
Erlang은 라우팅 작업의 Redis pub/sub에 대한 유효한 대안으로 제시되며 특히 분산 및 동시성이 높은 응용 프로그램에서 적합하다고 칭찬받고 있습니다. 이 권고는 Erlang의 생태계인 Erlang 또는 Elixir 프로그래밍 언어, BEAM 가상 머신 및 OTP 런타임 라이브러리를 포함한다는 것에서 나왔습니다. Erlang의 이점은 경제적으로 생성하고 관리할 수 있는 가벼운 프로세스를 효율적으로 처리하여 최소한의 리소스 사용으로 단일 서버에서 수백만 개의 프로세스가 동시에 실행될 수 있다는 점에 있습니다.
실제 응용 프로그램에서 Erlang을 사용하면 Redis pub/sub 설정을 각 사용자를 개별 프로세스로 표현하는 분산된 Erlang 시스템으로 대체할 수 있습니다. 이러한 프로세스는 실시간 업데이트(예: 위치 변경)를 효과적으로 관리하고 사용자의 친구들 사이에 업데이트 네트워크를 용이하게 하는 데 유용할 수 있어 Erlang은 확장 가능하고 실시간 데이터 분배가 필요한 시스템에 강력한 후보가 됩니다. 전문적인 Erlang 프로그래밍 기술이 있는 경우를 가정합니다.
마무리
이 포괄적인 블로그에서는 시스템 아키텍처의 다양한 측면에 대해 탐구했습니다. 캐싱, 발행/구독, 그리고 대량 쓰기 처리에 중점을 두었습니다. 세부적으로 두 가지 이미지 아키텍처 흐름을 탐색하고, 각 구성 요소의 기능과 상호 작용에 대한 통찰을 제공했습니다.
게다가, 확장 가능성 전략에 대해 철저히 검토했으며 각 구성 요소를 독립적으로 확장할 수 있는 능력을 강조했습니다. 관리되는 서비스를 활용하고 모베스트 프랙티스를 구현함으로써, 우리 시스템은 높은 확장 가능성과 가용성을 갖추도록 설계되었습니다. 변화하는 요구사항을 수용하고 원활한 성능을 보장할 수 있습니다.
더 많은 콘텐츠를 원하신다면 팔로우해주세요!
지금까지 오셨다면 이와 유사한 콘텐츠를 더 받고 싶으시다면 제 Medium 및 Linkedin 팔로우 부탁드립니다.