모든 이야기

출금만 되고 입금은 안 되는 일을 막는 트랜잭션

트랜잭션은 여러 데이터베이스 작업을 하나의 논리적인 실행 단위로 묶는 방식이다. 업무가 요구하는 변경들이 함께 성공하거나 함께 취소되어야 할 때 경계를 만든다. SQL 한 문장을 빠르게 실행하는 기술이라기보다 실패와 동시성 속에서도 어떤 상태를 최종 결과로 인정할지 정하는 개념이다.

두 계좌의 이체 변경이 함께 커밋되거나 오류 때 함께 원래 잔액으로 돌아가는 그림

트랜잭션은 함께 성립해야 하는 데이터 변경의 성공과 취소 경계를 만든다.

두 변경이 하나의 업무가 될 때

계좌 A에서 B로 30을 옮기는 이체를 생각해 보자. 처음 잔액이 A=100, B=50이라면 출금 뒤 A=70이 되고 입금 뒤 B=80이 된다. 출금만 반영한 상태에서 작업이 끝나면 전체 금액이 줄어든다. 두 변경을 한 트랜잭션에 넣으면 중간 오류가 발생했을 때 둘을 함께 취소할 수 있다.

BEGIN은 트랜잭션의 시작을, COMMIT은 변경을 성공 결과로 확정할 의사를 나타낸다. ROLLBACK은 변경을 취소한다. 실제 문법과 자동 커밋 설정은 시스템과 클라이언트에 따라 확인해야 한다. 자동 커밋 모드에서는 각 문장이 별도 트랜잭션이 될 수 있으므로 문장 두 개를 연속으로 실행했다는 사실만으로 같은 단위가 되지는 않는다.

이체에는 잔액이 충분한지 확인하고 음수 잔액을 막는 규칙도 필요하다. 트랜잭션이 잘못 작성된 출금 금액이나 잘못된 대상 계좌를 자동으로 올바르게 고치지는 않는다. 유효한 입력과 제약, 동시성 처리, 오류 처리 코드가 함께 업무의 정확성을 만든다.

중간 상태를 누가 볼 수 있을까

다른 사용자가 동시에 잔액을 읽거나 수정하면 작업 순서가 중요해진다. 격리 수준과 잠금 또는 버전 관리에 따라 중간 상태의 관찰과 충돌 처리가 달라진다. 트랜잭션 안에 넣었다고 모든 동시 실행이 가장 강한 격리 수준으로 자동 처리되는 것은 아니다. 실제 설정과 접근 방법을 확인해야 한다.

한 연결에서 실행한 트랜잭션이 다른 연결의 작업까지 임의로 묶어 주지도 않는다. 업무에서 여러 서비스나 데이터 저장소를 함께 변경하면 로컬 DB 트랜잭션의 범위를 넘어갈 수 있다. 외부 결제 API 호출, 이메일 발송이나 파일 생성은 DB 롤백으로 자동 취소되지 않는 경우가 많다. 이런 부수 효과는 업무의 실패 처리와 별도로 설계해야 한다.

트랜잭션을 오래 열어 둔 채 사용자 입력이나 원격 응답을 기다리면 잠금과 오래된 스냅샷, 연결 자원을 오랫동안 유지할 수 있다. 짧은 경계로 필요한 변경을 묶는 것이 일반적으로 유리하지만, 너무 잘게 나누면 함께 지켜야 할 불변식을 잃을 수 있다. 업무의 원자적 단위와 자원 사용을 함께 고려해야 한다.

실패 시에는 원인을 구분해야 한다. 데이터 제약 위반은 입력이나 업무 규칙을 수정해야 할 수 있고, 직렬화 충돌은 트랜잭션 전체를 다시 시도하는 정책이 필요할 수 있다. 일부 문장만 다시 실행하면 이전 판단이 더는 유효하지 않을 수 있다. 재시도는 외부 부수 효과의 중복 가능성도 함께 고려한다.

커밋 응답을 받기 전에 연결이 끊기면 결과를 모르는 상황이 생길 수 있다. 서버에서는 커밋했지만 응답만 유실되었을 가능성을 포함한다. 요청 식별자로 결과를 조회하고 중복을 처리하는 업무 설계가 필요한 이유다. 트랜잭션의 경계를 이해한다는 것은 성공과 실패 사이의 모든 관찰 상태까지 고려하여 업무의 약속을 만드는 것이다.

참고 자료