3 minute read

ACID 에서 A(Atomicity)와 I(Isolation)은 하나의 클라이언트가 하나의 트랜잭션 안에서 여러번의 쓰기 연산을 수행할 때 데이터베이스는 어떤 행동을 해야하는지를 설명한다.

  • 원자성은 데이터베이스에서 발생하는 부분적인 실패에 대해 걱정할 필요가 없도록 해준다.
  • 격리는 동시에 실행 중인 트랜잭션은 서로 간섭하지 않게 해준다.

단일 객체 쓰기

단일 객체를 변경할 때도 원자성과 격리가 적용된다.

예를 들어 데이터베이스에 20KB의 JSON 문서를 쓴다고 가정하자.

  • 처음 10KB가 전송된 후 네트워크 연결이 중단되면 데이터베이스는 분석할 수 없는 10KB의 JSON 조각을 저장할까?
  • 데이터베이스가 디스크의 이전 값을 덮어쓰는 도중에 정전이 발생하면 이전 값과 새 값이 합쳐진 상태로 끝나게 되는가?
  • 쓰기가 진행되는 동안 다른 클라이언트가 해당 문서를 읽으면 부분적으로 업데이트된 값이 표시되는가?

스토리지 엔진은 보편적으로 하나의 노드에서 단일 객체(예: 키-값 쌍) 수준에서 원자성과 격리성을 제공하는 것을 목표로 한다. 원자성은 크래시 복구를 위한 로그를 사용하여 구현할 수 있으며, 격리는 각 객체에 대한 잠금을 사용하여 구현할 수 있다.

이러한 단일 객체 연산은 여러 클라이언트가 동일한 객체에 동시에 쓰기를 시도할 때 업데이트 손실을 방지할 수 있으므로 유용하다. 하지만 일반적인 의미의 트랜잭션은 아니다. 트랜잭션은 일반적으로 여러 객체에 대한 여러 연산을 하나의 실행 단위로 그룹화하는 메커니즘으로 이해되는 것이 옳다.

다중 오브젝트에 트랜잭션이 필요한 이유

많은 분산 데이터스토어가 다중 오브젝트 트랜잭션을 포기한 이유는 파티션 간에 구현하기 어렵고, 매우 높은 가용성이나 성능이 필요한 일부 시나리오에서 방해가 될 수 있기 때문이다.

다중 객체 트랜잭션이 반드시 필요할까? 키-값 데이터 모델과 단일 객체 연산만으로 모든 애플리케이션을 구현할 수 있을까?

다중 객체 트랜잭션은 외래 키가 최신을 유지하게 한다.

관계형 데이터 모델에서는 한 테이블의 행에 다른 테이블의 행에 대한 외래 키 참조가 있는 경우가 많다. 다중 객체 트랜잭션을 사용하면 이러한 참조가 유효한 상태로 유지되도록 할 수 있다. 서로를 참조하는 여러 레코드를 삽입할 때 외래 키가 정확하고 최신 상태여야 하며, 그렇지 않으면 데이터가 무의미해진다.

문서 데이터 모델은 여러 문서를 한 번에 업데이트 한다.

문서 데이터 모델에서는 함께 업데이트해야 하는 필드가 동일한 문서 내에 있는 경우가 많으며, 이는 단일 개체로 취급되므로 단일 문서를 업데이트할 때 다중 개체 트랜잭션이 필요하지 않다. 그러나 조인 기능이 없는 문서 데이터베이스는 비정규화를 권장하기도 한다. 비정규화된 정보를 업데이트해야 하는 경우 여러 문서를 한 번에 업데이트해야 한다. 이 상황에서 트랜잭션은 정규화된 데이터가 동기화되지 않는 것을 방지하는 데 매우 유용하다

보조 인덱스를 관리하려면 대중 객체 트랜잭션이 필요하다.

보조 인덱스가 있는 데이터베이스(순수한 키-값 저장소를 제외한 거의 모든 데이터베이스가 이에 속함)에서는 값을 변경할 때마다 인덱스도 업데이트해야 한다. 이러한 인덱스는 트랜잭션 관점에서 볼 때 서로 다른 데이터베이스 객체이다. 예를 들어 트랜잭션 격리 기능이 없는 경우, 두 번째 인덱스에 대한 업데이트가 아직 이루어지지 않았기 때문에 레코드가 한 인덱스에는 나타나지만 다른 인덱스에는 나타나지 않을 수 있다.

이러한 애플리케이션은 트랜잭션 없이도 구현할 수 있다. 하지만, 원자성이 없으면 오류 핸들링이 훨씬 더 복잡해지고 격리 부족으로 인해 동시성 문제가 발생한다.

오류 및 중단 처리

트랜잭션의 핵심 기능은 오류가 발생하면 트랜잭션을 중단하고 안전하게 재시도할 수 있다는 것이다. 데이터베이스가 원자성, 격리 또는 내구성 보장을 위반할 위험이 있는 경우, 트랜잭션을 반쯤 완료된 상태로 두는 것보다 완전히 중단하는 것이 좋다.

오류는 필연적으로 발생하지만 많은 소프트웨어 개발자는 오류 처리의 복잡성보다는 행복한 경로만 생각하는 것을 선호한다.

예를 들어, Rails의 ActiveRecord나 Django와 같은 객체 관계형 매핑(ORM) 프레임워크의 경우

  • 중단된 트랜잭션을 다시 시도하지 않는다.
  • 오류로 인해 일반적으로 스택에 예외가 발생하여 모든 사용자 입력이 버려지고 사용자에게 오류 메시지가 표시된다.

중단된 트랜잭션을 다시 시도하는 것은 간단하고 효과적인 오류 처리 메커니즘이지만 완벽하지는 않다.

트랜잭션 성공, 네트워크 실패의 경우

  • 트랜잭션이 실제로는 성공했지만 서버가 클라이언트에 성공적인 커밋을 확인하려고 하는 동안 네트워크가 실패한 경우, 트랜잭션을 다시 시도하면 추가적인 애플리케이션 수준의 중복 제거 메커니즘이 없는 한 트랜잭션이 두 번 수행된다.

과부하로 인해 트랜잭션을 다시 시도할 경우

  • 과부하로 인한 오류인 경우 트랜잭션을 다시 시도하면 문제가 개선되는 것이 아니라 악화된다. 이러한 피드백 주기를 피하려면 재시도 횟수를 제한하고, 지수 백오프를 사용하고, 과부하 관련 오류를 다른 오류와 다르게 처리할 수 있다.

일시적 오류와 영구적 오류

  • 일시적인 오류(예: 교착 상태, 규격 위반, 일시적인 네트워크 중단 및 장애 조치)가 발생한 후에만 재시도할 가치가 있으며, 영구적인 오류(예: 제약 조건 위반)가 발생한 후에는 재시도가 무의미합니다.

데이터베이스 외부에 부수 효과가 있는 경우

  • 트랜잭션이 데이터베이스 외부에도 사이드 이펙트가 있는 경우 트랜잭션이 중단되더라도 이러한 부작용이 발생할 수 있다. 예를 들어, 이메일을 보내는 경우 트랜잭션을 다시 시도할 때마다 이메일을 다시 보내고 싶지 않을 것이다. 여러 다른 시스템이 함께 커밋하거나 중단하도록 하려면 2단계 커밋이 도움이 될 수 있다.

클라이언트 재시도 실패

  • 클라이언트 프로세스가 재시도하는 동안 실패하면 데이터베이스에 쓰려고 했던 모든 데이터가 손실된다.