UI5

OData 호출 90%가 낭비 — $batch 묶기 #shorts #SAP #UI5

▶ YouTube에서 보기

개요 및 핵심 포인트

SAP Fiori/UI5 애플리케이션이 복잡해질수록 화면 하나에서 발생하는 OData 호출 수가 빠르게 늘어납니다. 주문 목록, 고객 정보, 배송 상태, 첨부 파일을 각각 별도의 HTTP 요청으로 보낸다면 네트워크 왕복 비용과 서버 세션 부담이 모두 누적됩니다. 이때 OData 스펙이 제공하는 $batch 엔드포인트를 활용하면 여러 요청을 하나의 HTTP 요청으로 묶어 처리할 수 있습니다. 이 글은 SAPUI5 1.108 이상(LTS) 기준으로 ODataModel v2와 v4 양쪽에서 배치 호출을 구성하는 방법을 다룹니다.

  • $batch 요청의 multipart/mixed 구조 이해
  • ODataModel v2의 groupId + submitChanges 흐름 익히기
  • ODataModel v4의 submitBatch + $auto/$direct 그룹 구분
  • Deferred 그룹으로 전송 타이밍 직접 제어
  • ChangeSet의 원자성(Atomicity) 범위와 트랜잭션 경계 파악
  • 부분 실패 시 에러 추적과 사용자 피드백 처리

먼저 알고 있어야 하는 것

이 글은 advanced 난이도로 작성되었습니다. OData v2/v4 메타데이터 구조, SAPUI5 Model/Binding 개념, Promise 기반 비동기 처리에 익숙해야 합니다. Fiori Elements가 아닌 Freestyle UI5에서 ODataModel을 직접 다뤄본 경험이 있다면 이해가 빠릅니다.

실습 환경과 준비물

  • SAPUI5 SDK: 1.108 LTS 이상
  • 백엔드: OData v2(SAP Gateway 또는 CAP @odata.v2) 또는 OData v4(CAP/RAP)
  • 개발 도구: SAP Business Application Studio 또는 VS Code + ui5-tooling
  • 샘플 엔티티: SalesOrders, Products, Invoices

핵심 개념 정리

$batch는 한 마디로 "택배 합포장"입니다. 작은 박스 10개를 따로 보내는 대신 큰 박스 하나에 담아 한 번에 운송하는 셈입니다. HTTP 레벨에서는 POST /sap/opu/odata/sap/ZSALES_SRV/$batch로 단일 요청을 보내고, 본문은 multipart/mixed 형식으로 여러 하위 요청을 담습니다.

두 가지 단위 구분이 중요합니다.

  • Batch: 전체 요청 묶음. 읽기(GET)와 쓰기(POST/PUT/MERGE/DELETE)를 함께 담을 수 있는 최상위 컨테이너입니다.
  • ChangeSet: Batch 안에 들어가는 "원자적 변경 묶음". ChangeSet 내부에서는 하나라도 실패하면 전부 롤백됩니다. GET 요청은 ChangeSet에 들어가지 못하고 Batch 직속으로 배치됩니다.

UI5의 groupId 개념은 이 ChangeSet/Batch를 추상화한 것입니다. 같은 groupId로 변경을 누적하다가 submitChanges(v2) 또는 submitBatch(v4)를 호출하는 순간 한 번의 $batch 요청이 발사됩니다.

1단계 — 기본 패턴: v2 모델로 두 개 읽기 묶기

onInit: function () {
    var oModel = this.getOwnerComponent().getModel();
    oModel.setDeferredGroups(["orderInit"]);
    this._loadInitialData("4711");
},

_loadInitialData: function (sOrderId) {
    var oModel = this.getView().getModel();

    oModel.read("/SalesOrders('" + sOrderId + "')", {
        groupId: "orderInit",
        urlParameters: { "$expand": "ShipToParty" }
    });

    oModel.read("/SalesOrders('" + sOrderId + "')/Items", {
        groupId: "orderInit",
        urlParameters: { "$top": "50" }
    });

    // 두 read 호출이 하나의 $batch 로 묶여 전송된다
    oModel.submitChanges({
        groupId: "orderInit",
        success: function (oData) {
            console.log("배치 응답 수신", oData);
        },
        error: function (oError) {
            console.error("배치 전송 실패", oError);
        }
    });
}

네트워크 탭을 열어보면 두 개의 GET 대신 한 개의 POST .../$batch만 보입니다. setDeferredGroups에 등록되지 않은 그룹은 즉시 전송되므로, 묶음 효과를 보려면 반드시 deferred 등록이 선행되어야 합니다.

2단계 — 실무: 변경과 읽기를 한 배치에 담기

onApproveOrder: function () {
    var oModel = this.getView().getModel();
    var sOrderId = this.getView().getBindingContext().getProperty("OrderId");

    oModel.setDeferredGroups(["approveFlow"]);
    oModel.setChangeGroups({
        "SalesOrder": { groupId: "approveFlow", changeSetId: "csHeader", single: false },
        "Invoice":    { groupId: "approveFlow", changeSetId: "csInvoice", single: false }
    });

    // 헤더 상태 변경 (ChangeSet csHeader)
    oModel.update("/SalesOrders('" + sOrderId + "')", {
        OrderStatus: "APPROVED"
    }, { groupId: "approveFlow" });

    // 송장 초안 생성 (ChangeSet csInvoice — 헤더 실패와 독립)
    oModel.create("/Invoices", {
        OrderId: sOrderId,
        InvoiceType: "DRAFT"
    }, { groupId: "approveFlow" });

    // 변경 후 최신 상태 재조회 (GET 은 ChangeSet 밖)
    oModel.read("/SalesOrders('" + sOrderId + "')", {
        groupId: "approveFlow",
        urlParameters: { "$expand": "Items,ShipToParty" }
    });

    oModel.submitChanges({
        groupId: "approveFlow",
        success: this._onBatchSuccess.bind(this),
        error:   this._onBatchError.bind(this)
    });
},

_onBatchSuccess: function (oData) {
    var aResponses = (oData && oData.__batchResponses) || [];
    aResponses.forEach(function (oResp) {
        if (oResp.response && oResp.response.statusCode >= "400") {
            sap.base.Log.warning("배치 내부 응답 실패", oResp.response.body);
        }
    });
    sap.m.MessageToast.show("승인이 적용되었습니다.");
}

success 콜백이 호출되어도 내부 응답이 실패했을 수 있습니다. __batchResponses를 순회하여 statusCode를 확인해야 정확한 부분 실패 처리가 가능합니다.

3단계 — v4 모델에서 submitBatch와 그룹 전략

manifest.json에 그룹 속성을 정의합니다.

{
  "sap.ui5": {
    "models": {
      "": {
        "settings": {
          "groupProperties": {
            "orderApprove": { "submit": "API" },
            "lazyDetail":   { "submit": "Auto" }
          }
        }
      }
    }
  }
}
onConfirmShipment: async function () {
    var oModel = this.getView().getModel();
    var oOrderCtx = this.getView().getBindingContext();

    try {
        // 헤더 업데이트 — API 그룹에 누적
        oOrderCtx.setProperty("ShipmentStatus", "READY", "orderApprove");

        // 송장 생성
        var oInvList = this.byId("invoiceTable").getBinding("items");
        oInvList.create({
            OrderId: oOrderCtx.getProperty("OrderId"),
            Amount:  oOrderCtx.getProperty("NetAmount"),
            Status:  "OPEN"
        }, false, false, true);

        // 명시적으로 배치 전송
        await oModel.submitBatch("orderApprove");
        sap.m.MessageToast.show("배송 처리 완료");

    } catch (oError) {
        sap.base.Log.error("submitBatch 실패", oError);
        sap.m.MessageBox.error(oError.message || "처리에 실패했습니다.");
    }
}

흔한 실수와 트러블슈팅

Q1. submitChanges를 호출했는데 요청이 두 개로 분리됩니다.
setDeferredGroups에 그룹을 등록하지 않은 경우입니다. 등록되지 않은 그룹은 변경 직후 즉시 전송됩니다.

Q2. ChangeSet 안에서 한 건이 실패했는데 다른 건도 롤백됐습니다.
ChangeSet의 정의된 동작입니다. 독립적으로 처리해야 한다면 setChangeGroups로 ChangeSet ID를 분리하세요.

Q3. v4에서 $direct 그룹에 update를 누적했더니 매번 요청이 발사됩니다.
$direct는 묶음을 만들지 않습니다. 묶음이 필요하면 submit: "API" 또는 "Auto" 그룹을 사용하세요.

Q4. 배치를 쓰니까 오히려 느려졌습니다.
너무 많은 요청(수십 개 이상)을 한 배치에 담으면 서버가 직렬 처리하면서 단일 요청 시간이 길어질 수 있습니다. 화면 단위로 5~15개 정도가 일반적인 균형점입니다.

이어서 살펴보면 좋은 주제

  • v4의 deepCreate 패턴 — 마스터-디테일 단일 트랜잭션 저장
  • CAP Draft 시나리오와 $batch 결합 — 임시저장 UX 구성
  • RAP 액션 호출과 배치의 결합 패턴
  • OPA5 MockServer — 배치 응답 시뮬레이션 테스트

더 읽을거리

댓글 0

아직 댓글이 없습니다.