본문 바로가기
나의 IT 기억

🎯 스타트업이 놓치기 쉬운 DB 리플리케이션 - 사용자 급증에 대비하라

by 浪畅 (Làng Chàng) 2025. 5. 28.

 


https://github.com/onesound71/mysql-replication-test

 

*"갑자기 사용자가 몰려와서 서버가 다운됐어요!"* - 성공하는 스타트업이라면 한 번쯤 겪는 달콤한 비명이다. 하지만 이 순간을 제대로 준비하지 못한다면, 기회를 놓칠 수 있다.

스타트업의 흔한 실수: 모든 걸 한 대에

대부분의 초기 스타트업은 비용을 절약하기 위해 하나의 서버에 모든 것을 몰아넣는다.

  단일 서버                  
  
  🌐 웹 서버 (Nginx/Apache)        
  ⚙️  WAS (Node.js/Django/Spring)
  🗄️  데이터베이스 (MySQL)          
  📦 Redis/파일시스템

처음엔 괜찮다. 사용자도 적고, 트래픽도 많지 않으니까. 하지만 서비스가 성장하면서 문제가 시작된다.

실제로 겪은 위기 상황들

사례 1: 새벽 2시의 알림

"DB CPU 100%, 서버 응답 안 됨"

갑작스러운 viral 마케팅으로 동시 접속자가 10배 증가. WAS와 DB가 서로 리소스를 놓고 경쟁하며 둘 다 다운.

사례 2: 한 번의 장애가 모든 것을 무너뜨림

디스크 풀로 인한 MySQL 크래시 → WAS도 함께 다운 → 전체 서비스 중단

사례 3: 백업 복구의 악몽

단일 서버 장애 시 백업에서 복구하는 동안 8시간 서비스 중단 → 사용자 이탈

DB 리플리케이션: 작은 투자, 큰 효과

MySQL 마스터-슬레이브 리플리케이션을 도입하면 이런 문제들을 대부분 해결할 수 있다.

리플리케이션의 핵심 이점

1. 읽기 성능 대폭 향상

Before: 모든 요청 → 단일 DB
After:  쓰기 요청 → Master DB
        읽기 요청 → Slave DB (또는 여러 대)

대부분의 웹 서비스에서 읽기:쓰기 비율은 80:20 또는 90:10이다. 읽기 전용 슬레이브 DB를 추가하는 것만으로도 전체 DB 부하를 절반으로 줄일 수 있다.

2. 장애 대응력 강화

  • Master DB 장애 시 → Slave를 Master로 승격 (몇 분 내 복구)
  • 데이터 백업 → Slave에서 진행 (Master 성능에 영향 없음)
  • 점검 작업 → 무중단 롤링 업데이트 가능

3. 비용 효율성

  • 읽기 전용 슬레이브는 저사양 서버도 충분
  • 클라우드에서는 Read Replica가 Master보다 저렴
  • 스케일 아웃으로 점진적 확장 가능

실전! Docker로 MySQL 리플리케이션 구성하기

준비 단계

가장 간단한 방법은 Docker Compose를 활용하는 것이다. 로컬 개발 환경에서 먼저 테스트해보자.

# docker-compose.yml
version: '3.8'
services:
  mysql-master:
    image: mysql:8.0
    container_name: mysql-master
    ports:
      - "13306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: masterpass
      MYSQL_DATABASE: testdb
    volumes:
      - ./master/my.cnf:/etc/mysql/my.cnf
      - ./master/init.sql:/docker-entrypoint-initdb.d/init.sql
      - master_data:/var/lib/mysql

  mysql-slave:
    image: mysql:8.0
    container_name: mysql-slave
    ports:
      - "13307:3306"
    environment:
      MYSQL_ROOT_PASSWORD: slavepass
    volumes:
      - ./slave/my.cnf:/etc/mysql/my.cnf
      - ./slave/init.sql:/docker-entrypoint-initdb.d/init.sql
      - slave_data:/var/lib/mysql
    depends_on:
      - mysql-master

volumes:
  master_data:
  slave_data:

핵심 설정 포인트

Master 설정 (master/my.cnf)

[mysqld]
# 서버 식별 (클러스터 내 유니크)
server-id = 1

# 바이너리 로깅 (리플리케이션의 핵심)
log-bin = mysql-bin
binlog_format = ROW

# GTID 활성화 (MySQL 8.0 권장)
gtid_mode = ON
enforce_gtid_consistency = ON

# 성능 최적화
innodb_buffer_pool_size = 256M
sync_binlog = 1

# 모든 IP에서 접근 허용
bind-address = 0.0.0.0

Slave 설정 (slave/my.cnf)

[mysqld]
server-id = 2

# 읽기 전용 모드
read_only = ON

# GTID 설정
gtid_mode = ON
enforce_gtid_consistency = ON

# 리플리케이션 최적화
replica_parallel_workers = 4
replica_parallel_type = LOGICAL_CLOCK

bind-address = 0.0.0.0

리플리케이션 사용자 생성

Master에서 복제 전용 사용자를 생성한다.

-- master/init.sql
CREATE USER 'replication_user'@'%' IDENTIFIED BY 'replication_pass';
GRANT REPLICATION SLAVE ON *.* TO 'replication_user'@'%';
FLUSH PRIVILEGES;

Slave에서 Master에 연결한다.

-- slave/init.sql
CHANGE REPLICATION SOURCE TO
    SOURCE_HOST='mysql-master',
    SOURCE_USER='replication_user',
    SOURCE_PASSWORD='replication_pass',
    SOURCE_AUTO_POSITION=1;

START REPLICA;

한 줄로 실행하기

# 환경 시작
docker-compose up -d

# 상태 확인
docker exec mysql-master mysql -uroot -pmasterpass -e "SHOW MASTER STATUS;"
docker exec mysql-slave mysql -uroot -pslavepass -e "SHOW REPLICA STATUS\G"

검증: 정말 동작하는지 확인해보자

Python 테스트 스크립트

import mysql.connector
import time
import random

# 연결 설정
MASTER_CONFIG = {
    'host': 'localhost', 'port': 13306,
    'user': 'root', 'password': 'masterpass',
    'database': 'testdb'
}

SLAVE_CONFIG = {
    'host': 'localhost', 'port': 13307,
    'user': 'root', 'password': 'slavepass',
    'database': 'testdb'
}

def test_replication():
    # 1. Master에 데이터 INSERT
    master_conn = mysql.connector.connect(**MASTER_CONFIG)
    master_cursor = master_conn.cursor()

    test_id = random.randint(10000, 99999)
    test_name = f"test_user_{test_id}"
    test_email = f"test_{test_id}@example.com"

    master_cursor.execute(
        "INSERT INTO users (name, email) VALUES (%s, %s)",
        (test_name, test_email)
    )
    master_conn.commit()
    print(f"✅ Master에 삽입: {test_name}")

    # 2. 리플리케이션 대기
    time.sleep(3)

    # 3. Slave에서 데이터 확인
    slave_conn = mysql.connector.connect(**SLAVE_CONFIG)
    slave_cursor = slave_conn.cursor()

    slave_cursor.execute(
        "SELECT name, email FROM users WHERE name = %s",
        (test_name,)
    )
    result = slave_cursor.fetchone()

    if result:
        print(f"✅ Slave에서 확인: {result[0]} - {result[1]}")
        print("🎉 리플리케이션 성공!")
    else:
        print("❌ 리플리케이션 실패")

    master_conn.close()
    slave_conn.close()

if __name__ == "__main__":
    test_replication()

실행 결과

✅ Master에 삽입: test_user_58582
✅ Slave에서 확인: test_user_58582 - test_58582@example.com
🎉 리플리케이션 성공!

실무 적용 시 주의사항

1. MySQL 버전별 차이점

MySQL 5.7 vs 8.0의 주요 차이

기능 MySQL 5.7 MySQL 8.0
기본 인증 mysql_native_password caching_sha2_password
GTID 수동 설정 필요 기본 권장
그룹 리플리케이션 실험적 안정화
성능 기준점 10-20% 향상

⚠️ 주의: 기존에 5.7을 사용 중이라면 8.0으로 업그레이드 시 호환성 검토가 필수다.

2. 애플리케이션 코드 수정

리플리케이션을 도입하면 읽기/쓰기를 분리해야 한다.

# Before: 모든 작업을 단일 DB에
db = get_database_connection()

# After: 읽기/쓰기 분리
def get_db_connection(read_only=False):
    if read_only:
        return connect_to_slave()
    else:
        return connect_to_master()

# 사용 예시
def get_user(user_id):
    db = get_db_connection(read_only=True)  # Slave 사용
    return db.query("SELECT * FROM users WHERE id = %s", user_id)

def create_user(name, email):
    db = get_db_connection(read_only=False)  # Master 사용
    return db.execute("INSERT INTO users (name, email) VALUES (%s, %s)", name, email)

3. 리플리케이션 지연 처리

문제 상황: 사용자가 게시글을 작성한 직후 목록 페이지로 이동했는데 방금 쓴 글이 안 보임

해결 방법:

  1. 읽기 후 일관성 - 쓰기 직후에는 Master에서 읽기
  2. 세션 기반 라우팅 - 사용자별로 일정 시간 Master 사용
  3. 애플리케이션 캐시 - Redis 등으로 최신 데이터 캐싱
def create_post_and_redirect(title, content, user_id):
    # 1. Master에 쓰기
    master_db.execute("INSERT INTO posts (title, content, user_id) VALUES (%s, %s, %s)", 
                      title, content, user_id)

    # 2. 세션에 '방금 쓰기 작업 함' 표시
    session['last_write_time'] = time.time()

    # 3. 리다이렉트
    redirect('/posts')

def get_posts_list(user_id):
    # 방금 쓰기 작업을 했다면 Master에서 읽기
    if session.get('last_write_time', 0) > time.time() - 5:  # 5초 이내
        db = get_master_db()
    else:
        db = get_slave_db()

    return db.query("SELECT * FROM posts WHERE user_id = %s ORDER BY created_at DESC", user_id)

성능 모니터링과 알림 설정

핵심 지표들

-- 리플리케이션 지연 확인
SELECT 
    CHANNEL_NAME,
    SERVICE_STATE,
    LAST_ERROR_MESSAGE,
    LAST_ERROR_TIMESTAMP
FROM performance_schema.replication_connection_status;

-- 슬레이브 상태 확인
SHOW REPLICA STATUS\G

간단한 모니터링 스크립트

#!/bin/bash
# monitor_replication.sh

SLAVE_LAG=$(docker exec mysql-slave mysql -uroot -pslavepass -e "
    SELECT TIMESTAMPDIFF(SECOND, 
        STR_TO_DATE(Master_Log_Time, '%Y-%m-%d %H:%i:%s'),
        NOW()) as lag_seconds
    FROM (SHOW REPLICA STATUS) AS replica_status;" 2>/dev/null | tail -1)

if [ "$SLAVE_LAG" -gt 10 ]; then
    echo "⚠️  리플리케이션 지연: ${SLAVE_LAG}초"
    # Slack 알림 등 추가
fi

다음 단계: 확장 로드맵

단계별 확장 계획

1단계: 기본 Master-Slave (현재)

Master (쓰기) → Slave (읽기)

2단계: 다중 읽기 슬레이브

                ┌─ Slave 1 (읽기)
Master (쓰기) ──┼─ Slave 2 (읽기)
                └─ Slave 3 (백업)

3단계: 지역별 분산

Seoul Master ──→ Tokyo Slave
     │
     └──→ Singapore Slave

4단계: 샤딩 도입

App Layer
    │
    ├─ Shard 1 (User A-M)
    ├─ Shard 2 (User N-Z)
    └─ Shard 3 (Analytics)

클라우드 서비스 활용

AWS RDS Read Replica

  • 자동 백업, 모니터링
  • 장애 시 자동 승격
  • 다중 AZ 배포

Google Cloud SQL

  • 고가용성 설정
  • 자동 스케일링
  • 통합 모니터링

마무리: 미리 준비하는 것의 가치

*"서비스가 성공하면 사용자가 몰릴 텐데, 그때 대응하면 되지 않을까?"*

이런 생각이 위험한 이유는 기회는 한 번만 온다는 것이다. 바이럴이 터졌을 때, 언론에 소개됐을 때, 그 순간에 서버가 다운되면 다음 기회는 언제 올지 모른다.

리플리케이션 도입의 ROI

비용:

  • 개발 시간: 2-3일
  • 추가 서버 비용: 월 50-100달러
  • 운영 복잡성: 약간 증가

수익:

  • 서비스 안정성: 99.9% → 99.99%
  • 성능 향상: 2-5배
  • 기회 손실 방지: 무한대

오늘부터 시작하기

  1. 로컬에서 먼저 테스트 - Docker Compose로 30분이면 구성 가능
  2. 작은 기능부터 적용 - 조회가 많은 API부터 읽기/쓰기 분리
  3. 모니터링 도구 설정 - 지연 시간, 에러율 추적
  4. 팀 내 공유 - 동료들과 함께 운영 노하우 축적

기억하자: 스타트업의 성공은 준비된 자의 몫이다. 사용자가 몰려오기 전에 미리 준비하는 것, 그것이 성장하는 스타트업과 사라지는 스타트업의 차이다.


추가 리소스

  • GitHub 저장소: [완전한 예제 코드와 설정 파일들]
  • MySQL 공식 문서: [리플리케이션 가이드]
  • 모니터링 도구: Prometheus + Grafana 설정 예제
  • 클라우드 마이그레이션: AWS RDS, GCP Cloud SQL 가이드

성공하는 스타트업의 인프라는 하루아침에 만들어지지 않는다. 지금 시작하자.