데이터베이스 복제를 이용한 읽기 및 쓰기 확장
Daniel Hayes
Full-Stack Engineer · Leapcell

소개
오늘날 데이터 중심 세상에서 애플리케이션은 높은 성능과 가용성을 요구합니다. 사용자 기반이 성장하고 데이터 볼륨이 폭등함에 따라 단일 데이터베이스 인스턴스는 종종 병목 현상이 됩니다. 특히 쓰기 작업과 동일한 리소스에 액세스하려는 읽기 중심 워크로드로 인해 성능 문제가 발생하면 응답 시간이 느려지고 사용자 경험이 저하될 수 있습니다. 이러한 과제는 확장 가능한 데이터베이스 아키텍처의 중요성을 강조합니다. 이러한 한계를 극복하기 위한 강력하고 널리 채택된 솔루션 중 하나는 데이터베이스 마스터-리플리카 복제를 활용하는 읽기-쓰기 분할입니다. 이 접근 방식은 애플리케이션의 처리량을 높일 뿐만 아니라 복원력도 향상시켜 현대 분산 시스템의 초석이 됩니다.
데이터베이스 확장성 기본 개념
읽기-쓰기 분할의 복잡성을 자세히 알아보기 전에 이 아키텍처 패턴을 뒷받침하는 몇 가지 기본 개념을 이해해 보겠습니다.
-
복제: 근본적으로 복제는 데이터의 여러 복사본을 생성하고 유지 관리하는 프로세스입니다. 데이터베이스에서는 일반적으로 기본(마스터) 데이터베이스에서 하나 이상의 보조(리플리카 또는 슬레이브) 데이터베이스로 데이터를 복사하는 작업이 포함됩니다. 주요 목적은 데이터 중복을 보장하고 가용성을 개선하며 워크로드를 분산하는 것입니다.
-
마스터(기본) 데이터베이스: 모든 쓰기 작업(INSERT, UPDATE, DELETE)을 수락하는 책임이 있는 권한 있는 데이터베이스 인스턴스입니다. 읽기 작업도 처리할 수 있지만, 읽기-쓰기 분할 설정에서는 주로 리플리카로 읽기 작업을 오프로드합니다.
-
리플리카(슬레이브) 데이터베이스: 리플리카 데이터베이스는 마스터의 데이터 복사본을 포함합니다. 일반적으로 전용으로 읽기 작업을 처리하도록 구성됩니다. 리플리카는 마스터로부터 비동기적으로 변경 사항을 수신하고 적용하여 가능한 한 최신 상태를 유지하려고 노력합니다.
-
비동기 복제: 비동기 복제에서 마스터 데이터베이스는 트랜잭션을 커밋한 다음 변경 사항을 리플리카로 보냅니다. 마스터는 자체 트랜잭션을 커밋하기 전에 리플리카가 변경 사항을 수신하거나 적용했음을 확인하기를 기다리지 않습니다. 이는 마스터에서 높은 성능을 제공하지만 마스터와 리플리카 간에 약간의 지연(복제 지연)이 발생할 수 있습니다. 대부분의 마스터-리플리카 설정은 비동기 복제를 사용합니다.
-
읽기-쓰기 분할: 애플리케이션이 모든 쓰기 작업을 마스터 데이터베이스로 라우팅하고 읽기 작업을 하나 이상의 리플리카 데이터베이스로 분산하는 아키텍처 패턴입니다. 이러한 관심사 분리는 마스터가 읽기 쿼리와의 충돌 없이 쓰기 작업을 효율적으로 처리할 수 있도록 하고, 리플리카는 많은 읽기 작업을 동시에 처리할 수 있습니다.
읽기-쓰기 분할의 원칙 및 구현
마스터-리플리카 복제를 사용한 읽기-쓰기 분할의 기본 원칙은 영향에 따라 데이터베이스 작업을 분리하는 것입니다. 즉, 쓰기는 데이터를 수정하고 읽기는 데이터를 검색하기만 합니다. 마스터를 쓰기 전용으로, 리플리카를 읽기 전용으로 지정하면 시스템은 더 큰 확장성과 성능을 달성할 수 있습니다.
작동 방식
- 쓰기 작업: 모든
INSERT
,UPDATE
,DELETE
쿼리는 마스터 데이터베이스로 라우팅됩니다. 마스터는 이러한 트랜잭션을 처리하고 데이터를 업데이트하며 이진 로그(MySQL의 binlog) 또는 트랜잭션 로그(PostgreSQL의 WAL)에 변경 사항을 기록합니다. - 복제: 리플리카는 마스터의 트랜잭션 로그를 지속적으로 모니터링합니다. 새 변경 사항이 감지되면 해당 변경 사항을 가져와 로컬 데이터 복사본에 적용하여 마스터와의 최종 일관성을 보장합니다.
- 읽기 작업: 모든
SELECT
쿼리는 하나 이상의 리플리카 데이터베이스로 전달됩니다. 이는 마스터의 읽기 부담을 덜어주어 쓰기 트랜잭션에 집중할 수 있도록 합니다. 로드 밸런서 또는 애플리케이션 수준 라우팅 메커니즘이 이러한 읽기 쿼리를 사용 가능한 리플리카 간에 분배합니다.
구현 전략
읽기-쓰기 분할을 구현하려면 일반적으로 애플리케이션 계층, 데이터베이스 프록시 계층 또는 둘의 조합에서 수정이 필요합니다.
1. 애플리케이션 수준 라우팅
이 접근 방식에서는 애플리케이션 코드 자체가 쿼리가 읽기인지 쓰기인지 결정하고 적절한 데이터베이스 인스턴스에 연결하는 책임을 집니다.
예시 (가상의 Python/SQLAlchemy 설정을 사용):
from sqlalchemy import create_engine, text from sqlalchemy.orm import sessionmaker # 데이터베이스 연결 문자열 MASTER_DB_URL = "mysql+mysqlconnector://user:password@master_host/db_name" REPLICA_DB_URL = "mysql+mysqlconnector://user:password@replica_host/db_name" # 엔진 생성 master_engine = create_engine(MASTER_DB_URL) replica_engine = create_engine(REPLICA_DB_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False) def get_db_session(write_operation: bool): """ 마스터 또는 리플리카에 연결된 SQLAlchemy 세션을 반환합니다. """ if write_operation: SessionLocal.configure(bind=master_engine) else: # 여러 리플리카 간의 로드 밸런싱을 위한 로직을 추가할 수 있습니다. SessionLocal.configure(bind=replica_engine) session = SessionLocal() try: yield session finally: session.close() # 웹 애플리케이션 컨텍스트에서 사용: def create_new_user(user_data): with next(get_db_session(write_operation=True)) as db: db.execute(text("INSERT INTO users (name, email) VALUES (:name, :email)"), user_data) db.commit() return {"message": "사용자가 성공적으로 생성되었습니다."} def get_user_by_id(user_id): with next(get_db_session(write_operation=False)) as db: user = db.execute(text("SELECT * FROM users WHERE id = :id"), {"id": user_id}).fetchone() return user
장점: 최대 유연성, 라우팅에 대한 세분화된 제어. 단점: 상당한 애플리케이션 코드 변경이 필요하며, 라우팅에서 개발자 오류가 발생할 수 있으며, 여러 데이터베이스 연결을 관리하는 것이 복잡할 수 있습니다.
2. 데이터베이스 프록시 수준
더 일반적이고 종종 선호되는 접근 방식은 데이터베이스 프록시를 사용하는 것입니다. 프록시는 애플리케이션과 데이터베이스 인스턴스 간의 중개자 역할을 합니다. 수신 쿼리를 가로채고, 검사하고, 구성된 규칙(예: 쿼리 유형, SQL 키워드)에 따라 마스터 또는 리플리카로 라우팅합니다. 인기 있는 프록시 솔루션에는 MaxScale(MySQL용), PgBouncer(PostgreSQL용, 주로 연결 풀러이지만 라우팅용으로 확장 가능) 및 독점 솔루션이 있습니다.
예시 (개념적 MaxScale 구성 스니펫):
[master_server] type=server address=192.168.1.10 port=3306 protocol=MySQLBackend [replica_server_1] type=server address=192.168.1.11 port=3306 protocol=MySQLBackend [replica_server_2] type=server address=192.168.1.12 port=3306 protocol=MySQLBackend [readwritesplit_service] type=service router=readwritesplit servers=master_server,replica_server_1,replica_server_2 router_options=master=master_server # MaxScale은 쿼리를 자동으로 분석하여 쓰기를 마스터로, 읽기를 리플리카로 라우팅합니다. # 여러 리플리카 간의 읽기 로드 밸런싱도 처리할 수 있습니다. [readwritesplit_listener] type=listener service=readwritesplit_service protocol=MySQLClient port=4006
이 설정에서는 애플리케이션이 프록시의 리스너 포트(예: 4006)에만 연결하고, 프록시가 투명하게 라우팅을 처리합니다.
장점: 애플리케이션 코드는 대부분 변경되지 않고, 라우팅 규칙의 중앙 집중식 관리, 강력한 로드 밸런싱 기능, 애플리케이션의 연결 관리 단순화. 단점: 추가적인 복잡성 계층과 잠재적인 단일 실패 지점이 발생합니다(프록시도 고가용성으로 구성할 수 있음).
주요 고려 사항
- 복제 지연: 비동기 복제는 마스터와 리플리카 간에 지연이 발생합니다. 애플리케이션은 이 점을 반드시 인지해야 합니다. 예를 들어, 사용자가 마스터에 데이터를 쓰고 즉시 리플리카에서 읽으려고 하면 데이터가 아직 리플리카에 없을 수 있어 "오래된 읽기"가 발생할 수 있습니다. 이를 완화하기 위한 전략은 다음과 같습니다:
- 쓰기 후 읽기 일관성: 쓰기 직후의 중요한 읽기의 경우 마스터로 읽기를 지시합니다.
- 복제 대기: 특정 경우 애플리케이션은 읽기를 수행하기 전에 리플리카가 마스터의 특정 트랜잭션 ID까지 따라잡기를 명시적으로 기다릴 수 있습니다.
- 최종 일관성 수용: 덜 중요한 데이터의 경우 약간의 지연을 수용하는 것이 종종 허용됩니다.
- 로드 밸런싱: 여러 리플리카가 있는 경우 로드 밸런서(외부 시스템 또는 데이터베이스 프록시에 내장)는 읽기 쿼리를 리플리카 간에 고르게 분산하여 단일 리플리카가 병목 현상이 되는 것을 방지하는 데 중요합니다.
- 장애 조치: 마스터에 문제가 발생하면 어떻게 됩니까? 강력한 설정에는 자동 또는 수동 장애 조치 메커니즘이 포함되어 리플리카 중 하나를 새로운 마스터로 승격시킵니다. 이는 고가용성을 보장합니다.
- 모니터링: 모든 데이터베이스 인스턴스에서 복제 상태, 복제 지연 및 리소스 사용률(CPU, 메모리, I/O)을 면밀히 모니터링하여 문제를 사전에 식별하고 해결합니다.
결론
데이터베이스 마스터-리플리카 복제와읽기-쓰기 분할은 확장 가능하고 탄력적인 애플리케이션을 구축하는 데 필수적인 아키텍처 패턴입니다. 쓰기 및 읽기 작업을 지능적으로 분리함으로써 데이터베이스 성능을 크게 향상시키고 기본 인스턴스의 부하를 줄이며 전반적인 시스템 가용성을 개선합니다. 복제 지연 및 장애 조치와 같은 고려 사항은 신중한 주의가 필요하지만, 처리량 및 응답성 향상의 이점은 이 접근 방식을 현대 데이터 집약적 시스템을 위한 최고의 솔루션으로 만들어, 애플리케이션이 끊임없이 증가하는 요구 사항을 우아하게 처리할 수 있도록 합니다. 이 전략은 단일 데이터베이스 병목 현상을 엄청난 부하를 처리할 수 있는 분산 파워하우스로 변화시킵니다.