데이터베이스 동시성 설계의 핵심 원칙
동시성 설계 원칙은 여러 트랜잭션이 동시에 실행될 때 데이터의 정합성을 보장하고 충돌을 효율적으로 관리하기 위한 데이터베이스 설계 지침입니다. 서비스가 성장함에 따라 발생하는 성능 및 정합성 문제를 해결하는 데 필수적인 역할을 합니다. 이 글에서는 동시성 설계의 핵심 개념과 다른 설계 원칙과의 관계를 정리합니다.
동시성 설계의 핵심 개념
격리 수준 (Isolation Level)
격리 수준은 동시에 실행되는 트랜잭션들이 서로에게 영향을 미치는 정도를 결정하는 장치입니다. 격리 수준이 높을수록 데이터 정합성은 향상되지만 동시성 성능은 저하될 수 있으므로, 서비스의 특성에 맞춰 적절한 수준을 선택해야 합니다.
READ COMMITTED 다른 트랜잭션이 커밋하지 않은 데이터의 읽기(Dirty Read)를 방지합니다. 그러나 한 트랜잭션 내에서 동일한 데이터를 두 번 읽을 때 결과가 다를 수 있는 현상(Non-repeatable Read)은 허용합니다. 뉴스나 게시판 목록처럼 읽기 위주이고 데이터 정합성 요구가 비교적 낮은 화면에 적합하며, Gap Lock이 적어 동시성이 높습니다. PostgreSQL의 기본 격리 수준입니다.
REPEATABLE READ 트랜잭션이 시작될 때의 데이터 스냅샷을 트랜잭션이 종료될 때까지 유지합니다. 이를 통해 Dirty Read와 Non-repeatable Read를 방지합니다. 특히 MySQL은 Gap Lock을 사용하여 새로운 레코드가 추가되어 결과가 달라지는 팬텀 현상(Phantom Read)도 대부분 막아줍니다. 장바구니 합계 계산이나 재고 차감처럼 “읽고 곧바로 쓰는” 패턴이 많은 로직에 적합하며, MySQL의 기본 격리 수준입니다.
SERIALIZABLE 모든
SELECT연산에 공유 락(Shared Lock) 또는 범위 락(Range Lock)을 적용하여 트랜잭션을 사실상 순차적으로 처리하는 것처럼 동작하게 합니다. 팬텀 현상까지 완벽하게 차단하지만, 락 경합과 대기가 증가하여 성능은 가장 느립니다. 회계, 결제 승인, 금융 이체 등 높은 수준의 데이터 정합성이 요구되는 업무에 사용됩니다.
같은 데이터베이스 내에서도 각 트랜잭션의 목적에 따라 개별적으로 격리 수준을 지정하는 것이 가능합니다.
락 (Lock)
락은 여러 트랜잭션이 동시에 같은 행(Row)을 변경하지 못하도록 제어하는 장치입니다.
- Gap Lock: 행과 행 사이의 ‘빈 공간’을 잠그는 범위 락입니다. 특정 범위 내에 새로운 레코드가 삽입되는 것을 막아 팬텀 현상을 방지하는 데 도움을 줍니다. 그러나 인덱스 구조가 잘못 설계된 경우 의도치 않은 Gap Lock이 발생하여 동시성 문제를 유발할 수 있으므로, 고유한 인덱스나 명확한 조인 조건 설정이 중요합니다.
데드락 (Deadlock)
데드락은 두 개 이상의 트랜잭션이 서로가 점유한 락을 얻기 위해 무한 대기 상태에 빠지는 현상입니다.
대부분의 데드락은 트랜잭션 내에서 테이블에 접근하는 순서를 일관되게 고정함으로써 예방할 수 있습니다. 예를 들어, 모든 트랜잭션이 users → orders → payments 순서로 테이블에 접근하도록 규칙을 정하는 것입니다. 접근 순서가 뒤바뀌면 교차 락(Cross Lock)이 발생할 수 있으며, 이 경우 MySQL과 같은 DBMS는 데드락을 감지하고 한쪽 트랜잭션을 강제로 종료합니다. 데드락 발생 시에는 애플리케이션 레벨에서 재시도(back-off) 로직을 구현하여 대응할 수 있습니다.
PK/인덱스 구조와 락 경합
PK 설계: 순차적으로 증가하는 PK(Auto-increment)만 사용하면
INSERT작업이 테이블의 마지막 페이지에 집중되는 ‘Insert Hotspot’이 발생할 수 있습니다. 이를 완화하기 위해 UUID v4나 분산 키(Distributed Key)를 사용하여 쓰기 작업을 여러 페이지로 분산시키는 전략을 고려할 수 있습니다.인덱스 활용:
WHERE조건절에서 자주 사용되는 컬럼에는 반드시 인덱스를 생성해야 합니다. 인덱스가 없으면 특정 레코드 대신 테이블 전체가 잠겨 락 경합이 길어지고 성능이 저하될 수 있습니다. 복합 인덱스는 인덱스를 구성하는 컬럼 순서대로, 즉 왼쪽부터 순차적으로만 활용되므로 자주 조합되는 검색 조건을 고려하여 설계해야 합니다.
DB 설계 단계에서 해야 할 일
- 격리 수준 문서화: 테이블별로 비즈니스 로직에 맞는 격리 수준을 표로 정리하여 문서화합니다.
- PK 분산 전략 정의: 샤딩 키(Sharding Key)나 UUID 사용 여부 등 PK 분산 전략을 결정합니다.
- 복합 인덱스 미리 설계: 자주 사용되는 검색 키를 효과적으로 커버할 수 있도록 복합 인덱스를 미리 설계합니다.
- 락 순서 명시: 트랜잭션 내 테이블 접근 순서를 코드 컨벤션에 명시하고, 코드 리뷰 시 이를 준수하는지 확인합니다.
동시성 설계와 다른 DB 설계 원칙의 관계
트랜잭션: 동시성은 트랜잭션의 4대 특성(ACID) 중 격리성(Isolation)과 직접적인 관련이 있습니다. 데이터 정합성과 일관성의 기초가 되는 트랜잭션의 단위를 명확히 구성하는 것이 중요합니다.
정규화와 반정규화: 과도한 정규화는 조인(JOIN) 횟수를 늘려 락 경합 가능성을 높일 수 있습니다. 반대로, 과도한 반정규화는 중복 데이터 업데이트를 증가시켜 데드락 발생 확률을 높일 수 있습니다.
읽기 스케일아웃: 리플리카(Replica) DB를 이용한 읽기 스케일아웃은 읽기 트래픽을 분산시키는 좋은 전략이지만, 비동기 복제 방식의 특성상 복제 지연(Replication Lag)이 발생할 수 있습니다. “쓰기 직후의 읽기”와 같은 작업은 마스터(Master) DB를 사용하고, 비즈니스 로직에 따라 DB 연결을 선택하는 기준을 명확히 하여 지연 문제에 대응해야 합니다.
결론적으로, 동시성 설계는 단순히 성능을 높이는 기술을 넘어, 여러 사용자가 동시에 데이터를 사용하더라도 데이터의 신뢰성을 유지하고 예상치 못한 오류를 방지하는 데 필수적인 요소입니다.