유튜브, 우버, 틱톡, 인스타그램과 같은 서비스들은 어떻게 억 단위의 사용자를 처리할까? 🤔
이러한 서비스들은 처음부터 완벽한 시스템으로 설계되지 않았습니다.
대규모 트래픽을 처리하고 급성장에 대응하기 위해서는 확장 가능한 시스템 설계 원리를 따르는 것이 중요하다.
초창기 출시부터, 아키텍처를 개선하는 시나리오를 통해 어떻게 설계하는지 알아보도록 하자.
아키텍처
MVP 출시하기
먼저, 최소한의 기능만을 갖춘 MVP(Minimum Viable Product)를 출시하는 것이 중요하다.
모든 기능을 먼저 고려하면 오버헤드가 발생할 수 있고, 결국 사용자가 원치않는 기능들을 만들 수 있다는 이유이다.
- API 규격 선정: API 규격을 먼저 논의하여 개발 방향을 일치시킵니다.
- 스크럼 기반 개발: 스크럼 기반으로 스프린트를 진행하며, 각 스프린트 종료 시마다 기능을 반영하고 피드백을 수렴한다.
- CI/CD 구축: CI/CD를 통해 지속적인 통합과 배포를 자동화하여 빠른 개발 주기를 확보한다.
이러한 기반을 갖추면 문제 발생 시 기민하게 대처하고 빠르게 개선할 수 있게된다.
아키텍처
기본적이고 간단한 클라이언트-서버 아키텍처 스타일을 채택하여 첫 번째 기능을 출시한다.
예를 들어, React(FE), Django(BE), PostgreSQL(DB)을 사용하는 구조를 생각해 볼 수 있다.
사용자 증가에 따른 부하 해결 #1
사용자가 증가하면서 CPU 부하 문제가 발생하면 N-Tier 아키텍처로 전환시도를 한다.
웹 서버와 애플리케이션 서버를 분리하여 부하를 분산하고 성능을 향상시킬 수 있다.
수평 확장
기능을 추가하면서 처리량(Throughput) 개념을 고려할 때가 되었다.
처리량은 한정되어있고, 사용자가 많아진다면 지연이 발생할 수 있기 때문이다.
RPS(Request Per Second), TPS(Transaction Per Second)와 같은 지표를 통해 시스템 성능을 측정하고, 수직 확장 또는 수평 확장을 통해 성능을 개선해야한다.
수평 확장(스케일 아웃)은 서버를 탄력적으로 늘리거나 줄일 수 있어 비용 효율적인 확장이 가능하다는 장점으로 수직 확장(스케일 업)보다 많이 채택하는 편이다.
무조건 스케일 아웃이 정답이라고 할 수 없지만, 현재 상황을 고려하여 결정해야 한다.
스케일 아웃만 하면 끝나는 것이 아니다.
사용자의 요청을 분산시켜야만이 스케일 아웃의 장점을 누릴 수 있다. 이때 사용하는 것이 로드 밸런싱이다.
- 로드 밸런싱: Nginx, HAProxy, AWS ELB와 같은 로드 밸런서를 사용하여 트래픽을 분산하고 특정 서버에 부하가 집중되는 것을 방지한다.
- 자체 인프라 환경을 갖추고 있다면, nginx, HAProxy를 이용하고, 클라우드 환경이라면 클라우드 프로바이더가 제공하는 로드밸런스를 사용하는 것을 추천한다.
우리 서비스에서 파일을 업로드하고 저장하는 기능을 제공하고 있다고 가정하자.
파일들이 애플리케이션 서버 디스크에 저장하고 있을 때 스케일 아웃이된다면 파일이 분산되어 저장되는 문제가 발생한다.
스케일 아웃시에는 외부 원격 스토리지에 저장하도록 해야한다.
클라우드 스토리지에 접근할 수 있게 변경하여 서버가 분산되어도 일관된 데이터를 제공할 수 있게해야한다.
- 클라우드 파일 스토리지: AWS S3, Google Cloud Storage와 같은 클라우드 스토리지를 활용하여 파일 저장 용량을 확보하고 서버 간 일관된 데이터 제공을 가능하게 합니다.
클라우드 파일 스토리지를 이용하는 이유는 파일에 대한 관리를 자동으로 해줄뿐만이 아니라, 부하까지 클라우드사에서 처리를 할 수 있어 확장에도 유연한 구조가 된다.
이렇게 실제 파일들은 클라우드 스토리지에 저장하고, 실제 파일의 주소는 DB에 저장하는 방식을 채택한다.
사용자 증가에 따른 부하 해결 #2
사용자가 늘어나 서버 접속이 불가능한 문제 발생한다.
원인을 찾아보니 데이터베이스가 장애를 일으켜 시스템 전체가 마비된 상황이었다.
앞서 스케일 아웃을 통해 애플리케이션 서버는 부하 분산이 되었다.
그러나 데이터베이스는 스케일 아웃하지 않았기에 모든 요청을 받아 처리하는 구조이다.
즉, SPOF(Single Point Of Failure) 문제가 발생한 것이다.
데이터베이스 클러스터링 기술을 도입하여 active/standby 구성으로 데이터베이스를 운영하고, 장애 발생 시 standby 서버를 active 서버로 승격시켜 서비스 연속성을 보장하기로 한다.
DB가 죽는 상황을 해결했다고 했지만, 부하를 분산시키지는 않았기에 데이터베이스에 병목이 발생하여 지연을 발생시키고 있는 상황이 될 수 있다.
이때 데이터베이스 복제(replication)를 통해 읽기 작업을 분산시킬 수 있다.
- 마스터(primary): 쓰기 작업을 처리
- 레플리카(secondary): 마스터로 부터 복제받아 읽기 작업만을 처리하고,
마스터가 죽으면 레플리카 서버 중 하나를 마스터로 승격한다.
단, 레플리카에 복제하는 동기화 지연이 발생할 수 있고, 설정이 복잡하다는 단점이 있다.
관계형 DB의 상황에서 설명한 내용이다. 수평 확장이 쉬운 NoSQL을 선택한다면 달라질 수 있다.
사용자 증가에 따른 부하 해결 #3
레플리카 복제를 늘려도 DB에 요청이 몰리는 문제는 여전하다.
언젠가는 장애가 다시 발생할 수 있으니 DB에 접근하는 요청을 줄이는 방법을 고려해야한다.
캐시를 도입하여 데이터베이스 접근 횟수를 줄이고 응답 속도를 향상시키면 좋다.
캐시는 로컬 캐시와 원격 캐시를 주로 사용하는데
로컬 캐시는 애플리케이션 서버의 내부 메모리 사용하여 저장하는 방식으로 설정이 간단하고 빠르지만, 스케일 아웃되어 서버가 여러대일 때 일관성을 유지하기 위해 캐시 동기화를 별도로 구성해야하는 단점이 존재한다.
반면, Redis, Memcached와 같은 인메모리 원격 캐시를 사용하면 서버 간 캐시 일관성을 유지할 수 있다.
검색 기능과 검색 엔진 도입
데이터베이스에 '빨간 자동차'가 저장되어있다.
사용자가 검색을 'red car'라고 검색하면 단순 문자열을 매칭시키는 로직만으로는 데이터를 찾을 수 없다는 결과를 내뱉게 된다.
검색 기능을 위해서는 RDB의 한계를 극복할 수 있는 검색 엔진을 도입하면 좋다.
Elasticsearch, Meilisearch와 같은 검색 엔진은 텍스트 분석, 자동 완성, 유사도 기반 검색 등 다양한 기능을 제공한다.
이렇게 읽기 역할에 집중된 솔루션을 이용하여 DB에서 발생하는 복잡한 조인, 풀 스캔 문제를 회피할 수 있다.
이렇게 솔루션만 도입한다고 문제가 해결될까?
사용자가 데이터를 변경하면
백엔드 시스템은 데이터베이스에 변경 사항을 저장하고, 검색 엔진에 추가 반영을 해야한다.
이 두 요청을 처리하기 까지 사용자는 대기해야한다.
두 요청을 동기적으로 기다리면 사용자 경험이 떨어질 수 있으니
데이터 변경 요청이 정상 처리되었다는 응답을 먼저 주고, 검색 엔진에 데이터를 반영하는 것은 비동기로 하여 빠른 응답을 주면 좋지 않을까?
비동기 처리와 메시지 큐
데이터베이스와 검색 엔진 간 이기종 시스템의 동기화 방식을 변경해야 한다.
데이터 동기화는 이벤트 기반 아키텍처를 사용하면 이벤트를 전달하고 시스템 간 결합도를 낮춰 확장성을 높이면서 비동기적으로 처리할 수 있다.
이벤트 기반 아키텍처를 구성하기 위해 메시지 큐(RabbitMQ, Apache ActiveMQ, Apache Kafka)를 도입하면된다.
동기식은 간단하지만 의존적이므로 시스템이 변경될 때 여파가 상당하다.
비동기식은 유연하고 확장가능하지만, 복잡한 구성이 필요하다.
현재 상황에 따라 선택하여 구성하면 좋겠다.
이벤트를 메시지큐에만 전달하고 다음 작업을 처리하게한다.
- RabbitMQ: 간단하고 안정적으로 사용할 수 있다.
- Apache ActiveMQ: JMS 기반으로 Java 애플리케이션과 통합시켜 원할하게 이용이 가능하다.
- Apache Kafka: RabbitMQ 보다 높은 처리량, 고성능 확장 중점, 실시간 스트리밍 지원
개인화 추천 기능 추가
개인화 추천 기능을 위해서는 사용자 데이터를 수집하고 분석하여 사용자 프로필을 생성합니다. 머신러닝, 딥러닝 기반의 추천 알고리즘을 사용하여 사용자에게 맞춤형 콘텐츠를 추천할 수 있다.
- 데이터 수집
- 사용자 행동 파악을 위해 '좋아요' 기능 같은 상호작용 데이터를 수집한다.
- 사용자 프로필 생성
- 수집된 데이터를 기반으로 취향 파악, 관심사, 선호도, 행동 패턴등을 생성한다.
- 추천 알고리즘
- 머신러닝, 딥러닝으로 모델을 생성하여 사용자의 행동을 예측한다.
글로벌 확장과 CDN
입소문이 퍼지며 실 사용자가 늘어남에 따라 글로벌로 확장되었다.
초기 인프라는 한국에 맞춤되었기 때문에 해외 사용자들은 지리적 거리로 인해 네트워크 지연시간이 발생할 수 밖에 없는 구조이다.
네트워크 지연은 주로 정적 콘텐츠에서 지연이 발생한다. 이런 데이터는 용량이 크고 빈번하게 요청한다는 특징이 있다.
다음은 사용자 요청에 따라 생성되고 변하는 데이터의 지연이 있다.
글로벌 확장시에는 CDN(Content Delivery Network)을 도입하여 정적 콘텐츠를 여러 지역에 분산 배치해야 한다.
사용자는 지리적으로 가까운 서버에서 콘텐츠를 제공받아 네트워크 지연 시간을 최소화할 수 있게된다.
멀티 데이터 센터를 적용하고 DNS를 통해 가까운 서버로 라우팅시키게한다.
팀 구성 및 역할 분담
플랫폼 엔지니어, 서버 엔지니어, 클라이언트 엔지니어 등 다양한 역할을 가진 엔지니어들이 협력하여 시스템을 구축하고 운영해야한다.
플랫폼 엔지니어들은 개발에 집중할 수 있는 환경을 구축한다.
- SRE: 시스템 안정성과 성능 책임, 자동화 효율, 배포 및 모니터링 업무
- DBA: DB 설계부터 관리 및 보안
- 보안: 시스템 네트워크, 데이터를 외부 공격으로 부터 보호한다. 또는 보안 정책을 수립한다.
- 네트워크: 네트워크 인프라를 설계, 구축, 최적화한다. 주로 보안 업무와 협력한다.
서버 엔지니어
- 백엔드: 클라이언트 애플리케이션에서 필요한 기능을 구현, API 개발, 비즈니스 로직 구현
- Data/ML: 데이터 저장/처리 관리, 파이프라인 구축, 분석, 데이터 웨어하우스 구축 및 모델 학습, 모델 성능 측정
클라이언트 엔지니어
- 프론트엔드: 주로 웹 사이트 사용자 경험을 설계하고 구현, 편리한 웹 경험을 제공, 다양한 기기에서 최적화된 기능 제공
- 모바일: Native 애플리케이션을 개발, 앱 성능/보안을 전반적으로 고려한다.
소프트웨어 아키텍처
느슨한 결합, 관심사 분리
느슨한 결합과 관심사 분리 원칙을 적용하여 시스템의 유연성과 확장성을 높여야한다.
인터페이스를 활용하여 느슨한 결합을 통해 특정 기술에 종속되지 않도록 설계시켜 의존성을 최소화한다.
이렇게 특정 기술에 종속되지 않도록 관심사를 분리하다보면 변화에 유연하게 대응할 수 있게한다.
관련한 아키텍처로는 대표적으로 클린 아키텍처, 헥사고날(포트와 어댑터)이 유명하다.
가령 우리가 저장하는 데이터가 영원히 RDB에 저장되지 않을 수 있다.
언제든지 NoSQL 같은 저장소로 교체할 수 있기에 저장소 인터페이스를 만들고 구현체를 교체하는 전략을 이용하는 것이 좋다.
대표적으로 스프링의 DI를 통해 구현체 주입을 편리하게 사용할 수 있다.
"설계를 올바르게 유지하려면 지속적으로 설계를 개선해야 한다."
프로젝트가 성장함에 따라 시스템 설계도 지속적으로 개선되어야 한다.
결론
억 단위 사용자를 처리하는 시스템은 단순히 MSA나 복잡한 기술 도입만으로 완성되지 않습니다.
단계적인 확장, 적절한 기술 선택, 핵심 설계 원칙 준수, 지속적인 개선을 통해 대규모 트래픽에도 안정적인 서비스를 제공할 수 있다.