[Nexacro N] 트러블슈팅: 이중 트랜잭션

Nexacro N에서 버튼 더블클릭이나 이벤트 중복 호출로 트랜잭션이 두 번 실행되는 문제를 진단하고 방지하는 방법을 설명합니다. 락 플래그 패턴, 버튼 비활성화 패턴을 다룹니다.

· 4 min read · PALDYN Team

지난 글에서 폼 미표시 문제를 다루었다. 이번에는 데이터 정합성에 직결되는 이중 트랜잭션 문제를 살펴본다. 사용자가 저장 버튼을 빠르게 두 번 클릭하거나, 네트워크가 느려 응답이 늦을 때 같은 트랜잭션이 두 번 실행되는 현상이다.

문제 발생 구조

이중 트랜잭션은 아주 간단한 이유로 발생한다. 첫 번째 클릭에서 트랜잭션을 호출하고 응답을 기다리는 동안, 두 번째 클릭이 들어오면 또 트랜잭션이 호출된다.

이중 트랜잭션 발생 타임라인

결과적으로 서버에 같은 요청이 두 번 전달되고, 데이터가 두 번 저장되거나 삭제될 수 있다. 또는 콜백 함수가 두 번 실행되어 예상치 못한 화면 동작이 발생한다.

방지 패턴 1: 락 플래그

가장 범용적인 방법이다. 트랜잭션 시작 시 플래그를 true로 설정하고, 콜백에서 false로 해제한다.

이중 트랜잭션 방지 패턴

var g_isProcessing = false;

function fn_save() {
    if (g_isProcessing) {
        gfn_logWarn("저장 중 중복 호출 차단. 무시.");
        return;
    }
    g_isProcessing = true;

    this.transaction(
        "save",
        "/api/user/save",
        "dsInput=dsUser",
        "",
        "",
        "fn_saveCb"
    );
}

function fn_saveCb(svcId, errCode, errMsg) {
    g_isProcessing = false; // 반드시 콜백에서 해제

    if (errCode != 0) {
        gfn_alert(errMsg);
        return;
    }
    gfn_alert("저장이 완료되었습니다.");
}

주의: fn_saveCb의 에러 케이스에서도 반드시 g_isProcessing = false를 실행해야 한다. 에러 시 해제를 빠뜨리면 그 이후 저장이 영구적으로 막힌다.

방지 패턴 2: 버튼 비활성화

트랜잭션 진행 중 버튼 자체를 비활성화해서 추가 클릭을 막는 방법이다. 사용자가 버튼 상태를 시각적으로 확인할 수 있어 UX에도 좋다.

function fn_save() {
    btn_save.set_enable(false); // 버튼 비활성화

    this.transaction(
        "save", "/api/user/save",
        "dsInput=dsUser", "", "", "fn_saveCb"
    );
}

function fn_saveCb(svcId, errCode, errMsg) {
    btn_save.set_enable(true); // 콜백에서 재활성화

    if (errCode != 0) {
        gfn_alert(errMsg);
        return;
    }
    gfn_alert("저장이 완료되었습니다.");
}

두 패턴의 조합 사용

락 플래그와 버튼 비활성화를 함께 쓰면 더 안전하다.

function fn_setProcessingState(bProcessing) {
    g_isProcessing = bProcessing;
    btn_save.set_enable(!bProcessing);
    btn_delete.set_enable(!bProcessing);

    // 로딩 인디케이터 토글 (선택)
    div_loading.set_visible(bProcessing);
}

function fn_save() {
    if (g_isProcessing) return;
    fn_setProcessingState(true);

    this.transaction("save", url,
        "dsInput=dsUser", "", "", "fn_saveCb");
}

function fn_saveCb(svcId, errCode, errMsg) {
    fn_setProcessingState(false);
    if (errCode != 0) { gfn_alert(errMsg); return; }
    gfn_alert("저장 완료");
}

공통 함수로 래핑

프로젝트 전체에서 동일한 패턴을 강제하려면 트랜잭션 래퍼를 공통 함수로 만든다.

// gfn_common.xjs
function gfn_transaction(svcId, url, inParam, outParam, args, cbFunc) {
    if (g_isProcessing) {
        gfn_logWarn("트랜잭션 중복 차단: " + svcId);
        return;
    }
    g_isProcessing = true;

    // 콜백을 래핑해서 자동으로 플래그 해제
    var wrappedCb = svcId + "_wrapped";
    this[wrappedCb] = function(sid, ec, em) {
        g_isProcessing = false;
        if (cbFunc) this[cbFunc](sid, ec, em);
    };

    this.transaction(svcId, url, inParam, outParam, args, wrappedCb);
}

이중 트랜잭션 탐지

기존 코드에서 이중 트랜잭션이 발생하는지 확인하려면 로그를 추가한다.

function fn_save() {
    gfn_logDebug("fn_save 호출 시각: " + gfn_getSysDateTime());
    // ...
}

같은 시각에 콜이 두 번 찍힌다면 이중 호출이 발생 중인 것이다.


지난 글: 트러블슈팅: 폼 미표시

다음 글: 트러블슈팅: 빈 Dataset


읽어주셔서 감사합니다. 😊