데이터와 보안 ·

데이터베이스 변경은 트랜잭션으로 한 묶음에 담아야 한다

먼저 답부터

데이터베이스 트랜잭션은 여러 읽기와 변경을 하나의 작업 단위로 묶는 장치입니다. 모든 조건과 쿼리가 성공했을 때만 COMMIT으로 확정하고, 중간에 오류나 조건 불일치가 생기면 ROLLBACK으로 묶음 전체를 취소해야 주문만 생기거나 재고만 줄어드는 반쪽 상태를 막을 수 있습니다.

데이터베이스 변경은 트랜잭션으로 한 묶음에 담아야 한다 대표 이미지

주문을 저장하고 재고를 줄이는 기능은 화면에서 버튼 한 번으로 끝나는 것처럼 보입니다. 데이터베이스 안에서는 주문 행 추가, 재고 수량 변경, 결제 상태 기록처럼 여러 단계가 차례로 실행될 수 있습니다. 앞 단계만 반영되고 뒤 단계가 실패하면 화면은 닫혀도 데이터에는 서로 맞지 않는 기록이 남습니다.

식당의 한 쟁반을 떠올려 보세요. 샐러드와 본요리와 수프를 한 주문으로 내보내기로 했다면 세 접시가 모두 준비됐을 때 쟁반 전체를 손님에게 건넵니다. 한 접시가 빠졌다면 나머지만 보내지 않고 쟁반 전체를 주방으로 돌려보냅니다. 트랜잭션도 여러 데이터 변경을 이런 한 쟁반처럼 다룹니다.

트랜잭션은 여러 변경을 하나의 작업 단위로 묶는다

트랜잭션은 데이터베이스에서 여러 읽기와 쓰기를 하나의 작업 단위로 다루는 방식입니다. PostgreSQL 공식 문서는 트랜잭션의 핵심을 여러 단계를 전부 실행하거나 전부 실행하지 않는 한 묶음으로 설명합니다. 중간 단계는 다른 작업에서 완성된 결과처럼 보이지 않아야 하고, 실패하면 앞에서 실행한 변경도 최종 결과에 남지 않아야 합니다.

예를 들어 상품 재고를 하나 줄였는데 주문 저장이 실패하면 재고만 사라져서는 안 됩니다. 반대로 주문은 생겼는데 재고가 그대로라면 품절 상품을 계속 판매할 수 있습니다. 두 변경은 서로 다른 SQL 문이어도 업무상 하나의 결과라면 같은 트랜잭션 경계를 검토해야 합니다.

BEGIN과 COMMIT은 쟁반을 열고 완성해 건네는 표시다

BEGIN은 지금부터 한 묶음으로 처리할 작업을 시작한다는 표시입니다. 그 안에서 필요한 확인과 변경을 실행하고, 모든 결과가 기대와 맞을 때 COMMIT으로 확정합니다. COMMIT이 성공해야 트랜잭션 안의 변경이 하나의 완료된 결과로 다른 작업에 보입니다.

다음 예시는 재고를 줄이고 주문을 추가하는 구조만 보여 줍니다. 실제 표 이름과 열, 재고 확인 방식은 서비스마다 다르므로 그대로 실행하는 명령이 아닙니다.

BEGIN;

UPDATE inventory
SET stock = stock - 1
WHERE product_id = 42 AND stock > 0;

INSERT INTO orders (product_id, quantity, status)
VALUES (42, 1, 'created');

COMMIT;

중요한 점은 SQL 문이 오류 없이 끝났다는 사실만으로 업무 조건까지 자동으로 맞는 것은 아니라는 것입니다. 재고 변경 결과가 0건이라면 프로그램이 그 결과를 확인하고 주문 추가를 멈춰야 합니다. 트랜잭션은 묶음을 지키지만 무엇을 성공으로 볼지는 서비스 규칙이 정합니다.

ROLLBACK은 실패한 묶음을 확정하지 않고 되돌린다

ROLLBACK은 현재 트랜잭션에서 만든 변경을 버리고 시작 전 상태로 돌아가게 하는 명령입니다. PostgreSQL 공식 문서에서도 ROLLBACK은 현재 트랜잭션을 중단하고 그 안에서 만든 업데이트를 폐기한다고 설명합니다. 재고 부족, 잘못된 주문 값, 쿼리 오류처럼 계속 진행하면 안 되는 조건을 발견했을 때 사용합니다.

BEGIN;
-- 재고 확인과 주문 변경을 시도한다.
-- 조건이 맞지 않으면 아래처럼 전체를 취소한다.
ROLLBACK;

오류가 났다고 모든 데이터베이스와 도구가 같은 방식으로 자동 취소한다고 가정하지 마세요. SQLite 공식 문서는 오류 종류와 상황에 따라 현재 문장만 취소되거나 전체 트랜잭션이 취소될 수 있다고 설명합니다. 사용하는 데이터베이스와 연결 라이브러리의 오류 처리 방식을 확인하고, 실패 뒤 트랜잭션 상태를 명시적으로 정리해야 합니다.

한 SQL의 성공과 한 업무의 성공은 다르다

트랜잭션 안의 각 SQL 문이 실행됐어도 필요한 행을 실제로 찾았는지, 금액과 수량이 허용 범위인지, 같은 요청이 이미 처리됐는지는 별도 확인이 필요합니다. 존재하지 않는 상품을 조건으로 UPDATE했을 때 오류 대신 변경된 행 0건이 반환될 수 있습니다. 이 상태에서 다음 INSERT를 계속하면 문법은 맞아도 업무 결과는 틀릴 수 있습니다.

작업을 시작하기 전에 성공 조건과 중단 조건을 나눠 적습니다.

  • 주문에 필요한 상품이 실제로 존재하는지 확인합니다.
  • 재고 변경 행 수가 기대한 수와 같은지 확인합니다.
  • 금액과 수량이 서버가 허용한 범위인지 확인합니다.
  • 같은 요청 식별자가 이미 처리됐는지 확인합니다.
  • 하나라도 맞지 않으면 COMMIT하지 않고 ROLLBACK합니다.

결제 승인, 메일 발송, 외부 API 호출은 데이터베이스 트랜잭션만으로 함께 되돌릴 수 없습니다. 데이터베이스를 ROLLBACK해도 이미 발송된 메일이나 외부 결제가 사라지는 것은 아니므로, 외부 작업은 중복 방지 식별자와 상태 조회와 별도 취소 절차를 둬야 합니다.

트랜잭션은 짧고 경계가 분명해야 한다

트랜잭션을 오래 열어 둔 채 사람의 입력이나 느린 외부 응답을 기다리면 다른 작업이 필요한 데이터에 접근하거나 변경하는 시간이 길어질 수 있습니다. SQLite 공식 문서처럼 데이터베이스마다 동시에 허용하는 읽기와 쓰기 방식도 다릅니다. 한 데이터베이스의 동작을 다른 제품에 그대로 적용하지 말고 현재 제품의 공식 문서와 연결 설정을 확인하세요.

화면에서 사용자가 결정을 내리는 시간, 결제 제공사의 응답을 기다리는 시간, 파일을 업로드하는 시간을 모두 한 트랜잭션 안에 넣기보다 필요한 데이터베이스 읽기와 변경만 짧게 묶습니다. 잠금 대기나 충돌이 생겼을 때 무한히 다시 시도하지 않고 재시도 횟수와 사용자 안내를 정합니다.

자동 저장과 명시적 트랜잭션을 구분한다

PostgreSQL은 BEGIN을 쓰지 않은 개별 SQL 문도 각각 하나의 트랜잭션처럼 처리합니다. SQLite도 트랜잭션이 열려 있지 않을 때 데이터베이스 접근 문장이 자동으로 트랜잭션을 시작할 수 있습니다. 이를 자동 커밋이라고 부르기도 합니다. 한 문장만 바꾸는 작업에는 편리하지만, 여러 문장을 하나의 업무로 묶어 주지는 않습니다.

사용하는 프레임워크가 BEGIN과 COMMIT을 자동으로 처리할 수도 있습니다. 겉으로 트랜잭션 코드가 보이지 않는다고 보호가 없는 것도 아니고, 함수 이름에 transaction이 있다고 모든 외부 작업까지 보호되는 것도 아닙니다. 연결이 끊겼을 때, 예외가 났을 때, 중첩 호출을 했을 때 실제로 어떤 동작을 하는지 해당 도구 문서와 테스트로 확인합니다.

안전 경계는 시험 데이터와 복구 가능성부터 세운다

트랜잭션을 연습하려고 운영 데이터베이스에서 주문이나 잔액을 바꾸지 않습니다. 먼저 복사한 시험 데이터나 폐기 가능한 로컬 데이터베이스에서 진행하고, 작업 전후의 대표 행과 변경 건수를 비교합니다. DELETE나 대량 UPDATE를 트랜잭션 안에 넣었다는 이유만으로 안전하다고 믿지 마세요. 잘못된 상태로 COMMIT하면 그대로 확정됩니다.

비밀번호, API 키, 데이터베이스 접속 문자열, 실제 고객 정보는 예시나 화면 캡처에 넣지 않습니다. 결제와 환불은 시험 모드인지 확인하고 실제 금액을 사용하지 않습니다. 모르는 명령, 권한 상승, 강제 잠금 해제, 운영 데이터 삭제는 실행하지 말고 담당자와 복구 계획을 먼저 확인합니다.

오늘 바로 해볼 작은 실습

실제 데이터베이스를 바꾸지 않고 10분 동안 종이에 트랜잭션 경계를 그려 봅니다.

  1. 회원가입, 주문, 예약 중 하나의 기능을 고릅니다.
  2. 그 기능이 바꾸는 데이터 항목을 두 개 이상 적습니다.
  3. 함께 성공해야 하는 항목을 하나의 쟁반 테두리로 묶습니다.
  4. 각 변경이 성공했는지 확인할 결과를 적습니다.
  5. 변경 행이 0건일 때 계속할지 중단할지 정합니다.
  6. 모든 조건이 맞을 때만 COMMIT이라고 표시합니다.
  7. 오류나 조건 불일치가 생기면 ROLLBACK이라고 표시합니다.
  8. 메일과 결제처럼 ROLLBACK으로 취소되지 않는 외부 작업을 따로 표시합니다.
  9. 비밀키, 실제 고객 정보, 실결제 값을 적지 않았는지 확인합니다.
  10. 운영 데이터에서 실행하거나 삭제 명령을 시험하지 않습니다.

마지막으로 “어떤 변경이 모두 성공해야 하며, 어느 조건에서 전체를 취소하는가”를 한 문장으로 말해 보세요. 성공 조건과 취소 조건이 분명하지 않다면 SQL을 실행하기 전에 업무 규칙부터 다시 확인하는 것이 안전합니다.

확인한 자료

함께 읽기

  • 환경변수와 API 키를 서랍과 금고처럼 구분하는 법

    환경변수는 주소나 실행 모드처럼 프로그램 밖에서 바꿀 설정을 담는 서랍이고, API 키는 외부 서비스 사용 권한을 가진 비밀번호에 가깝습니다. 비밀키는 브라우저 코드나 Git 저장소에 넣지 말고 서버의 비밀 저장소에서만 읽도록 해야 합니다.

  • 쿠키와 세션과 브라우저 저장소를 보관함 위치로 구분하기

    쿠키는 브라우저가 보관하며 조건에 맞는 서버 요청에 함께 보낼 수 있는 작은 값이고, 세션은 보통 서버가 로그인 상태를 보관하고 쿠키에는 연결용 식별자만 두는 방식입니다. localStorage는 같은 출처의 브라우저에 남지만 요청에 자동 첨부되지 않으므로 화면 설정에는 쓸 수 있어도 비밀키 저장소로 보면 안 됩니다.

  • SQL로 데이터를 만들고 찾고 고치고 지우는 첫 연습

    SQL은 데이터베이스에 말을 거는 문법입니다. INSERT는 새 행을 넣고 SELECT는 조건에 맞는 행을 읽으며 UPDATE는 기존 값을 고치고 DELETE는 행을 지웁니다. 변경 명령은 WHERE 조건이 빠지면 범위가 커질 수 있으므로 같은 조건을 SELECT로 먼저 확인해야 합니다.