BTP

트랜잭션 없이 DB 수정하면 생기는 문제 3가지 #shorts #SAP #HANA

▶ YouTube에서 보기

이 글에서 다루는 것

SAP HANA를 비롯한 모든 관계형 데이터베이스에서 데이터를 안전하게 변경하려면 트랜잭션(Transaction) 개념을 반드시 이해해야 합니다. 이 글은 SQL의 COMMITROLLBACK을 활용해 트랜잭션을 제어하는 방법을 실무 시나리오 중심으로 설명합니다. 특히 트랜잭션을 사용하지 않고 데이터를 직접 수정했을 때 발생하는 데이터 불일치 문제와, ACID 원칙이 이를 어떻게 해결하는지를 단계별로 짚어 봅니다.

  • COMMIT과 ROLLBACK의 정확한 동작 시점 파악
  • Auto-Commit 모드와 명시적 트랜잭션의 차이 구분
  • 주문/재고 시나리오에서 ROLLBACK으로 일관성 복구
  • SAVEPOINT를 활용한 부분 롤백 전략 습득
  • SAP HANA에서 트랜잭션 격리 수준이 갖는 의미 이해

이 글을 읽기 전 알아두면 좋은 것

기본적인 SQL INSERT, UPDATE, DELETE 구문에 익숙해야 합니다. 또한 SAP HANA Studio 또는 SAP HANA Database Explorer로 SQL 콘솔에 접속해 명령을 실행해 본 경험이 있으면 따라오기 수월합니다. 스키마와 테이블 생성 권한도 필요합니다.

실습 환경 및 준비물

이 글의 예제는 다음 환경을 기준으로 작성되었습니다.

  • SAP HANA Cloud (QRC 2/2026) 또는 SAP HANA 2.0 SPS 07 이상
  • SAP HANA Database Explorer 또는 SAP HANA Studio 2.3.x
  • 샘플 스키마 생성 권한이 있는 데이터베이스 사용자
  • SQL Console에서 Auto-Commit 옵션을 켜고 끌 수 있는 환경

SAP HANA Database Explorer에서는 우측 상단 톱니바퀴 아이콘을 통해 Auto-Commit을 On/Off로 전환할 수 있고, HANA Studio에서는 SQL Console 우측 상단의 토글 버튼으로 같은 설정을 변경할 수 있습니다. 예제에서는 별도 언급이 없으면 Auto-Commit이 꺼진 상태를 가정합니다.

핵심 개념: 트랜잭션이 보장하는 네 가지 약속

트랜잭션은 "하나의 작업 단위로 묶인 여러 SQL 문장의 집합"입니다. 은행에서 A 계좌의 100만 원을 B 계좌로 이체한다고 가정해 봅시다. 두 개의 UPDATE 문이 필요한데, A 계좌에서 100만 원이 빠진 직후 시스템 오류가 발생해 B 계좌 입금이 실행되지 않는다면 돈이 사라지는 셈이 됩니다. 트랜잭션은 이 두 동작을 "전부 성공 또는 전부 실패"로 묶어 이런 사고를 막아줍니다.

이 약속을 ACID라는 네 가지 속성으로 정리합니다.

  • Atomicity(원자성): 트랜잭션 안의 모든 변경은 전부 반영되거나 전부 취소됩니다. 중간 상태는 외부에 노출되지 않습니다.
  • Consistency(일관성): 트랜잭션 시작 전·종료 후 데이터베이스는 항상 정의된 제약(예: 외래키, NOT NULL, CHECK)을 만족합니다.
  • Isolation(격리성): 동시에 실행되는 트랜잭션끼리 서로의 중간 결과를 보지 않습니다. SAP HANA는 기본적으로 Read Committed 격리 수준을 사용합니다.
  • Durability(지속성): COMMIT이 완료된 변경은 시스템 장애가 발생해도 사라지지 않습니다. HANA는 redo log를 디스크에 영구 기록해 이 속성을 지킵니다.

COMMIT은 "지금까지의 변경을 확정해 디스크에 영구 반영하라"는 명령이고, ROLLBACK은 "마지막 COMMIT 시점 또는 트랜잭션 시작 시점으로 모든 변경을 되돌리라"는 명령입니다. 두 명령 중 하나가 실행되는 순간 현재 트랜잭션은 종료되고, 다음 DML이 실행되면 새로운 트랜잭션이 자동으로 시작됩니다.

비유하자면 COMMIT은 워드프로세서의 "저장" 버튼, ROLLBACK은 "마지막 저장 후 모든 작업 취소" 버튼에 가깝습니다. 단, 다른 사용자도 같은 문서를 보고 있다면 저장하기 전까지는 내 수정 내용이 보이지 않는다는 점이 일반 문서 편집과 다릅니다.

1단계 예제 — 기본 COMMIT과 ROLLBACK 흐름

먼저 주문 헤더 테이블 하나를 만들고, COMMIT과 ROLLBACK의 동작을 눈으로 확인해 보겠습니다. Auto-Commit은 반드시 꺼 놓으세요.

CREATE COLUMN TABLE SALES_ORDER_HEADER (
    ORDER_ID      NVARCHAR(10) PRIMARY KEY,
    CUSTOMER_ID   NVARCHAR(10) NOT NULL,
    TOTAL_AMOUNT  DECIMAL(15,2) NOT NULL,
    ORDER_STATUS  NVARCHAR(20) DEFAULT 'OPEN',
    CREATED_AT    TIMESTAMP    DEFAULT CURRENT_TIMESTAMP
);

-- 첫 번째 트랜잭션: 두 건을 넣고 확정
INSERT INTO SALES_ORDER_HEADER (ORDER_ID, CUSTOMER_ID, TOTAL_AMOUNT)
VALUES ('SO-1001', 'CUST-01', 125000.00);

INSERT INTO SALES_ORDER_HEADER (ORDER_ID, CUSTOMER_ID, TOTAL_AMOUNT)
VALUES ('SO-1002', 'CUST-02', 89000.00);

COMMIT;

-- 두 번째 트랜잭션: 입력 후 되돌리기
INSERT INTO SALES_ORDER_HEADER (ORDER_ID, CUSTOMER_ID, TOTAL_AMOUNT)
VALUES ('SO-1003', 'CUST-03', 4500000.00);

SELECT COUNT(*) FROM SALES_ORDER_HEADER;  -- 결과: 3건

ROLLBACK;

SELECT COUNT(*) FROM SALES_ORDER_HEADER;  -- 결과: 2건

같은 세션에서는 COMMIT 전이라도 본인의 변경 결과가 보입니다. 그러나 다른 세션에서 동일한 SELECT를 실행하면 COMMIT 전까지는 SO-1003이 보이지 않습니다. 이것이 격리성이 동작하는 방식입니다. ROLLBACK 후에는 본인 세션에서도 SO-1003이 사라집니다.

2단계 예제 — 주문/재고 동시 변경에서 에러 시 ROLLBACK

실제 업무 코드는 단일 INSERT가 아니라 여러 테이블을 동시에 갱신합니다. 주문을 받으면 재고 테이블을 차감하고 주문 라인을 기록해야 하는 시나리오를 살펴보겠습니다. 도중에 재고 부족이 발생하면 전체를 ROLLBACK해 데이터 일관성을 유지합니다.

CREATE COLUMN TABLE PRODUCT_STOCK (
    PRODUCT_ID  NVARCHAR(10) PRIMARY KEY,
    QTY_ON_HAND INTEGER NOT NULL,
    CHECK (QTY_ON_HAND >= 0)
);

CREATE COLUMN TABLE SALES_ORDER_ITEM (
    ORDER_ID   NVARCHAR(10),
    LINE_NO    INTEGER,
    PRODUCT_ID NVARCHAR(10),
    QUANTITY   INTEGER NOT NULL,
    PRIMARY KEY (ORDER_ID, LINE_NO)
);

INSERT INTO PRODUCT_STOCK VALUES ('P-001', 50);
INSERT INTO PRODUCT_STOCK VALUES ('P-002', 3);
COMMIT;

아래는 SQLScript 프로시저로 트랜잭션을 캡슐화한 모습입니다. EXIT HANDLER로 예외를 잡아 ROLLBACK을 실행합니다.

CREATE OR REPLACE PROCEDURE PLACE_ORDER (
    IN  IV_ORDER_ID   NVARCHAR(10),
    IN  IV_CUSTOMER   NVARCHAR(10),
    IN  IV_PRODUCT    NVARCHAR(10),
    IN  IV_QTY        INTEGER,
    IN  IV_UNIT_PRICE DECIMAL(15,2),
    OUT OV_RESULT     NVARCHAR(50)
) LANGUAGE SQLSCRIPT AS
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        OV_RESULT := 'FAILED: ' || ::SQL_ERROR_MESSAGE;
    END;

    INSERT INTO SALES_ORDER_HEADER (ORDER_ID, CUSTOMER_ID, TOTAL_AMOUNT)
    VALUES (:IV_ORDER_ID, :IV_CUSTOMER, :IV_QTY * :IV_UNIT_PRICE);

    INSERT INTO SALES_ORDER_ITEM (ORDER_ID, LINE_NO, PRODUCT_ID, QUANTITY)
    VALUES (:IV_ORDER_ID, 10, :IV_PRODUCT, :IV_QTY);

    UPDATE PRODUCT_STOCK
       SET QTY_ON_HAND = QTY_ON_HAND - :IV_QTY
     WHERE PRODUCT_ID  = :IV_PRODUCT;

    COMMIT;
    OV_RESULT := 'OK';
END;

이제 재고가 3개뿐인 P-002 상품을 5개 주문하면 CHECK 제약(QTY_ON_HAND >= 0)이 실패하고, EXIT HANDLER가 ROLLBACK을 호출해 헤더와 아이템 INSERT까지 모두 취소됩니다.

CALL PLACE_ORDER('SO-2001', 'CUST-09', 'P-002', 5, 12000.00, ?);
-- 결과: FAILED: check constraint violation ...

SELECT * FROM SALES_ORDER_HEADER WHERE ORDER_ID = 'SO-2001';  -- 0건
SELECT * FROM SALES_ORDER_ITEM   WHERE ORDER_ID = 'SO-2001';  -- 0건
SELECT QTY_ON_HAND FROM PRODUCT_STOCK WHERE PRODUCT_ID = 'P-002'; -- 여전히 3

만약 트랜잭션 처리를 하지 않았다면 헤더와 아이템은 들어갔는데 재고만 차감되지 않아, 매출 보고서와 재고 보고서가 어긋나는 데이터 불일치가 발생합니다. 이런 상황을 디버깅하려고 야근하는 일이 줄어드는 것이 트랜잭션의 진짜 가치입니다.

3단계 예제 — SAVEPOINT, 격리 수준, 운영 환경 고려사항

여러 단계로 구성된 배치 작업에서 일부만 되돌리고 싶다면 SAVEPOINT를 사용합니다. 또한 운영 환경에서는 동시 접근에 따른 락 경합을 줄이기 위해 트랜잭션을 가능한 한 짧게 유지하고, 적절한 격리 수준을 선택해야 합니다.

-- 대량 주문 마이그레이션 중 특정 고객만 부분 롤백
INSERT INTO SALES_ORDER_HEADER (ORDER_ID, CUSTOMER_ID, TOTAL_AMOUNT)
VALUES ('SO-3001', 'CUST-A', 100000);

SAVEPOINT SP_AFTER_A;

INSERT INTO SALES_ORDER_HEADER (ORDER_ID, CUSTOMER_ID, TOTAL_AMOUNT)
VALUES ('SO-3002', 'CUST-B', 250000);

INSERT INTO SALES_ORDER_HEADER (ORDER_ID, CUSTOMER_ID, TOTAL_AMOUNT)
VALUES ('SO-3003', 'CUST-B', -1);  -- 잘못된 데이터

-- B 고객의 작업만 되돌리고 A 고객 데이터는 유지
ROLLBACK TO SAVEPOINT SP_AFTER_A;

COMMIT;  -- SO-3001만 확정

SAP HANA는 기본 격리 수준이 Read Committed이지만, 분석 보고서를 만드는 동안 데이터가 바뀌면 곤란한 경우 Repeatable Read 또는 Serializable로 격상할 수 있습니다.

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

SELECT SUM(TOTAL_AMOUNT) AS MONTHLY_SALES
  FROM SALES_ORDER_HEADER
 WHERE CREATED_AT >= ADD_MONTHS(CURRENT_DATE, -1);

COMMIT;  -- 분석이 끝나면 즉시 종료해 락 해제

운영 코드에 적용할 때 권장되는 패턴은 다음과 같습니다.

  • 트랜잭션은 최대한 짧게 유지하고, 사용자 입력 대기 같은 외부 I/O를 포함하지 않습니다.
  • 트랜잭션 안에서 발생할 수 있는 모든 예외를 catch해 ROLLBACK을 호출하고, 호출자에게 명확한 오류 메시지를 반환합니다.
  • JDBC/ODBC 클라이언트는 기본이 Auto-Commit인 경우가 많으므로, 애플리케이션 초기화 시 conn.setAutoCommit(false)로 명시 전환합니다.
  • 장시간 idle 트랜잭션은 다른 세션의 락을 잡고 있을 수 있으므로 모니터링 뷰 M_TRANSACTIONS로 주기적으로 점검합니다.

자주 마주치는 실수와 해결 방법

Q1. ROLLBACK을 했는데 데이터가 그대로 남아 있어요. 가장 흔한 원인은 Auto-Commit이 켜져 있어 매 SQL이 즉시 확정되는 경우입니다. 클라이언트 도구의 Auto-Commit 설정을 확인하고, JDBC라면 setAutoCommit(false)를 호출하세요. 또한 DDL(CREATE, ALTER, DROP)은 SAP HANA에서 암묵적 COMMIT을 유발하므로 DDL 직전에 실행된 DML도 함께 확정됩니다.

Q2. 트랜잭션을 길게 열어두니 다른 세션이 멈춥니다. UPDATE/DELETE는 행 잠금을 발생시키고 COMMIT/ROLLBACK까지 유지됩니다. M_BLOCKED_TRANSACTIONS 뷰로 차단 사슬을 확인하고, 사용자 응답 대기 같은 로직을 트랜잭션 밖으로 분리하세요. 필요하다면 SELECT ... FOR UPDATE NOWAIT로 즉시 실패하도록 처리합니다.

Q3. 프로시저 내부에서 COMMIT을 호출해도 되나요? 가능하지만 권장되지는 않습니다. 호출자가 트랜잭션 경계를 제어하지 못하기 때문입니다. 일반적으로 트랜잭션의 시작과 종료는 가장 바깥의 호출자(애플리케이션 계층 또는 최상위 프로시저)에서 책임지는 것이 안전합니다.

Q4. ROLLBACK이 시퀀스 값까지 되돌리지는 않습니다. 시퀀스(SEQUENCE)는 트랜잭션과 독립적으로 동작하므로 ROLLBACK 후에도 다음 NEXTVAL은 이미 증가한 값을 반환합니다. 빈 번호가 생기는 것은 정상 동작이며, 연속 번호가 필요하다면 별도의 번호 채번 테이블을 설계하세요.

이어서 살펴보면 좋은 주제

트랜잭션 제어를 익혔다면 다음 단계에서는 동시성 제어와 성능 최적화로 시야를 넓혀가는 것이 자연스럽습니다.

  • SAP HANA의 MVCC(Multi-Version Concurrency Control) 동작 원리와 가비지 컬렉션
  • SELECT ... FOR UPDATE를 활용한 비관적 락 처리
  • 분산 트랜잭션과 XA 프로토콜, 그리고 Saga 패턴
  • RAP(ABAP RESTful Application Programming Model)에서 트랜잭션 버퍼와 Save Sequence
  • M_TRANSACTIONS, M_BLOCKED_TRANSACTIONS 등 모니터링 뷰 활용

댓글 0

아직 댓글이 없습니다.