SAPUI5 OData V4 바인딩 완전 가이드 — ListBinding, ContextBinding, $batch 처리까지
1. 개요 -- OData V4 바인딩이란 무엇인가
SAPUI5의 OData V4 모델(sap.ui.model.odata.v4.ODataModel)은 OData V4 프로토콜을 기반으로 백엔드 서비스와 프론트엔드 UI를 연결하는 데이터 바인딩 레이어입니다. OData V2 모델과 비교했을 때 근본적인 아키텍처 차이가 있으며, V4는 보다 표준 지향적이고 성능 최적화에 유리한 설계를 갖추고 있습니다.
V2와 V4의 핵심 차이
- 데이터 동기화 방식: V2는 클라이언트 측 캐시(
ODataModel)에 전체 엔티티를 저장하고 변경 사항을 추적합니다. V4는 바인딩 단위로 데이터를 관리하며, 각 바인딩이 독립적으로 서버와 통신합니다. - 배치 처리: V2는
setDeferredGroups와submitChanges를 사용합니다. V4는$updateGroupId와submitBatch로 더 세밀한 배치 그룹 제어가 가능합니다. - 자동 $select/$expand: V4는
autoExpandSelect: true설정 시, 뷰에서 실제로 바인딩된 프로퍼티만 자동으로$select에 포함시켜 네트워크 페이로드를 최소화합니다. - 트랜지언트 컨텍스트: V4에서는
create()로 생성한 엔티티가 서버에 저장되기 전까지 "트랜지언트(transient)" 상태로 관리되며,isTransient()로 상태를 확인할 수 있습니다. - 프로토콜 표준: V4는 JSON 기반이 기본이며, ATOM/XML 포맷을 지원하지 않습니다. URL 컨벤션도 OData 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로 처리될 수 있습니다. |
synchronizationMode | V4에서는 "None"만 지원됩니다. 이 값을 명시적으로 설정하는 것을 권장합니다. |
operationMode | "Server"(기본값)는 필터/정렬을 서버에 위임합니다. "Client"는 클라이언트에서 처리하지만 대량 데이터에는 부적합합니다. |
autoExpandSelect | true로 설정하면 뷰에 바인딩된 프로퍼티만 $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>
쿼리 파라미터 상세
$count: true-- 서버에$count=true를 전송하여 전체 레코드 수를 함께 반환받습니다. 페이지네이션 UI 구현 시 필수입니다.$orderby-- 서버 측 정렬 기준을 지정합니다. 복수 필드는'Price desc,ProductName asc'형태로 작성합니다.$filter-- OData 필터 표현식입니다. 뷰에서 정적 필터로 선언할 수 있으며, 동적 필터는 컨트롤러에서filter()메서드를 사용합니다.$select-- 가져올 프로퍼티를 명시합니다.autoExpandSelect: true사용 시 뷰 바인딩 기반으로 자동 결정되므로 수동 지정이 불필요할 수 있습니다.$updateGroupId-- 이 바인딩에서 발생하는 변경 요청을 특정 배치 그룹에 할당합니다.submitBatch로 수동 전송할 때 사용합니다.
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.Decimal은 sap.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 요청으로 묶어 전송합니다. 이를 통해 네트워크 왕복 횟수를 줄이고 트랜잭션 일관성을 확보할 수 있습니다.
배치 그룹의 개념
$auto(기본) -- 읽기 요청(GET)은 자동으로 하나의 배치로 묶여 전송됩니다. 별도 설정이 없으면 모든 읽기 요청이 여기에 속합니다.$auto.xxx--$auto의 서브 그룹으로, 동일 배치 내에서 별도 Change Set으로 분리됩니다.$direct-- 배치를 사용하지 않고 개별 HTTP 요청으로 전송합니다. 디버깅 시 유용합니다.- 커스텀 그룹 (예:
myBatch) --submitBatch()를 명시적으로 호출할 때까지 요청이 대기합니다.
수동 배치 패턴
// 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. 실무 팁 및 마무리
성능 최적화 체크리스트
autoExpandSelect: true-- 반드시 활성화하세요. 뷰에서 사용하는 프로퍼티만 서버에 요청하므로 페이로드가 크게 줄어듭니다.$count: true-- 총 건수가 필요한 경우에만 사용하세요. 불필요한 COUNT 쿼리는 백엔드 부하를 유발합니다.- $direct 모드 제한 -- 개발/디버깅 외에는
$direct그룹을 사용하지 마세요. 배치 모드가 네트워크 효율에서 유리합니다. - 필터 설계 -- 대량 데이터에서
operationMode: "Client"는 초기 로딩이 느려질 수 있으므로, 서버 측 필터("Server")를 기본으로 사용하세요. - 바인딩 단위 refresh --
oModel.refresh()대신 개별 바인딩의refresh()를 사용하면 불필요한 요청을 방지할 수 있습니다.
자주 발생하는 실수
- V2 API를 V4에서 사용:
oModel.read(),oModel.create(),oModel.submitChanges()는 V2 전용 메서드입니다. V4에서는 바인딩 기반 API(oBinding.create(),oModel.submitBatch())를 사용해야 합니다. - $updateGroupId 누락: 수동 배치를 의도했으나
$updateGroupId를 지정하지 않으면 변경사항이$auto그룹으로 즉시 전송됩니다. - manifest.json의 odataVersion 누락:
"odataVersion": "4.0"을 명시하지 않으면 V2 모델이 생성되어 런타임 오류가 발생합니다. - 트랜지언트 컨텍스트 미처리:
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 이상 버전을 권장합니다.