BTP

COMMIT 없이 데이터 입력하면 90% 날아가는 이유 #shorts #SAP #HANA

▶ YouTube에서 보기

개요 및 이 글에서 얻어갈 것

온라인 쇼핑몰에서 주문이 들어와 재고를 1개 차감하는 UPDATE를 실행한 직후, 결제 시스템 호출에서 네트워크 오류가 발생했다고 가정해 봅시다. 만약 그 UPDATE가 자동으로 즉시 반영되는 상태였다면 재고는 이미 줄어든 채로 결제는 실패한 모순 상태가 됩니다. 이런 사고를 막는 핵심 도구가 바로 트랜잭션(Transaction)이며, 그 시작과 끝을 결정하는 명령이 COMMITROLLBACK입니다.

이 글을 끝까지 읽으면 다음을 직접 다룰 수 있게 됩니다.

  • SAP HANA에서 트랜잭션이 시작·종료되는 시점과 autocommit 모드의 차이를 구분
  • SalesOrder, OrderItem, StockLevel 테이블을 활용한 COMMIT/ROLLBACK 패턴 작성
  • SAVEPOINT를 이용한 부분 롤백으로 복잡한 비즈니스 로직 안정화
  • ACID 4대 특성과 HANA의 MVCC가 동시성 환경에서 어떻게 데이터 무결성을 지키는지 설명
  • 실무에서 자주 발생하는 락(lock) 이슈와 장시간 미커밋 트랜잭션 진단

읽기 전에 알고 있으면 좋은 것

이 글은 SAP HANA SQL을 처음 접하는 입문자를 대상으로 합니다. 기본적인 SELECT, INSERT, UPDATE, DELETE 구문을 한 번이라도 실행해 본 경험과, 테이블·컬럼·기본키의 개념을 알고 있다면 무리 없이 따라올 수 있습니다. SAP HANA Cockpit이나 SAP HANA Database Explorer 같은 SQL 콘솔에 접속하는 방법, 그리고 본인 스키마에 테이블을 생성할 수 있는 권한이 있는 환경이면 충분합니다.

실습 환경과 준비물

예제는 다음 환경에서 작성·검증된 패턴을 기준으로 합니다. 버전이나 에디션이 다르면 일부 동작에 차이가 있을 수 있으니 자신의 환경에 맞춰 일반적인 권장 사항으로 참고하세요.

  • 데이터베이스: SAP HANA Cloud (2026년 기준 최신 QRC 빌드) 또는 SAP HANA Platform 2.0 SPS 07 이상
  • 접속 도구: SAP HANA Database Explorer, SAP HANA Cockpit, 또는 hdbsql CLI
  • 권한: 본인 스키마에서 CREATE TABLE, INSERT, UPDATE, DELETE 권한
  • 세션 설정: autocommit 모드를 OFF로 변경할 수 있어야 함 (hdbsql의 경우 \\ac OFF)

예제용 테이블은 아래와 같이 세 개로 단순화합니다. 실무에서는 더 많은 컬럼과 제약이 붙겠지만, 트랜잭션 제어를 익히기에는 이 정도면 충분합니다.

CREATE COLUMN TABLE SalesOrder (
    order_id      INTEGER PRIMARY KEY,
    customer_id   INTEGER NOT NULL,
    total_amount  DECIMAL(12,2) NOT NULL,
    order_status  NVARCHAR(20) DEFAULT 'CREATED',
    created_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE COLUMN TABLE OrderItem (
    item_id     INTEGER PRIMARY KEY,
    order_id    INTEGER NOT NULL,
    product_id  INTEGER NOT NULL,
    quantity    INTEGER NOT NULL,
    unit_price  DECIMAL(10,2) NOT NULL
);

CREATE COLUMN TABLE StockLevel (
    product_id     INTEGER PRIMARY KEY,
    on_hand_qty    INTEGER NOT NULL,
    reserved_qty   INTEGER DEFAULT 0
);

핵심 개념 정리

트랜잭션은 "전부 성공하거나, 전부 없던 일로 되돌리거나" 둘 중 하나만을 보장하는 작업 단위입니다. 은행 송금에서 출금과 입금이 한 묶음으로 처리되어야 하는 것처럼, 여러 SQL 문이 비즈니스적으로 하나의 의미를 가진다면 그것이 하나의 트랜잭션이 되어야 합니다.

트랜잭션의 안전성을 떠받치는 네 기둥을 ACID라 부릅니다.

  • Atomicity (원자성): 트랜잭션 내 모든 작업이 전부 적용되거나 하나도 적용되지 않습니다. 중간 상태가 외부에 노출되지 않습니다.
  • Consistency (일관성): 트랜잭션이 끝난 뒤 데이터는 모든 제약 조건과 비즈니스 규칙을 만족합니다.
  • Isolation (격리성): 동시에 실행되는 트랜잭션끼리 서로의 중간 결과를 보지 못합니다.
  • Durability (지속성): 커밋된 변경은 시스템 장애가 나도 사라지지 않습니다.

SAP HANA는 격리성을 효율적으로 구현하기 위해 MVCC(Multi-Version Concurrency Control)를 사용합니다. 같은 행을 누가 수정하더라도 다른 세션은 자신의 트랜잭션이 시작된 시점의 "스냅샷"을 기준으로 데이터를 읽기 때문에, 읽기와 쓰기가 서로를 막지 않습니다. 비유하자면, 도서관에서 누군가 책을 빌려가 수정 중이라도 다른 독자는 원본의 사본을 받아 읽는 것과 비슷합니다. 다만 같은 행을 두 세션이 동시에 UPDATE하려고 하면 한쪽은 락 대기 상태가 됩니다.

HANA의 기본 격리 수준은 일반적으로 READ COMMITTED이며, 필요에 따라 세션 단위로 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 등으로 변경할 수 있습니다.

트랜잭션 종료 명령은 두 가지입니다.

  • COMMIT: 지금까지의 모든 변경을 영구 저장하고 다른 세션에 공개합니다.
  • ROLLBACK: 지금까지의 모든 변경을 폐기하고 트랜잭션 시작 시점으로 되돌립니다.

여기에 더해 SAVEPOINT를 활용하면 트랜잭션 도중 특정 지점까지만 되돌리는 부분 롤백이 가능합니다.

autocommit 모드가 켜져 있으면 SQL 한 줄 한 줄이 즉시 커밋됩니다. 빠르게 조회·실험할 때는 편하지만, 여러 문장이 묶여야 하는 업무 로직에서는 위험합니다. 명시적 트랜잭션 제어를 하려면 autocommit을 OFF로 두고 COMMIT/ROLLBACK으로 직접 마무리해야 합니다.

1단계: 기본 COMMIT과 ROLLBACK

먼저 autocommit을 끄고 가장 단순한 시나리오를 살펴봅니다. 신규 주문 한 건과 그에 딸린 상품 항목 하나를 함께 적재하는 경우입니다.

-- 세션 시작 시 autocommit 해제 (hdbsql 기준)
-- \ac OFF

INSERT INTO SalesOrder (order_id, customer_id, total_amount)
VALUES (10001, 5001, 49900);

INSERT INTO OrderItem (item_id, order_id, product_id, quantity, unit_price)
VALUES (90001, 10001, 7001, 1, 49900);

-- 두 INSERT가 모두 성공했을 때만 영구 반영
COMMIT;

중간에 실수를 발견했다면 어떻게 될까요? 예를 들어 total_amount를 잘못 입력했다고 가정해 봅시다.

INSERT INTO SalesOrder (order_id, customer_id, total_amount)
VALUES (10002, 5002, 9990000);  -- 0이 하나 더 붙은 실수

-- 다른 세션에서 SELECT 해봐도 아직 보이지 않음 (격리성)
-- 실수를 발견하고 되돌리기
ROLLBACK;

-- 이후 다시 올바르게 입력
INSERT INTO SalesOrder (order_id, customer_id, total_amount)
VALUES (10002, 5002, 99900);
COMMIT;

핵심은 COMMIT 전까지는 다른 세션이 변경을 볼 수 없으며, ROLLBACK으로 깔끔하게 취소된다는 점입니다.

2단계: 실무 시나리오 — 주문 생성과 재고 차감을 한 묶음으로

실제 업무에서는 여러 테이블을 동시에 건드립니다. 주문이 생성되면 SalesOrder·OrderItem에 행이 들어가고, 동시에 StockLevel의 재고가 줄어야 합니다. 중간 어느 한 곳이라도 실패하면 전체를 되돌려야 합니다.

DO BEGIN
    DECLARE v_order_id      INTEGER := 10003;
    DECLARE v_customer_id   INTEGER := 5003;
    DECLARE v_product_id    INTEGER := 7002;
    DECLARE v_quantity      INTEGER := 3;
    DECLARE v_unit_price    DECIMAL(10,2) := 12500;
    DECLARE v_on_hand       INTEGER;

    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        -- 호출자에게 의미 있는 메시지 전달
        SIGNAL SQL_ERROR_CODE 10001
            SET MESSAGE_TEXT = 'Order creation failed, transaction rolled back';
    END;

    -- 재고 확인 (FOR UPDATE로 동시성 충돌 방지)
    SELECT on_hand_qty INTO v_on_hand
    FROM StockLevel
    WHERE product_id = :v_product_id
    FOR UPDATE;

    IF :v_on_hand < :v_quantity THEN
        SIGNAL SQL_ERROR_CODE 10002
            SET MESSAGE_TEXT = 'Insufficient stock';
    END IF;

    INSERT INTO SalesOrder (order_id, customer_id, total_amount)
    VALUES (:v_order_id, :v_customer_id, :v_unit_price * :v_quantity);

    INSERT INTO OrderItem (item_id, order_id, product_id, quantity, unit_price)
    VALUES (90003, :v_order_id, :v_product_id, :v_quantity, :v_unit_price);

    UPDATE StockLevel
    SET on_hand_qty = on_hand_qty - :v_quantity
    WHERE product_id = :v_product_id;

    COMMIT;
END;

여기서 주목할 부분은 세 가지입니다. 첫째, EXIT HANDLER로 예외 발생 시 자동 ROLLBACK이 보장됩니다. 둘째, FOR UPDATE로 재고 행에 락을 걸어 다른 세션이 같은 상품을 동시에 차감하는 경합을 방지합니다. 셋째, 마지막 COMMIT은 모든 단계가 성공했을 때만 도달합니다.

3단계: 프로덕션 — SAVEPOINT와 부분 롤백

주문 한 건에 여러 상품을 담는 경우, 일부 상품이 품절이라면 그 항목만 건너뛰고 나머지는 정상 처리하고 싶을 수 있습니다. 이때 SAVEPOINT가 유용합니다.

DO BEGIN
    DECLARE v_order_id INTEGER := 10010;

    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SIGNAL SQL_ERROR_CODE 10099
            SET MESSAGE_TEXT = 'Fatal error during multi-item order';
    END;

    INSERT INTO SalesOrder (order_id, customer_id, total_amount)
    VALUES (:v_order_id, 5010, 0);

    -- 첫 번째 아이템
    SAVEPOINT sp_item_1;
    BEGIN
        DECLARE EXIT HANDLER FOR SQLEXCEPTION
        BEGIN
            ROLLBACK TO SAVEPOINT sp_item_1;
            -- 이 아이템만 건너뛰고 계속
        END;

        INSERT INTO OrderItem VALUES (91001, :v_order_id, 7010, 2, 8900);
        UPDATE StockLevel SET on_hand_qty = on_hand_qty - 2
        WHERE product_id = 7010 AND on_hand_qty >= 2;

        IF SQL%ROWCOUNT = 0 THEN
            SIGNAL SQL_ERROR_CODE 10003 SET MESSAGE_TEXT = 'Stock low';
        END IF;
    END;

    -- 두 번째 아이템도 같은 패턴
    SAVEPOINT sp_item_2;
    BEGIN
        DECLARE EXIT HANDLER FOR SQLEXCEPTION
        BEGIN
            ROLLBACK TO SAVEPOINT sp_item_2;
        END;

        INSERT INTO OrderItem VALUES (91002, :v_order_id, 7011, 1, 19900);
        UPDATE StockLevel SET on_hand_qty = on_hand_qty - 1
        WHERE product_id = 7011 AND on_hand_qty >= 1;

        IF SQL%ROWCOUNT = 0 THEN
            SIGNAL SQL_ERROR_CODE 10003 SET MESSAGE_TEXT = 'Stock low';
        END IF;
    END;

    -- 실제 처리된 합계로 주문 금액 보정
    UPDATE SalesOrder s
    SET total_amount = (
        SELECT IFNULL(SUM(quantity * unit_price), 0)
        FROM OrderItem WHERE order_id = s.order_id
    )
    WHERE s.order_id = :v_order_id;

    COMMIT;
END;

이 패턴은 결제·외부 API 호출 같은 멱등성이 중요한 작업과 결합하면 더 강력해집니다. 외부 호출은 트랜잭션 밖에서 처리하고, DB 상태만 SAVEPOINT 단위로 관리하는 식입니다. 또한 운영 환경에서는 다음 사항을 권장합니다.

  • 트랜잭션 길이 최소화: 사용자 입력을 기다리는 동안 트랜잭션을 열어두지 않기
  • 로깅: COMMIT/ROLLBACK 경로마다 애플리케이션 로그를 남겨 사후 추적 가능하게 하기
  • 권한 분리: 트랜잭션을 수행하는 기술 사용자와 조회 전용 사용자를 분리
  • 모니터링: M_TRANSACTIONS, M_BLOCKED_TRANSACTIONS 뷰로 장기 트랜잭션과 락 대기 감시

자주 부딪히는 실수와 해결책

Q1. COMMIT을 잊고 세션을 닫았는데 데이터가 사라졌어요.
명시적 트랜잭션에서 COMMIT 없이 세션이 종료되면 일반적으로 자동 ROLLBACK됩니다. 즉, 변경은 폐기됩니다. 배치 스크립트라면 마지막에 반드시 COMMIT을 호출하고, 애플리케이션이라면 finally 블록에서 커밋·롤백 분기를 명확히 구현해야 합니다.

Q2. 같은 행을 동시에 UPDATE하다가 한 세션이 무한 대기에 빠집니다.
HANA는 행 단위 락을 사용하므로, 한 트랜잭션이 행을 잠근 채 COMMIT/ROLLBACK 하지 않으면 다른 세션은 계속 대기합니다. M_BLOCKED_TRANSACTIONS 뷰에서 블로킹 주체를 찾아 해당 세션을 분석하고, 필요 시 ALTER SYSTEM DISCONNECT SESSION로 정리합니다. 근본 해결은 트랜잭션 길이를 짧게 유지하고, 락 타임아웃(transaction_lock_wait_timeout) 설정으로 무한 대기를 차단하는 것입니다.

Q3. autocommit이 켜져 있는지 모르고 ROLLBACK 했더니 아무 효과가 없습니다.
autocommit ON 상태에서는 각 SQL이 즉시 커밋되므로, 그 뒤의 ROLLBACK은 되돌릴 대상이 없어 무의미합니다. 운영용 SQL 스크립트나 데이터 보정 작업은 반드시 세션 시작 시 autocommit을 OFF로 설정한 뒤 진행하세요. JDBC라면 connection.setAutoCommit(false)가 같은 역할을 합니다.

Q4. DDL(예: CREATE TABLE) 직후 ROLLBACK 했는데 테이블이 그대로 있어요.
HANA에서 DDL은 일반적으로 암묵적 커밋을 동반합니다. 따라서 DDL 앞뒤의 DML과 함께 묶어 ROLLBACK하려는 시도는 의도대로 동작하지 않을 수 있습니다. 스키마 변경과 데이터 변경 트랜잭션은 분리해 운영하는 편이 안전합니다.

이어서 공부하면 좋은 주제

트랜잭션 제어를 익혔다면 다음 주제로 확장해 보세요. 첫째, 격리 수준(READ COMMITTED, REPEATABLE READ, SERIALIZABLE)의 차이와 팬텀 리드·논리피터블 리드 시나리오를 직접 재현해 보면 동시성 감각이 크게 늘어납니다. 둘째, 분산 트랜잭션과 XA는 여러 데이터 소스를 묶어야 하는 마이크로서비스 환경에서 필수입니다. 셋째, HANA Stored Procedure와 예외 처리 패턴을 깊이 보면 위 3단계 코드를 더 견고하게 다듬을 수 있습니다. 마지막으로 CAP(SAP Cloud Application Programming) 모델의 트랜잭션 모델은 애플리케이션 레이어에서 HANA 트랜잭션을 어떻게 추상화하는지 보여줍니다.

더 읽어볼 자료

댓글 0

아직 댓글이 없습니다.