클라우드 인프라

트래픽 폭증에 대비하는 데이터베이스 확장, 직접 해보니 이 방법이 통했습니다

강코의 코딩 일기 2026. 7. 19. 19:01
반응형

예상치 못한 트래픽 급증으로 서비스가 마비될 뻔한 경험, 관리형 데이터베이스의 읽기 복제본, 샤딩, 연결 풀 최적화를 통해 위기를 극복한 실전 노하우를 공유합니다.

안녕하세요, 주니어 개발자 여러분! 저도 여러분과 같은 길을 걷고 있는 한 명의 개발자입니다. 오늘은 제가 직접 겪었던 아찔한 경험과 그 과정에서 얻은 데이터베이스 확장성 노하우를 공유해볼까 합니다. "설마 우리 서비스에 트래픽 폭증이 오겠어?"라고 생각했던 적이 있습니다. 하지만 세상일은 알 수 없는 법이죠. 예상치 못한 이벤트로 순식간에 수십 배의 트래픽이 몰려왔고, 저희 서비스는 그야말로 아수라장이 되었습니다. 이때 관리형 데이터베이스의 확장성 기능들을 실전에서 적용하고 최적화하면서 많은 것을 배웠습니다.

이 글은 저처럼 기본기는 있지만, 실제 서비스에서 트래픽 폭증을 겪어보지 못한 주니어 개발자분들이 미리 대비하고, 유사한 상황 발생 시 당황하지 않고 대응할 수 있도록 실전 체크리스트와 가이드를 제공하는 데 목적이 있습니다. 읽기 복제본, 샤딩, 연결 풀 최적화 같은 개념들이 이론적으로는 익숙해도, "언제 어떻게 적용해야 할까?"라는 질문에는 막막함을 느낄 수 있습니다. 제가 직접 해보니, 이 방법들이 정말 통했습니다. 자, 그럼 실전 경험담을 통해 함께 데이터베이스 확장성을 점검해볼까요?

📑 목차

트래픽 증가에 대비하는 관리형 데이터베이스 확장성 점검 가이드: 읽기 복제본, 샤딩, 연결 풀 최적화 실전 체크리스트 - art, sculpture, scrap sculpture, human, replica, head replica, artwork, head, rust, rusty, weld seams, scrap metal, sculpture made from scrap, head made of scrap, scrap head, man, masculine, profile, profile pic, sculpture, sculpture, rust, rust, rust, rust, rust, profile, profile, profile pic

Image by NoName_13 on Pixabay

문제 발생: 예상치 못한 트래픽 급증과 서비스 마비 위기

제가 참여했던 한 프로젝트는 특정 기간 동안 진행되는 온라인 이벤트 플랫폼이었습니다. 평소에는 수백 명 수준의 동시 접속자를 처리하는 데 전혀 문제가 없었죠. 저희는 AWS RDS MySQL을 사용하고 있었고, 기본적인 모니터링은 하고 있었지만, 대규모 트래픽에 대한 대비는 솔직히 미흡했습니다. 출시 초기, 작은 규모의 홍보 이벤트만으로도 서비스가 안정적으로 운영되었기 때문에, "이 정도면 충분하지 않을까?"라는 안일한 생각을 했던 것이 화근이었습니다.

문제는 본 이벤트 시작일에 터졌습니다. 예상치를 훨씬 뛰어넘는 수만 명의 사용자가 한꺼번에 몰려들면서, 백엔드 서버와 연결된 데이터베이스가 비명을 지르기 시작했습니다. 처음에는 일부 API 응답이 느려지는 수준이었지만, 몇 분 지나지 않아 데이터베이스 연결 오류, 타임아웃이 발생하기 시작했고, 결국 핵심 기능인 이벤트 참여 신청이 불가능해지는 상황에 이르렀습니다. 사용자들은 "서비스가 터졌다"며 불만을 쏟아냈고, 저희 팀은 그야말로 패닉 상태에 빠졌습니다. 모니터링 대시보드는 온통 빨간불로 가득했고, DB CPU 사용률은 100%를 찍고 내려올 줄 몰랐습니다.

이때 깨달았습니다. 이론으로만 알던 데이터베이스 확장성이 실제 서비스에서는 얼마나 중요한지, 그리고 미리 대비하지 않으면 한순간에 모든 노력이 물거품이 될 수 있다는 사실을요.

원인 분석: 우리 DB는 왜 비명을 질렀을까?

서비스 마비 직후, 팀원들과 함께 긴급하게 원인 분석에 들어갔습니다. 모니터링 지표와 로그를 살펴보며 문제의 근원을 파고들었죠. 몇 가지 핵심적인 원인을 발견할 수 있었습니다.

1. 단일 프라이머리 DB에 집중된 부하: 읽기와 쓰기 동시 폭증

저희는 단일 RDS 인스턴스를 사용하고 있었습니다. 평소에는 문제가 없었지만, 이벤트 시작과 동시에 수많은 사용자가 페이지를 새로고침하고, 이벤트 상세 정보를 조회하고, 동시에 참여 버튼을 누르면서 읽기(Read) 부하와 쓰기(Write) 부하가 프라이머리 DB에 집중되었습니다. 특히 이벤트 상세 정보 조회와 같은 읽기 작업은 그 빈도가 압도적으로 많았죠. 단일 DB가 이 모든 요청을 처리하기에는 역부족이었습니다. RDS 대시보드의 CPUUtilization, DatabaseConnections, ReadIOPS, WriteIOPS 지표들이 모두 임계치를 훨씬 넘어선 상태였습니다.

2. 비효율적인 쿼리와 인덱스 부족

몇몇 핵심 쿼리들이 제대로 최적화되어 있지 않았습니다. 특히 이벤트 참여 여부를 확인하거나, 랭킹을 조회하는 쿼리들은 대량의 데이터를 스캔해야 했고, 적절한 인덱스가 없어 실행 시간이 매우 길어졌습니다. 이로 인해 DB 연결 풀의 커넥션이 오랫동안 점유되면서, 새로운 요청을 처리할 수 없는 상황이 발생했습니다.


-- 예시: 인덱스 없이 대량의 데이터를 스캔하는 쿼리
SELECT user_id, event_score FROM event_participants WHERE event_id = ? ORDER BY event_score DESC LIMIT 100;
-- event_score 컬럼에 인덱스가 없다면 매우 느려집니다.

3. 애플리케이션의 DB 연결 풀 설정 미흡

저희 애플리케이션은 Spring Boot와 HikariCP를 사용하고 있었는데, DB 연결 풀 설정이 기본값에 가깝게 되어 있었습니다. 갑작스러운 트래픽 증가로 인해 DB 연결 요청이 폭주하자, 연결 풀의 최대 연결 개수(maximumPoolSize)가 빠르게 소진되었고, 새로운 DB 연결 요청은 대기 상태에 머물거나 타임아웃되었습니다. 이는 DB 자체의 부하를 가중시키는 악순환으로 이어졌습니다.

결론적으로, 저희 DB는 읽기/쓰기 부하 분산 부재, 쿼리 비효율성, 그리고 애플리케이션 레벨의 연결 관리 미흡이라는 삼중고에 시달리고 있었던 것입니다.

트래픽 증가에 대비하는 관리형 데이터베이스 확장성 점검 가이드: 읽기 복제본, 샤딩, 연결 풀 최적화 실전 체크리스트 - airsoft, airsoftguns, specnaarms, replica asg, replica airsoft, specna, tactical, asg, replica, rifle, tactics, airsoft, airsoft, airsoft, airsoft, airsoft, tactical, tactical, tactical, tactical, tactical

Image by 8089514 on Pixabay

해결 과정: 읽기 복제본부터 샤딩, 연결 풀까지 실전 적용기

원인 분석을 마친 후, 서비스 정상화를 위한 긴급 대응과 장기적인 확장성 확보를 위한 개선 작업을 동시에 진행했습니다. 이 과정에서 관리형 데이터베이스의 강력한 기능들을 직접 활용하며 많은 것을 배웠습니다.

1. 읽기 복제본(Read Replica) 도입: 읽기 부하 분산의 첫걸음

가장 먼저 진행한 것은 읽기 복제본(Read Replica) 도입이었습니다. 저희 서비스의 부하 대부분이 읽기(SELECT) 작업에서 발생하고 있었기 때문에, 읽기 복제본은 가장 빠르고 효과적인 해결책이 될 것이라고 판단했습니다.

실전 적용 과정:

  1. RDS 콘솔에서 읽기 복제본 생성: 기존 프라이머리 DB와 동일한 인스턴스 타입으로 읽기 복제본을 추가했습니다. AWS RDS의 경우 몇 번의 클릭만으로 쉽게 생성할 수 있습니다.
  2. 애플리케이션 코드 수정: 읽기 전용 쿼리(SELECT)는 읽기 복제본 엔드포인트로, 쓰기 쿼리(INSERT, UPDATE, DELETE)는 기존 프라이머리 DB 엔드포인트로 향하도록 애플리케이션 코드를 수정했습니다. Spring Framework의 경우 AbstractRoutingDataSource를 활용하여 트랜잭션의 읽기 전용 여부에 따라 데이터소스를 분리하는 방식으로 구현했습니다.
  3. 모니터링 및 검증: 읽기 복제본 도입 후, 프라이머리 DB의 ReadIOPSCPUUtilization이 현저히 줄어드는 것을 확인할 수 있었습니다. 읽기 복제본 자체의 지표들도 함께 모니터링하며 부하 분산이 제대로 이루어지는지 검증했습니다.

읽기 복제본의 장점과 한계:

장점 한계
구현이 비교적 간단하고 빠르다. 쓰기(Write) 부하 분산에는 도움이 되지 않는다.
읽기 성능을 크게 향상시킬 수 있다. 복제 지연(Replication Lag)이 발생할 수 있다.
비용 효율적인 수평 확장이 가능하다. 복잡한 트랜잭션에서 데이터 일관성 문제가 발생할 수 있다.

팁: 복제 지연이 중요한 서비스라면, 읽기 복제본에서 읽은 데이터가 최신이 아닐 수 있음을 인지하고, 필요시 프라이머리 DB에서 읽도록 폴백 로직을 구현하는 것을 고려해야 합니다.

2. 연결 풀(Connection Pool) 최적화: 애플리케이션 레벨의 효율성 증대

읽기 복제본으로 DB 자체의 부하를 줄이는 동안, 애플리케이션 측면에서의 최적화도 병행했습니다. 바로 DB 연결 풀(Connection Pool) 최적화입니다. 저희는 HikariCP를 사용하고 있었으므로, application.yml 설정을 조정했습니다.

실전 적용 과정:

  1. maximumPoolSize 조정: 서버 인스턴스당 적절한 최대 연결 개수를 설정했습니다. 너무 많으면 DB에 과부하를 주고, 너무 적으면 대기 시간이 길어지므로, DB의 최대 연결 수와 서버 인스턴스 개수를 고려하여 결정했습니다. 일반적으로 (DB 서버 코어 수 * 2) + 1 정도를 시작점으로 고려합니다.
  2. minimumIdle 설정: 유휴 상태로 유지할 최소 연결 수를 설정하여, 갑작스러운 요청에도 빠르게 연결을 제공할 수 있도록 했습니다.
  3. connectionTimeout, idleTimeout, maxLifetime 설정: 연결 타임아웃, 유휴 연결 제거 시간, 연결 최대 수명 등을 조절하여 연결 풀의 효율성을 높였습니다. 특히 connectionTimeout을 적절히 설정하여, 연결을 기다리다 무한정 블로킹되는 것을 방지했습니다.

# application.yml 예시 (HikariCP)
spring:
  datasource:
    hikari:
      jdbc-url: ${DB_URL}
      username: ${DB_USERNAME}
      password: ${DB_PASSWORD}
      driver-class-name: com.mysql.cj.jdbc.Driver
      maximum-pool-size: 20 # 서버 인스턴스당 최대 연결 개수
      minimum-idle: 5      # 최소 유휴 연결 개수
      connection-timeout: 30000 # 30초
      idle-timeout: 600000    # 10분
      max-lifetime: 1800000   # 30분

팁: 연결 풀 설정은 애플리케이션의 특성, DB 서버의 사양, 그리고 예상 동시 접속자 수를 종합적으로 고려하여 신중하게 결정해야 합니다. 초기에는 보수적으로 설정하고, 모니터링을 통해 점진적으로 최적화하는 것이 좋습니다.

3. 샤딩(Sharding) 검토: 장기적인 수평 확장 전략

읽기 복제본과 연결 풀 최적화로 급한 불은 껐지만, 쓰기(Write) 부하가 지속적으로 증가할 경우를 대비하여 샤딩(Sharding)을 검토했습니다. 샤딩은 데이터를 여러 데이터베이스 인스턴스로 분할하여 저장하는 수평 확장 기법입니다.

샤딩의 종류와 고려사항:

종류 설명 고려사항
범위 기반 샤딩 특정 컬럼(예: ID 범위, 날짜 범위)을 기준으로 데이터를 분할 데이터 분포가 고르지 않을 경우 핫스팟 발생 가능
해시 기반 샤딩 샤드 키의 해시 값을 사용하여 데이터를 분할 데이터 분포는 고르지만, 특정 샤드 데이터 조회 시 키 필요
디렉터리 기반 샤딩 별도의 디렉터리 테이블에 샤드 키와 샤드 매핑 정보 저장 유연하지만, 디렉터리 서비스의 고가용성 확보가 중요

저희 프로젝트의 경우, 사용자 ID 기반으로 데이터를 분할하는 해시 기반 샤딩을 고려했습니다. 하지만 샤딩은 복잡도가 매우 높은 작업이며, 데이터 일관성, 분산 트랜잭션 처리, 샤드 재분배(re-sharding) 등 고려해야 할 사항이 많습니다. 특히 기존 데이터를 샤딩하기 위해서는 마이그레이션 전략도 필요합니다. 다행히 읽기 복제본과 연결 풀 최적화만으로도 충분히 부하를 감당할 수 있게 되어, 당장은 샤딩을 적용하지 않고 장기적인 성장 전략으로 남겨두었습니다.

팁: 샤딩은 서비스의 규모가 매우 커지고, 다른 확장성 방안으로는 더 이상 감당하기 어려울 때 최후의 수단으로 고려하는 것이 좋습니다. 처음부터 샤딩을 염두에 두고 설계하는 것이 가장 좋지만, 그렇지 않다면 신중한 계획과 충분한 테스트가 필수적입니다.

트래픽 증가에 대비하는 관리형 데이터베이스 확장성 점검 가이드: 읽기 복제본, 샤딩, 연결 풀 최적화 실전 체크리스트 - airsoft, airsoftguns, specnaarms, replica weapons, replica asg, replica airsoft, airsoft, airsoft, airsoft, airsoft, airsoft

Image by 8089514 on Pixabay

교훈 및 실전 체크리스트: 위기를 기회로 만드는 확장성 점검

이 경험을 통해 얻은 가장 큰 교훈은 데이터베이스 확장성은 미리미리 점검하고 대비해야 한다는 것입니다. 문제가 터진 후에 수습하는 것은 엄청난 비용과 노력이 들 뿐 아니라, 고객 신뢰를 잃을 수도 있습니다.

제가 실전에서 얻은 노하우를 바탕으로, 주니어 개발자분들이 지금 당장 적용해볼 수 있는 데이터베이스 확장성 점검 체크리스트를 공유합니다.

데이터베이스 확장성 실전 체크리스트

  1. 읽기/쓰기 부하 분리 및 읽기 복제본(Read Replica) 활용
    • 현재 DB의 읽기(SELECT) 부하 비율은 어느 정도인가요? (모니터링 툴 확인)
    • 읽기 전용 트래픽이 많다면, 읽기 복제본 도입을 고려하고 있나요?
    • 읽기 복제본 도입 시, 애플리케이션에서 읽기 쿼리를 분리하여 라우팅할 수 있는 구조인가요? (예: AbstractRoutingDataSource)
    • 복제 지연(Replication Lag)에 대한 대비책이 있나요? (예: 중요 데이터는 프라이머리에서 읽기)
  2. 데이터베이스 연결 풀(Connection Pool) 최적화
    • 애플리케이션에서 사용하는 DB 연결 풀(예: HikariCP)의 maximumPoolSize, minimumIdle은 적절하게 설정되어 있나요?
    • DB 서버의 최대 연결 개수(max_connections)와 애플리케이션 연결 풀 설정 간의 균형은 맞나요?
    • connectionTimeout 설정은 너무 길거나 짧지 않은가요? (적절한 타임아웃으로 불필요한 대기 방지)
    • 연결 풀 관련 모니터링 지표(활성 연결 수, 대기 연결 수 등)를 주기적으로 확인하고 있나요?
  3. 쿼리 최적화 및 인덱싱
    • Slow Query 로그를 주기적으로 확인하고 있나요? (MySQL slow_query_log)
    • 실행 계획(Execution Plan)을 통해 비효율적인 쿼리를 식별하고 있나요? (EXPLAIN 명령어 활용)
    • 테이블의 인덱스 설계는 적절한가요? 특히 WHERE 절, JOIN 조건, ORDER BY, GROUP BY에 사용되는 컬럼에 인덱스가 있나요?
    • 너무 많은 인덱스는 쓰기 성능 저하를 유발할 수 있음을 인지하고 있나요?
  4. 데이터 모델링 및 스키마 설계
    • 대용량 테이블의 경우, 파티셔닝(Partitioning)을 고려하고 있나요?
    • 불필요한 데이터가 많이 쌓이지 않도록 데이터 보관 정책이 있나요?
    • 정규화/비정규화 수준은 서비스 특성에 적합한가요? (읽기 성능을 위해 의도적인 비정규화 고려)
  5. 샤딩(Sharding) 검토 (대규모 서비스)
    • 읽기 복제본, 쿼리 최적화 등으로는 더 이상 감당하기 어려운 쓰기 부하가 예상되나요?
    • 샤딩 키(Sharding Key)를 선정할 수 있는 명확한 기준이 있나요? (예: 사용자 ID, 지역)
    • 샤딩 도입 시 데이터 마이그레이션, 분산 트랜잭션, 샤드 재분배 등에 대한 전략이 있나요?
    • 관리형 샤딩 솔루션(예: Vitess, CitusDB)을 고려하고 있나요?
  6. DB 모니터링 및 경고 시스템
    • CPU 사용률, 메모리 사용률, 디스크 I/O, 네트워크 처리량 등 핵심 DB 지표를 실시간으로 모니터링하고 있나요?
    • DB 연결 수, 활성 세션 수, 잠금(Lock) 발생 여부 등을 확인하고 있나요?
    • 임계치 초과 시 알림을 받을 수 있는 경고 시스템이 구축되어 있나요? (예: CloudWatch Alarms, Grafana Alerts)

이 체크리스트를 정기적으로 점검하고, 서비스의 성장 단계에 맞춰 적절한 확장성 전략을 수립하는 것이 중요합니다. 특히 주니어 개발자일수록 이런 실전 경험을 통해 문제 해결 능력을 키울 수 있습니다.

결론: DB 확장성, 미리 점검하고 대비하자

데이터베이스는 서비스의 핵심 엔진과 같습니다. 아무리 멋진 기능을 구현해도, DB가 비명을 지르면 결국 서비스는 멈출 수밖에 없습니다. 제가 겪었던 트래픽 폭증 경험은 뼈아팠지만, 덕분에 데이터베이스 확장성에 대해 깊이 이해하고 실전 노하우를 쌓을 수 있는 계기가 되었습니다.

주니어 개발자 여러분, 지금 당장 여러분의 서비스 데이터베이스를 한 번 점검해보세요. 당장 문제가 없더라도, 미래의 트래픽을 예상하고 미리 대비하는 자세가 중요합니다. 읽기 복제본, 연결 풀 최적화와 같은 비교적 쉬운 방법부터 시작하여, 필요에 따라 샤딩과 같은 고급 기술까지 단계적으로 접근해보시기 바랍니다.

이 글이 여러분의 서비스가 안정적으로 성장하는 데 작은 도움이 되기를 바랍니다. 여러분의 서비스는 어떤 확장성 문제에 직면했었나요? 혹은 어떤 방법으로 해결하셨는지 댓글로 자유롭게 경험을 공유해주세요!

📌 함께 읽으면 좋은 글

  • [오픈소스] 오픈소스 특허 소송 리스크 90% 줄이는 개발자 방어 전략: 생존과 성장을 위한 핵심 가이드
  • [튜토리얼] 대용량 파일 업로드, 어떤 전략이 현명할까요? Pre-signed URL vs. 백엔드 프록시 비교
  • [클라우드 인프라] 메인프레임 JCL vs 클라우드 네이티브 워크플로우: 현대화를 위한 전환 전략

이 글이 도움이 되셨다면 공감(♥)댓글로 응원해 주세요!
궁금한 점이나 다루었으면 하는 주제가 있다면 댓글로 남겨주세요.

반응형