
주문 수량을 차감하거나 계좌 잔액을 변경하는 작업은 여러 SQL이 모두 성공하거나 모두 취소돼야 합니다. 트랜잭션은 이 원자성을 제공하지만, 트랜잭션을 사용했다는 사실만으로 동시성 문제가 모두 해결되지는 않습니다. 여러 사용자가 같은 데이터를 동시에 읽고 수정하면 갱신 손실, 반복되지 않는 읽기, 팬텀 같은 현상이 생길 수 있습니다. 오라클과 Microsoft SQL Server에서 안정적인 업무를 만들려면 격리 수준, 잠금, 버전 관리와 재시도 정책을 업무 규칙에 맞게 선택해야 합니다.
ACID를 업무 흐름으로 이해하기
원자성은 작업 묶음이 전부 반영되거나 전부 취소되는 성질이고, 일관성은 커밋 전후에 데이터 규칙이 유지된다는 의미입니다. 격리성은 동시 트랜잭션이 서로에게 미치는 영향을 제어하며, 지속성은 커밋된 결과가 장애 뒤에도 보존됨을 뜻합니다. 데이터베이스가 ACID를 지원해도 애플리케이션이 외부 API 호출과 메시지 발행을 잘못 묶으면 전체 업무의 일관성은 깨질 수 있습니다.
격리 수준이 막는 현상
낮은 격리 수준은 동시 처리량을 높일 수 있지만 다른 트랜잭션의 변화가 조회 결과에 더 많이 보입니다. 높은 수준은 일관된 관점을 제공하지만 충돌과 재시도 가능성이 커질 수 있습니다. 단순 조회, 재고 예약, 정산 마감은 요구하는 일관성이 다릅니다. 시스템 전체에 가장 높은 수준을 일괄 적용하기보다 업무 단위로 허용할 수 없는 현상을 먼저 정의합니다.
결정 전 질문
- 한 트랜잭션 안에서 같은 행을 다시 읽을 때 같은 값이어야 합니까?
- 조회 조건에 맞는 새 행의 등장을 허용할 수 있습니까?
- 두 사용자의 동시 수정 중 하나가 조용히 사라질 수 있습니까?
- 충돌 시 사용자가 재시도해도 안전한 업무입니까?
- 보고서의 일관성과 최신성 중 무엇이 더 중요합니까?
오라클과 SQL Server의 기본 동작 차이
오라클은 다중 버전 읽기 일관성을 활용해 일반 조회가 변경 작업의 잠금에 덜 막히도록 설계돼 있습니다. 기본 READ COMMITTED에서도 문장 시작 시점의 일관된 데이터를 읽습니다. SQL Server의 기본 READ COMMITTED는 환경 설정에 따라 공유 잠금을 사용할 수 있으며, READ_COMMITTED_SNAPSHOT을 켜면 행 버전 기반으로 동작할 수 있습니다. 같은 격리 수준 이름만 보고 두 제품의 대기와 읽기 결과가 같다고 가정하면 안 됩니다.
SQL Server의 NOLOCK 또는 READ UNCOMMITTED를 성능 해결책처럼 붙이면 커밋되지 않은 값, 중복 또는 누락된 행을 읽을 수 있습니다. 정확성이 필요한 주문과 정산에는 적합하지 않습니다. 차단 문제는 긴 트랜잭션, 누락된 인덱스, 지나치게 넓은 조회 범위를 먼저 진단해야 합니다.
핵심 원칙: 잠금을 피하려고 정확성을 포기하기 전에 트랜잭션 시간과 실행 계획, 행 버전 옵션을 먼저 검토해야 합니다.
갱신 손실을 막는 두 가지 방식
비관적 잠금은 데이터를 읽을 때 다른 변경을 막아 충돌을 예방합니다. 충돌이 잦고 작업이 짧을 때 유용하지만 대기와 교착 상태 위험이 있습니다. 낙관적 동시성은 조회한 버전이나 수정 시각을 UPDATE 조건에 포함하고 영향받은 행이 0개면 다른 사용자가 먼저 변경했다고 판단합니다. SQL Server의 rowversion이나 별도 버전 숫자를 사용할 수 있으며 오라클에서도 버전 열을 명시적으로 관리할 수 있습니다.
낙관적 방식에서 충돌이 났을 때 최신 값을 무조건 덮어쓰면 검사의 의미가 없습니다. 사용자에게 변경 내용을 보여 주거나 업무가 자동 병합 가능한지 판단해야 합니다. 재고 차감은 재고 >= 요청수량 조건을 UPDATE에 포함해 검사와 변경을 한 문장으로 수행하는 방법도 효과적입니다.
교착 상태가 생기는 구조
트랜잭션 A가 주문을 잠근 뒤 고객을 기다리고, 트랜잭션 B가 고객을 잠근 뒤 주문을 기다리면 서로 진행하지 못합니다. 데이터베이스는 한쪽을 희생자로 선택해 롤백합니다. 교착 상태는 단순한 서버 고장이 아니라 동시 시스템에서 발생할 수 있는 정상적인 충돌이므로 애플리케이션이 오류를 인식하고 안전하게 재시도해야 합니다.
교착 상태 줄이기
- 모든 코드가 테이블과 행을 같은 순서로 접근합니다.
- 사용자 입력이나 외부 API를 기다리며 트랜잭션을 유지하지 않습니다.
- 적절한 인덱스로 잠그는 행 범위를 줄입니다.
- 한 번에 처리하는 배치 크기를 제한합니다.
- 교착 그래프와 세션 정보를 수집해 실제 자원 순서를 확인합니다.
재시도는 멱등성과 함께 설계하기
교착이나 일시적 연결 오류에 제한적 재시도를 적용할 수 있지만 이미 외부 결제나 메시지 발송이 성공했을 수 있습니다. 업무 요청에 멱등성 키를 부여하고 데이터베이스 결과와 외부 거래 식별자를 함께 기록해야 합니다. 지수 백오프와 최대 횟수를 두고, 제약 조건 위반이나 인증 오류처럼 재시도로 해결되지 않는 실패는 즉시 반환합니다.
트랜잭션 경계와 연결 풀
트랜잭션은 서비스 계층의 하나의 업무 단위에서 시작하고 종료하는 편이 명확합니다. 내부 함수가 임의로 커밋하면 상위 작업이 실패해도 일부 데이터만 남을 수 있습니다. 연결 풀에 반환하기 전에 커밋 또는 롤백 상태를 확정해야 다음 요청이 이전 트랜잭션을 이어받지 않습니다. 예외 처리에서 롤백이 실패할 가능성도 고려해 연결을 폐기할 수 있어야 합니다.
검증 체크리스트
- 같은 행을 동시에 수정하는 테스트를 실행합니다.
- 낙관적 버전 충돌이 정상적으로 감지되는지 확인합니다.
- 접근 순서를 반대로 만들어 교착을 재현합니다.
- 재시도 중 중복 결제와 중복 이벤트가 없는지 검사합니다.
- 오래 열린 트랜잭션과 잠금 대기 시간을 모니터링합니다.
- 오라클과 SQL Server의 격리 설정을 환경별로 기록합니다.
- 장애 후 커밋 여부가 모호한 거래의 확인 절차를 시험합니다.
격리 수준은 높을수록 무조건 좋은 품질 설정이 아닙니다. 업무가 요구하는 일관성, 충돌 빈도, 처리량과 재시도 가능성을 함께 맞추는 선택입니다. 짧은 트랜잭션, 일관된 잠금 순서, 조건부 UPDATE, 멱등성 있는 재시도를 조합하면 오라클과 MS SQL 환경에서 동시성 오류를 훨씬 예측 가능하게 다룰 수 있습니다.
한 줄 요약: 안정적인 DB 동시성은 격리 수준만 높이는 것이 아니라 짧은 트랜잭션과 충돌 감지, 일관된 잠금 순서, 안전한 재시도를 함께 설계해야 합니다.