UI5

JSONModel vs ODataModel — 선택 기준 3가지 #shorts #SAP #UI5

▶ YouTube에서 보기

JSONModel과 ODataModel이란 무엇인가

SAPUI5(1.120 LTS 기준)에서 뷰(View)와 데이터를 연결할 때 가장 먼저 마주하는 갈림길이 바로 JSONModelODataModel입니다. 두 모델 모두 sap.ui.model 네임스페이스 아래에 있으며, 뷰의 컨트롤(Table, List, Form)에 데이터를 공급하는 역할을 담당합니다. 그러나 내부 동작과 활용 목적은 상당히 다릅니다.

  • JSONModel: 클라이언트(브라우저) 메모리에 JSON 트리를 통째로 올려두고 다루는 클라이언트 사이드 모델입니다. 정적 파일, 설정, 국지적 UI 상태를 다룰 때 적합합니다.
  • ODataModel: SAP Gateway나 CAP(Cloud Application Programming) 서비스가 노출하는 OData v2/v4 엔드포인트와 통신하는 서버 사이드 모델입니다. 페이징, 필터, 정렬, CRUD 요청을 프로토콜 단에서 처리합니다.
이 글은 두 모델의 성격 차이를 3가지 실전 기준(데이터 출처, CRUD 복잡도, 성능 요구사항)으로 정리하고, 각 상황에 맞는 코드 패턴을 제시합니다.

이 글을 통해 확인할 체크리스트는 다음과 같습니다.

  • JSONModel과 ODataModel의 내부 동작 원리 이해
  • Two-way / One-way / One-time 바인딩 모드의 차이
  • 실무 프로젝트에서 두 모델을 조합해 쓰는 방법
  • 흔한 실수(경로 오타, 비동기 타이밍) 회피 방법

데이터 바인딩의 동작 원리와 3가지 모드

SAPUI5의 데이터 바인딩은 모델(Model) — 바인딩(Binding) — 컨트롤(Control)의 3계층으로 이해하면 명료합니다. 컨트롤이 {path} 표현식을 만나면 바인딩 객체가 생성되고, 바인딩은 모델에게 해당 경로의 값을 요청합니다. 모델은 값을 돌려주는 동시에, 값이 바뀔 때 바인딩을 통해 컨트롤에 알림(fireChange)을 보냅니다.

바인딩 모드는 세 가지가 있으며, 두 모델의 기본값이 다릅니다.

  • OneTime: 최초 1회만 값을 읽고 이후 변경을 반영하지 않습니다. 라벨, 정적 텍스트에 적합합니다.
  • OneWay: 모델 → 컨트롤 방향의 단방향 갱신. ODataModel v2의 기본값입니다.
  • TwoWay: 컨트롤에서 사용자가 입력한 값이 즉시 모델에 반영되는 양방향 바인딩. JSONModel의 기본값입니다.

이 차이가 실전에서 왜 중요할까요? 예를 들어 편집 가능한 폼을 만들 때 JSONModel은 별도 설정 없이 사용자 입력이 즉시 모델에 반영되지만, ODataModel v2는 명시적으로 setDefaultBindingMode(sap.ui.model.BindingMode.TwoWay)를 호출하거나 v4 모델을 써야 자연스러운 편집이 됩니다.

비유하자면 JSONModel은 손에 든 노트입니다. 무엇이든 즉시 쓰고 지울 수 있지만, 노트 자체가 서버와 동기화되지는 않습니다. ODataModel은 사무실 캐비닛의 서류철입니다. 필요한 페이지만 꺼내 보고, 수정하려면 정해진 절차(POST/PATCH/DELETE)를 거쳐야 하지만 조직 전체가 동일한 데이터를 공유합니다.

선택 기준 1 — 데이터 출처가 로컬인가 서버인가

가장 직관적인 기준입니다. 데이터가 정적 파일(mock/products.json), 사용자 세션 상태, 화면 간 임시 저장 값이라면 JSONModel이 정답입니다. 반대로 백엔드의 트랜잭션성 데이터(주문, 재고, 승인 이력)를 실시간으로 조회·변경해야 한다면 ODataModel이 자연스럽습니다.

중간 지대도 있습니다. 대시보드의 KPI처럼 백엔드에서 한 번 받아 오지만 이후 편집은 필요 없는 데이터는, ODataModel로 읽어 JSONModel에 담아 재사용하는 하이브리드 패턴이 흔히 쓰입니다.

선택 기준 2 — CRUD 복잡도와 OData 프로토콜 활용도

단순 조회만 필요한 경우와, 생성/수정/삭제를 트랜잭션 단위로 묶어야 하는 경우는 요구가 전혀 다릅니다. ODataModel v2/v4는 다음 기능을 프로토콜 수준에서 제공합니다.

  • $batch: 여러 요청을 한 번의 HTTP 호출로 묶어 전송
  • Deferred Groups(v2) / Batch Groups(v4): 저장 시점 제어
  • Draft-enabled Entity(v4): CAP의 draft 기능과 통합
  • Optimistic concurrency(ETag 헤더)로 동시 편집 충돌 감지

JSONModel에는 이런 개념이 없습니다. CRUD가 복잡할수록 ODataModel의 이점이 커집니다.

선택 기준 3 — 성능·페이징·필터링 요구사항

10만 건짜리 판매 주문 목록을 JSONModel로 불러오면 브라우저 메모리가 폭발합니다. ODataModel은 $skip, $top, $filter, $orderby를 서버로 위임하므로 화면에 필요한 만큼만 가져옵니다. sap.m.Table + growing 속성과 결합하면 스크롤에 따라 페이지 단위로 로드됩니다. 반면 JSONModel은 클라이언트 정렬/필터가 즉각적이고 오프라인에서도 동작하므로, 수백 건 이하의 코드 테이블이나 설정 데이터에 유리합니다.

실전 예제 1 — JSONModel로 제품 카탈로그 구성

먼저 정적 JSON 파일 webapp/localData/ProductCatalog.json을 준비합니다.

{
  "Categories": [
    { "code": "ELEC", "name": "전자기기" },
    { "code": "OFFC", "name": "사무용품" }
  ],
  "Products": [
    { "id": "P-1001", "name": "무선 키보드", "category": "ELEC", "price": 45000, "stock": 32 },
    { "id": "P-1002", "name": "A4 복사용지", "category": "OFFC", "price": 3200, "stock": 500 },
    { "id": "P-1003", "name": "노이즈캔슬링 헤드셋", "category": "ELEC", "price": 189000, "stock": 8 }
  ]
}

Component.js의 init 훅에서 모델을 등록합니다.

sap.ui.define([
  "sap/ui/core/UIComponent",
  "sap/ui/model/json/JSONModel"
], function (UIComponent, JSONModel) {
  "use strict";
  return UIComponent.extend("catalog.demo.Component", {
    metadata: { manifest: "json" },
    init: function () {
      UIComponent.prototype.init.apply(this, arguments);
      var oCatalog = new JSONModel(sap.ui.require.toUrl("catalog/demo/localData/ProductCatalog.json"));
      this.setModel(oCatalog, "catalog");
    }
  });
});

XML View에서는 명명된 모델 catalog를 사용해 바인딩합니다.

<Table items="{catalog>/Products}">
  <columns>
    <Column><Text text="상품ID"/></Column>
    <Column><Text text="상품명"/></Column>
    <Column hAlign="End"><Text text="단가"/></Column>
  </columns>
  <items>
    <ColumnListItem>
      <cells>
        <Text text="{catalog>id}"/>
        <Text text="{catalog>name}"/>
        <ObjectNumber number="{catalog>price}" unit="KRW"/>
      </cells>
    </ColumnListItem>
  </items>
</Table>

실전 예제 2 — ODataModel v2로 판매 주문 조회 및 에러 처리

manifest.json의 sap.app.dataSourcessap.ui5.models에 서비스를 등록합니다.

{
  "sap.app": {
    "dataSources": {
      "salesService": {
        "uri": "/sap/opu/odata/sap/ZSD_SALES_SRV/",
        "type": "OData",
        "settings": { "odataVersion": "2.0" }
      }
    }
  },
  "sap.ui5": {
    "models": {
      "": {
        "dataSource": "salesService",
        "settings": {
          "defaultBindingMode": "TwoWay",
          "defaultCountMode": "Inline",
          "useBatch": true
        }
      }
    }
  }
}

컨트롤러에서 조회 실패 시 사용자에게 메시지를 표시하고 로그를 남깁니다.

sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/m/MessageBox",
  "sap/base/Log"
], function (Controller, MessageBox, Log) {
  "use strict";
  return Controller.extend("sales.demo.controller.OrderList", {
    onInit: function () {
      var oTable = this.byId("orderTable");
      oTable.bindItems({
        path: "/SalesOrderSet",
        parameters: { expand: "ToCustomer" },
        filters: [new sap.ui.model.Filter("Status", "EQ", "OPEN")],
        events: {
          dataReceived: this._onOrdersLoaded.bind(this)
        }
      });
    },
    _onOrdersLoaded: function (oEvent) {
      var oError = oEvent.getParameter("error");
      if (oError) {
        Log.error("판매 주문 로딩 실패", oError, "sales.demo");
        MessageBox.error("주문 조회 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.");
      }
    }
  });
});

실전 예제 3 — 배치 처리와 코드 테이블 캐싱

실제 서비스에서는 여러 조회를 한 번의 $batch로 묶고, 코드 테이블은 클라이언트에 캐싱합니다.

// 코드 테이블은 최초 1회만 로드해 JSONModel로 캐싱
_loadCurrencyCache: function () {
  var oComponent = this.getOwnerComponent();
  var oCache = oComponent.getModel("cache");
  if (oCache && oCache.getProperty("/Currencies")) {
    return Promise.resolve();
  }
  var oOData = oComponent.getModel();
  return new Promise(function (resolve, reject) {
    oOData.read("/CurrencySet", {
      urlParameters: { "$select": "code,name,decimals" },
      success: function (oData) {
        var oJson = new sap.ui.model.json.JSONModel({ Currencies: oData.results });
        oComponent.setModel(oJson, "cache");
        resolve();
      },
      error: reject
    });
  });
},

// $batch로 헤더+아이템을 한 번에 전송
onSaveOrder: function () {
  var oModel = this.getView().getModel();
  oModel.setDeferredGroups(["orderSave"]);
  oModel.create("/SalesOrderSet", oHeaderPayload, { groupId: "orderSave" });
  aItems.forEach(function (oItem) {
    oModel.create("/SalesOrderItemSet", oItem, { groupId: "orderSave" });
  });
  oModel.submitChanges({
    groupId: "orderSave",
    success: this._onSaveSuccess.bind(this),
    error: this._onSaveError.bind(this)
  });
}

초보자가 자주 겪는 실수와 FAQ

Q1. 바인딩 경로 앞의 슬래시(/)를 왜 어떨 때는 붙이고 어떨 때는 뺄까요?
절대 경로(모델 루트 기준)는 /Products, 상대 경로(부모 바인딩 컨텍스트 기준)는 name처럼 슬래시 없이 씁니다. Table의 items가 /Products로 설정되면 그 아래 ColumnListItem 내부에서는 id, name만 써야 합니다.

Q2. ODataModel에서 데이터를 읽었는데 콘솔에는 값이 있지만 화면이 비어 있습니다.
비동기 타이밍 문제일 가능성이 높습니다. onInit에서 즉시 getProperty를 호출하면 아직 요청이 완료되지 않았을 수 있습니다. dataReceived 이벤트나 metadataLoaded() Promise를 활용해야 합니다.

Q3. TwoWay 바인딩인데 입력한 값이 서버에 반영되지 않습니다.
ODataModel v2에서 TwoWay는 클라이언트 모델에만 값을 반영합니다. 서버로 전송하려면 submitChanges()를 명시적으로 호출해야 합니다. v4에서는 $$updateGroupId 파라미터로 자동 저장 방식을 제어합니다.

두 모델을 함께 사용하는 패턴

현업 Fiori 앱의 상당수는 두 모델을 동시에 사용합니다. 대표적인 조합 패턴 3가지입니다.

  • 상태 모델(JSONModel) + 데이터 모델(ODataModel): 편집 중 여부, 선택된 행 인덱스, 로딩 스피너 표시 여부 등 순수 UI 상태는 JSONModel에, 실제 비즈니스 데이터는 ODataModel에 둡니다.
  • 코드 테이블 캐시(JSONModel) + 트랜잭션(ODataModel): 국가 코드, 통화 코드처럼 자주 바뀌지 않는 마스터 데이터는 최초 로딩 후 JSONModel에 캐싱해 반복 호출을 막습니다.
  • 오프라인 초안(JSONModel) + 온라인 제출(ODataModel): 사용자가 폼을 작성하는 동안에는 JSONModel에 임시 저장하고, 최종 제출 시에만 ODataModel을 통해 서버로 전송합니다. 이 패턴은 네트워크 오류 복구도 용이합니다.

댓글 0

아직 댓글이 없습니다.