본문 바로가기
Node.js

24/09/18 - Node.js 숙련(2 - 2): 트랜잭션(Transaction)

by Jini_Lamp 2024. 9. 18.

트랜잭션(Transaction)은 작업의 완전성을 보장해주기 위해 사용되는 개념이다. 특정한 작업을 전부 처리하거나, 전부 실패하게 만들어 데이터의 일관성을 보장해주는 역할을 한다.

이를 사용하는 이유는 작업의 단위를 하나의 쿼리에 종속하는 것이 아닌, 여러 개의 작업(쿼리)을 하나의 작업 단위로 그룹화하여 처리하는 것이다.

 

예를 들어, A 고객이 B 고객에게 1000원을 보내는 상황이 있다. 이를 좀 더 세분화 한다면 다음과 같다.

  1. A 고객의 계좌에서 1000원을 차감한다.
  2. B 고객의 계좌에 1000원을 추가한다.

여기서 1번 작업 이후, 2번 작업을 수행하던 중 문제가 발생하게 되면, A 고객의 계좌에 1000원만 차감되는 문제가 발생하게 된다.

이런 부분 업데이트(Partial Update)와 같은 상황을 방지하기 위해 트랜잭션이라는 개념이 생겨났다. 위의 예제처럼, 둘 중 하나라도 문제가 생기면 다시 처음부터 시작하거나, 둘 모두 문제 없이 성공했을 경우만 데이터 변경을 허용하는 걸 트랜잭션이라고 한다.

이처럼 트랜잭션을 이용한다면 사용자가 항상 어플리케이션 실행을 완료하도록 구성할 수 있게되고, 실행을 중단할 만한 치명적인 오류가 발생하더라도 DB에 피해가 가지 않아 더욱 안전하게 어플리케이션을 구성할 수 있게 된다.

 

 

 

트랜잭션의 특징

ACID는 트랜잭션을 이용해 DB를 더욱 안전하게 구성할 수 있게 도와주는 트랜잭션의 4가지 특징 맨 앞 단어를 하나씩 가져와 만든 것이다.

  • 원자성(Atomicity)
    트랜잭션 내에서 실행되는 명령들을 하나의 묶음으로 처리하여, 내부에서 실행된 명령들이 전부 성공하거나, 전부 실패해야한다. 여기서 원자성은 나눠질 수 없는 단일 작업을 의미한다.

    이전 은행 입출금 예제처럼, 계좌 금액 차감과 금액 증가를 하나의 계좌이체라는 기능으로 묶어서 관리할 수 있다.
    이처럼 원자성이라는 특징을 이용해 각각의 쿼리를 별도로 실행하는 게 아니라, 동시에 실행해햐하는 여러 개의 쿼리를 묶어서 관리할 수 있다. 그렇게 되면 여러 개의 비즈니스 로직을 상세하게 알지 않더라도 트랜잭션을 활용하여 비즈니스 로직을 전체적으로 관리할 수 있다.

  • 일관성(Consistency)
    트랜잭션 내부에서 처리되는 데이터의 일관성을 유지해야하는 특징이다. 즉, 에러가 발생하더라도 데이터의 상태가 일관성을 유지해야 한다. 즉, 작업이 성공한다면 아무런 문제가 생기지 않고, 실패하더라도 실패한 상태로 데이터를 방치하지 않는다.

    좀 더 쉽게 설명하자면, 위에서 다룬 은행 입출금 예제처럼 2번 부분에서 문제가 발생했을 경우, ROLLBACK 되어 1번도 없었던 일이 되어야 한다. 즉, 트랜잭션 시작 전 상태로 복구된다.
    만약 일관성이 지쳐지지 않는다면 언제 데이터가 파손될지 모르는 불안감을 가진 채 작업을 해야한다.

  • 격리성(Isolation)
    트랜잭션이 실행 중인 경우 다른 트랜잭션에 의해 데이터가 변경되는 걸 방지한다. 즉, 트랜잭션이 완전히 수행되거나 수행되지 않은 상태를 외부에서 참조할 수는 있지만, 트랜잭션의 중간 과정이나 중간 결과를 볼 수 없도록 한다.

    MySQL에서는 사용중인 DB 오브젝트에 락(Lock)을 걸어 격리성을 구현한다. 여기서 락을 건 상태는 DB에 접속한 또다른 클라이언트가 해당 DB 오브젝트를 읽거나, 사용할 수 없도록 방지하여, 데이터 무결성을 보장하게 된다.
    • 동시성(Concurrency)
      여러 클라이언트가 동시에 하나의 데이터를 사용 & 공유하는 걸 뜻한다. 다수의 사용자가 동일한 시스템을 공유하면서 발생하는 동시 접근 문제를 해결하는 과정 중 하나이며, 이런 문제를 해결하기 위해선 하나의 클라이언트만 해당 자원을 점유할 수 있도록 하여, 다른 사용자가 접근할 수 없도록 만들어 자원을 공유하는 원인을 제거한다. 이것을 자원 잠금(Resource Locking)이라고 하며, 락(Lock)이라는 개념이 나오게 되었다.
락(Lock)은 하나의 트랜잭션에서 사용 중인 데이터를 잠그는 방식으로, 다른 트랜잭션들이 그 데이터에 접근하지 못하도록 한다. 이 방식을 사용한다면 어떤 트랜잭션도 다른 트랜잭션의 중간 상태를 볼 수 없게 되므로 데이터 일관성을 유지할 수 있다.

하나의 데이터를 여러 사용자들이 동시에 변경하려고 할 때, 락이 존재하지 않는다면 한번에 여러 수정이 발생하게 되고, 최종 수정도니 결과값을 인지할 수 없게 되는 상황이 된다. 그렇게 되면 DB의 일관성이 깨지게 된다. 이처럼 락은 동시성을 제어하기 위해 사용되며, 필요한 데이터를 점유하여 다른 트랜잭션의 접근을 막아 동시성과 일관성의 균형을 맞추기 위해 사용된다.

 

  • 지속성(Durability)
    트랜잭션이 성공적으로 커밋된 후, 해당 트랜잭션에 의해 생성/수정된 데이터가 어떠한 상황에서도 보존되는 특징이다. 즉, 트랜잭션이 완료되면 결과는 DB에 영구적으로 저장되며, 이후 시스템에 어떠한 문제가 생기더라도 데이터는 손상되지 않는다.

    이처럼 지속성은 트랜잭션의 안전성을 보장하며, 데이터 손실 없이 시스템의 안정성을 유지하는데 중요한 역할을 담당한다. 또한 트랜잭션 수행 도중 시스템이 비정상 종료되더라도, 시스템은 트랜잭션 로그를 통해 아직 커밋되지 않은 트랜잭션을 복구할 수 있다.

 

 

 

락의 종류

  • 공유/읽기(Shared/Read) 락
    다른 트랜잭션이 데이터를 읽는 건 허용하지만, 수정하는 건 금지한다.
    해당 락을 사용하는 트랜잭션이 모든 작업을 수행했다면 공유 락은 해제된다.

  • 배타/쓰기(Exclusive/Write) 락
    다른 트랜잭션이 데이터를 읽거나 수정하는 걸 금지한다.
    트랜잭션이 필요한 데이터를 점유한 후 다른 트랜잭션이 해당 데이터에 접근할 수 없도록 한다.

 

 

 

락킹 수준

  • 글로벌/DB 락
    DB의 모든 테이블에 락을 걸어, 현재 트랜잭션을 제외한 나머지 트랜잭션들이 모든 테이블을 사용할 수 없도록 만든다.
    가장 높은 수준의 락을 가지고 있으며, 가장 큰 범위를 가지고 있다.

  • 테이블 락
    다른 사용자가 작업중인 테이블을 동시에 수정하지 못하도록 한다.

  • 네임드 락
    테이블이나 테이블 행과 같은 DB 오브젝트가 아닌, 특정한 문자열을 점유한다.

  • 메타데이터 락
    다른 사용자가 작업중인 테이블의 동일한 행 및 동일한 DB의 객체를 동시에 수정하지 못하도록 한다.

 

락은 다양한 락킹 수준을 가지고 있다. 그러나 잘못된 락 설정을 할 경우, 모든 API가 동작하지 않는 교착 상태(Dead Lock)가 발생되어 프로그램이 멈춰버리는 문제가 발생할 수 있다.

교착 상태(Dead Lock)
여러 테이블에 락을 적용하여, 다른 작업이 처리되지 못하게 점유하고 있는 작업이 있을 때, 다른 작업이 끝나길 무한정 기다리는 걸 나타낸다.

예를 들어, 두 트랜잭션이 1개 이상의 데이터를 점유하고, 서로 상대의 데이터를 원하고 있을 때, 이미 상대가 먼저 데이터를 점유하고 있기 때문에 상대가 끝나기 전까지는 어떠한 것도 할 수 없는 상태가 된다. 그러나 상대 역시 내가 갖고 있는 데이터가 있어야만 다음 행동을 할 수 있으므로, 결국 끝없는 기다림만 남을 뿐이다.

이런 상황을 방지하기 위해 트랜잭션에서 사용하는 락의 수준을 명확히 이해하고, 적재적소에 필요한 락 수준을 설정하여 트랜잭션을 구성해야 한다.

 

 

 

트랜잭션의 격리 수준(Isolation Level)

여러 트랜잭션이 동시에 처리될 때 다른 트랜잭션에서 변경 및 조회하는 데이터를 읽을 수 있도록 허용하거나 거부하는 걸 결정하기 위해 사용된다.

여기서 중요한 점은 데이터의 일관성동시성 처리 성능 사이에서 균형을 잡는 것이다. 트랜잭션의 격리 수준은 대표적으로 네 가지로 나타낸다.

  • READ UNCOMMITTED
    • 커밋 되지 않은 읽기(Uncommitted Read)를 허용하는 격리 수준
    • 가장 낮은 수준의 격리수준이며, 락을 걸지 않아 동시성이 높지만 일관성이 쉽게 깨질 수 있다.
  • READ COMMITTED
    • 커밋 된 읽기(Committed Read)만을 허용하고, SELECT 문을 실행할 때 공유락을 건다.
    • 다른 트랜잭션이 데이터를 수정하고 있는 중에는 데이터를 읽을 수 없어 커밋되지 않은 읽기현상이 발생하지 않는다.
  • REPEATABLE READ
    • 읽기를 마치더라도 공유락풀지 않으며, 트랜잭션이 완전히 종료될 때 까지 락을 유지한다.
    • 공유락이 걸린 상태에서 데이터를 수정하는 것은 불가능하지만, 데이터를 삽입하는 것이 가능해진다. 그로인해 팬텀 읽기가 발생할 수 있는 문제점이 있다.
  • SERIALIZABLE
    • 데이터를 읽는 동안 다른 트랜잭션이 해당 데이터를 읽거나 삽입할 수 없고, 새로운 데이터를 추가하는 것 또한 불가능하다.
    • 가장 높은 수준의 격리 수준이므로, 동시성이 떨어지는 문제점이 존재한다.
커밋되지 않은 읽기(Uncommitted Read)는 다른 트랜잭션에 의해 작업중인 데이터를 읽게 되는 것을 나타냅니다. 만약 커밋되지 않은 읽기가 발생할 경우, 의도치 않은 데이터를 참조하게 되어 데이터의 일관성이 깨지게 되는 상황이 발생하게됩니다.
팬텀 읽기(Phantom Read)
트랜잭션을 수행하던 중 다른 트랜잭션에 의해 삭제된 데이터를 팬텀행(Phantom Rows)이라고 합니다. 여기서, 팬텀행에 해당하는 데이터를 읽는 것을 팬텀 읽기(Phantom Read)라고 부릅니다.