SAPUI5 OData V4 바인딩 완전 가이드 — ListBinding, ContextBinding, $batch 처리까지

Moderator
SAPUI5 OData V4 바인딩 완전 가이드

1. 개요 -- OData V4 바인딩이란 무엇인가

SAPUI5의 OData V4 모델(sap.ui.model.odata.v4.ODataModel)은 OData V4 프로토콜을 기반으로 백엔드 서비스와 프론트엔드 UI를 연결하는 데이터 바인딩 레이어입니다. OData V2 모델과 비교했을 때 근본적인 아키텍처 차이가 있으며, V4는 보다 표준 지향적이고 성능 최적화에 유리한 설계를 갖추고 있습니다.

V2와 V4의 핵심 차이

이 가이드에서는 ListBinding(컬렉션 바인딩), ContextBinding(단일 엔티티 바인딩), PropertyBinding(개별 프로퍼티 바인딩)의 세 가지 바인딩 타입과 $batch 처리, 그리고 실무에서 자주 사용하는 고급 패턴까지 단계별로 다룹니다.

2. 환경 설정 -- manifest.json OData V4 모델 설정

SAPUI5에서 OData V4 모델을 사용하려면 manifest.json에 데이터 소스와 모델을 올바르게 선언해야 합니다. 아래는 일반적인 설정 예시입니다.

{
  "sap.app": {
    "dataSources": {
      "mainService": {
        "uri": "/sap/opu/odata4/sap/api_product/srvd_a2x/sap/product/0001/",
        "type": "OData",
        "settings": {
          "odataVersion": "4.0"
        }
      }
    }
  },
  "sap.ui5": {
    "models": {
      "": {
        "dataSource": "mainService",
        "settings": {
          "synchronizationMode": "None",
          "operationMode": "Server",
          "autoExpandSelect": true
        }
      }
    }
  }
}

주요 설정 항목 설명

속성설명
odataVersion"4.0"으로 지정해야 V4 모델이 생성됩니다. 누락하면 V2로 처리될 수 있습니다.
synchronizationModeV4에서는 "None"만 지원됩니다. 이 값을 명시적으로 설정하는 것을 권장합니다.
operationMode"Server"(기본값)는 필터/정렬을 서버에 위임합니다. "Client"는 클라이언트에서 처리하지만 대량 데이터에는 부적합합니다.
autoExpandSelecttrue로 설정하면 뷰에 바인딩된 프로퍼티만 $select에 자동 포함됩니다. 성능 최적화에 매우 유효합니다.

SAP BTP 환경에서 CAP(Cloud Application Programming Model) 백엔드를 사용하는 경우, uri는 일반적으로 /odata/v4/catalog/와 같은 형태가 됩니다. SAPUI5 버전은 1.110 이상을 권장하며, 최신 V4 기능을 모두 활용하려면 1.120+ 버전이 적합합니다.

3. ListBinding -- 테이블/리스트에 데이터 바인딩

ODataListBinding은 엔티티 컬렉션(예: /Products)을 UI 컨트롤의 어그리게이션(aggregation)에 바인딩할 때 사용됩니다. 테이블(sap.m.Table), 리스트(sap.m.List) 등의 items 어그리게이션이 대표적입니다.

XML View에서 ListBinding 선언

<Table id="productTable"
  items="{
    path: '/Products',
    parameters: {
      $count: true,
      $orderby: 'ProductName',
      $filter: 'Category eq 1',
      $select: 'ProductID,ProductName,Price',
      $updateGroupId: 'productUpdates'
    }
  }">
  <columns>
    <Column><Text text="ID" /></Column>
    <Column><Text text="상품명" /></Column>
    <Column><Text text="가격" /></Column>
  </columns>
  <items>
    <ColumnListItem>
      <Text text="{ProductID}" />
      <Text text="{ProductName}" />
      <ObjectNumber number="{Price}" unit="KRW" />
    </ColumnListItem>
  </items>
</Table>

쿼리 파라미터 상세

JavaScript에서 동적 필터/정렬 적용

// 컨트롤러 내부
onFilterProducts: function () {
  var oList = this.byId("productTable").getBinding("items");

  // 필터 적용
  oList.filter([
    new sap.ui.model.Filter("Category", sap.ui.model.FilterOperator.EQ, "Electronics"),
    new sap.ui.model.Filter("Price", sap.ui.model.FilterOperator.GT, 100)
  ]);

  // 정렬 적용
  oList.sort([
    new sap.ui.model.Sorter("Price", true) // true = descending
  ]);
},

onResetFilter: function () {
  var oList = this.byId("productTable").getBinding("items");
  oList.filter([]);
  oList.sort([]);
}

filter()sort()를 호출하면 V4 모델은 자동으로 새로운 서버 요청을 발생시킵니다. V2와 달리 별도의 refresh() 호출이 필요하지 않습니다.

4. ContextBinding -- 단일 엔티티 바인딩

ODataContextBinding은 단일 엔티티(예: /Products(1))를 컨트롤에 바인딩할 때 사용됩니다. 상세 화면(Detail View), 편집 폼 등에서 주로 활용됩니다.

XML View에서 ContextBinding

<Panel id="productDetail" binding="{/Products(1)}">
  <f:SimpleForm editable="true">
    <Label text="상품명" />
    <Input value="{ProductName}" />
    <Label text="가격" />
    <Input value="{Price}" type="Number" />
    <Label text="카테고리" />
    <Text text="{Category}" />
  </f:SimpleForm>
</Panel>

JavaScript에서 동적 바인딩

라우팅 이벤트에서 엔티티 키를 받아 동적으로 바인딩하는 패턴이 실무에서 가장 일반적입니다.

_onRouteMatched: function (oEvent) {
  var sProductId = oEvent.getParameter("arguments").productId;
  var sPath = "/Products(" + sProductId + ")";

  this.getView().bindElement({
    path: sPath,
    parameters: {
      $select: "ProductID,ProductName,Price,Category",
      $expand: "Supplier"
    },
    events: {
      dataReceived: function (oEvent) {
        // 데이터 수신 후 처리
        if (!oEvent.getParameter("data")) {
          // 엔티티가 존재하지 않는 경우 처리
          sap.m.MessageBox.error("상품을 찾을 수 없습니다.");
        }
      }
    }
  });
},

onSaveProduct: function () {
  // ContextBinding의 변경사항은 모델에 자동 반영
  // $updateGroupId가 설정된 경우 submitBatch로 전송
  var oModel = this.getView().getModel();
  oModel.submitBatch("productUpdates").then(function () {
    sap.m.MessageToast.show("저장 완료");
  });
}

requestObject()로 데이터 읽기

바인딩된 컨텍스트에서 프로그래밍 방식으로 데이터를 읽으려면 requestObject()를 사용합니다. 이 메서드는 Promise를 반환하므로 비동기로 처리해야 합니다.

var oBinding = this.getView().byId("productDetail").getElementBinding();
oBinding.getBoundContext().requestObject().then(function (oData) {
  console.log("상품명:", oData.ProductName);
  console.log("가격:", oData.Price);
});

5. PropertyBinding -- 개별 프로퍼티 바인딩 심화

ODataPropertyBinding은 스칼라 값 하나를 UI 컨트롤의 단일 속성에 바인딩합니다. XML View에서 {ProductName}처럼 작성하면 내부적으로 PropertyBinding이 생성됩니다.

타입 변환과 포맷

V4 모델은 OData의 Edm 타입을 SAPUI5 내부 타입으로 자동 변환합니다. 예를 들어 Edm.Decimalsap.ui.model.odata.type.Decimal로 매핑됩니다. 이를 활용하면 추가 타입 지정 없이도 적절한 유효성 검사와 포맷이 적용됩니다.

<!-- 명시적 타입 지정 예시 -->
<Input value="{
  path: 'Price',
  type: 'sap.ui.model.odata.type.Decimal',
  constraints: { scale: 2 }
}" />

<!-- 날짜 타입 -->
<DatePicker value="{
  path: 'CreatedAt',
  type: 'sap.ui.model.odata.type.DateTimeOffset',
  formatOptions: { style: 'medium' }
}" />

PropertyBinding은 상위 ContextBinding이나 ListBinding의 컨텍스트 내에서 상대 경로로 동작합니다. 절대 경로(/Products(1)/ProductName)를 사용할 수도 있지만, 일반적으로 상위 바인딩의 컨텍스트를 활용하는 상대 경로 방식을 권장합니다.

6. $batch 처리 -- 자동/수동 배치 관리

OData V4 모델은 여러 요청을 하나의 HTTP $batch 요청으로 묶어 전송합니다. 이를 통해 네트워크 왕복 횟수를 줄이고 트랜잭션 일관성을 확보할 수 있습니다.

배치 그룹의 개념

수동 배치 패턴

// 1. 바인딩에 커스텀 updateGroupId 지정 (XML 또는 JS)
// XML: $updateGroupId: 'saveGroup'

// 2. 사용자가 여러 필드를 수정 (Input 바인딩을 통해 자동으로 변경 추적)

// 3. 저장 버튼 클릭 시 일괄 전송
onSave: function () {
  var oModel = this.getView().getModel();

  // hasPendingChanges로 변경사항 존재 여부 확인
  if (!oModel.hasPendingChanges("saveGroup")) {
    sap.m.MessageToast.show("변경사항이 없습니다.");
    return;
  }

  oModel.submitBatch("saveGroup").then(function () {
    sap.m.MessageToast.show("저장이 완료되었습니다.");
  }, function (oError) {
    sap.m.MessageBox.error("저장 중 오류가 발생했습니다: " + oError.message);
  });
},

// 4. 변경 취소
onCancel: function () {
  this.getView().getModel().resetChanges("saveGroup");
}

Change Set과 실행 순서

하나의 $batch 요청 내에서 Change Set(POST/PATCH/DELETE)은 순차적으로 실행됩니다. 하나의 Change Set 내 요청이 실패하면 이후 요청은 중단됩니다. 읽기 요청(GET)은 Change Set 밖에서 병렬 처리됩니다. Content-ID 연결을 통해 요청 간 의존성도 처리할 수 있습니다.

7. 고급 패턴 -- 트랜지언트 컨텍스트, keep-alive 등

트랜지언트 컨텍스트로 신규 행 추가

ODataListBinding.create()를 사용하면 서버에 저장하기 전의 "트랜지언트" 엔티티를 생성할 수 있습니다. 이 엔티티는 테이블에 즉시 표시되며, 사용자가 데이터를 입력한 뒤 배치 전송으로 서버에 저장합니다.

onAddProduct: function () {
  var oBinding = this.byId("productTable").getBinding("items");

  // 초기값과 함께 트랜지언트 컨텍스트 생성
  var oContext = oBinding.create({
    ProductName: "",
    Price: 0,
    Category: "New"
  });

  // 트랜지언트 상태 확인
  console.log("트랜지언트?", oContext.isTransient()); // true

  // created() Promise -- 서버 저장 완료 시 resolve
  oContext.created().then(function () {
    sap.m.MessageToast.show("신규 상품이 저장되었습니다.");
    console.log("트랜지언트?", oContext.isTransient()); // false
  }, function (oError) {
    // 사용자가 취소하거나 서버 오류 시
    console.error("생성 실패:", oError);
  });
},

onDeleteProduct: function () {
  var oContext = this.byId("productTable").getSelectedItem().getBindingContext();
  oContext.delete("saveGroup").then(function () {
    sap.m.MessageToast.show("삭제 대기 중 (submitBatch 시 실행)");
  });
}

keep-alive 컨텍스트

V4에서는 oContext.setKeepAlive(true)를 호출하면 해당 컨텍스트가 리스트 바인딩의 현재 범위를 벗어나도(예: 필터 변경, 페이지 이동) 메모리에 유지됩니다. 마스터-디테일 패턴에서 디테일 뷰의 데이터가 마스터 리스트 필터 변경으로 사라지는 문제를 방지할 때 유용합니다.

@$ui5.context.isSelected

V4 바인딩은 @$ui5.context.isSelected 어노테이션을 통해 컨텍스트의 선택 상태를 추적할 수 있습니다. 이를 활용하면 다중 선택 시나리오에서 선택된 항목만 필터링하는 등의 작업이 가능합니다.

Refresh 전략

// 전체 모델 새로고침
oModel.refresh();

// 특정 바인딩만 새로고침 (권장)
this.byId("productTable").getBinding("items").refresh();

// 특정 컨텍스트만 새로고침
oContext.refresh();

성능을 고려할 때, 전체 모델 refresh()보다는 필요한 바인딩이나 컨텍스트 단위의 세밀한 새로고침을 권장합니다.

8. 실무 팁 및 마무리

성능 최적화 체크리스트

자주 발생하는 실수

  1. V2 API를 V4에서 사용: oModel.read(), oModel.create(), oModel.submitChanges()는 V2 전용 메서드입니다. V4에서는 바인딩 기반 API(oBinding.create(), oModel.submitBatch())를 사용해야 합니다.
  2. $updateGroupId 누락: 수동 배치를 의도했으나 $updateGroupId를 지정하지 않으면 변경사항이 $auto 그룹으로 즉시 전송됩니다.
  3. manifest.json의 odataVersion 누락: "odataVersion": "4.0"을 명시하지 않으면 V2 모델이 생성되어 런타임 오류가 발생합니다.
  4. 트랜지언트 컨텍스트 미처리: create()submitBatch()를 호출하지 않으면 신규 행이 서버에 저장되지 않은 채로 남습니다. created() Promise를 활용해 저장 완료를 확인하세요.

디버깅 팁

개발 중에는 manifest.json에서 그룹 ID를 "$direct"로 임시 변경하면, 개별 HTTP 요청을 브라우저 개발자 도구의 Network 탭에서 직접 확인할 수 있어 디버깅이 용이합니다. 또한 sap.ui.model.odata.v4 카테고리의 로깅 레벨을 DEBUG로 설정하면 바인딩 동작에 대한 상세 로그를 확인할 수 있습니다.

참고: OData V4 모델은 SAPUI5 1.44에서 처음 도입되었으나, 프로덕션 사용에 충분히 안정적인 수준에 도달한 것은 1.75+ 이후입니다. 최신 기능(keep-alive, @$ui5.context.isSelected 등)을 활용하려면 1.110 이상 버전을 권장합니다.

참고 자료