개요 — 이 글에서 다루는 것
UI5에서 OData V4 모델을 쓰면서도 생성·수정·삭제(CUD)를 여전히 fetch나 수동 AJAX 호출로 처리하는 코드를 실무에서 자주 봅니다. OData V4 모델(sap.ui.model.odata.v4.ODataModel)은 V2와 설계 철학이 완전히 달라서, V2 방식의 create()/update()/remove() 메서드가 모델에 존재하지 않습니다. 대신 바인딩과 컨텍스트가 트랜잭션의 주체가 됩니다. 이 글은 판매오더(SalesOrder) 관리 화면을 소재로, V4 모델의 CRUD를 3단계 패턴(기본 → 에러/로깅 → 프로덕션 배치 트랜잭션)으로 구현합니다.
- V4에서 CUD가 바인딩/컨텍스트 중심으로 동작하는 원리 이해
ListBinding.create(),Context.setProperty(),Context.delete()실전 적용- updateGroupId와
submitBatch()로 명시적 트랜잭션 제어 - 에러 메시지 처리, 보류 변경(pending changes) 관리, 테스트 포인트 점검
시작 전에 알아두면 좋은 것
UI5 XML View와 컨트롤러 구조, JSON/OData 모델 바인딩 기본 문법({path}), JavaScript Promise/async-await를 알고 있다면 충분합니다. OData V2 모델 경험이 있다면 차이점 비교 위주로 읽으면 더 빠르게 이해됩니다.
환경 및 준비물
이 예제는 다음 환경을 기준으로 합니다.
- SAPUI5 1.120 이상(LTS) — V4 모델의
Context.setProperty,Context.delete, 그룹 ID 제어가 안정화된 버전대입니다. OpenUI5로도 동일하게 동작합니다. - OData V4 서비스 — SAP BTP의 CAP(Node.js/Java) 서비스 또는 S/4HANA의 RAP 기반 서비스 모두 가능합니다. 예제 엔터티는
SalesOrders(키:OrderID)입니다. - 로컬 개발 도구: Node.js 18+, UI5 CLI 또는 SAP Business Application Studio.
manifest.json의 모델 선언은 아래와 같이 odataVersion: "4.0"을 명시해야 V4 모델이 인스턴스화됩니다.
{
"sap.app": {
"dataSources": {
"orderService": {
"uri": "/odata/v4/order-management/",
"type": "OData",
"settings": { "odataVersion": "4.0" }
}
}
},
"sap.ui5": {
"models": {
"": {
"dataSource": "orderService",
"settings": {
"autoExpandSelect": true,
"operationMode": "Server",
"groupId": "$auto",
"updateGroupId": "orderChanges"
}
}
}
}
}
핵심 개념 — 왜 모델에 create()가 없는가
V2 모델은 "모델이 곧 창구"였습니다. oModel.create("/SalesOrders", data)처럼 경로 문자열을 들고 모델에 직접 요청했습니다. V4 모델은 이 창구를 없애고 바인딩(Binding)과 컨텍스트(Context)에 권한을 위임했습니다. 비유하자면 V2가 "은행 본점 창구에서 모든 업무 처리"라면, V4는 "각 지점(바인딩)에서 내 계좌(컨텍스트)를 직접 관리"하는 구조입니다.
- 생성: 테이블에 연결된
ODataListBinding의create()를 호출하면, 서버 전송 전에 이미 화면에 행이 나타나는 transient context(임시 컨텍스트)가 만들어집니다. 서버가 응답하면 임시 상태가 해제됩니다. - 수정: 양방향 바인딩 또는
Context.setProperty()가 변경분을 모델에 기록하고, 그룹 설정에 따라 PATCH 요청이 자동 또는 수동으로 나갑니다. - 삭제: 행에 해당하는
Context.delete()를 호출합니다. 경로 문자열이 아니라 객체 참조로 지우는 것이 핵심입니다.
또 하나의 축은 그룹 ID입니다. $auto는 요청을 자동으로 배치에 묶어 즉시 전송하고, $direct는 배치 없이 개별 전송합니다. 사용자 정의 그룹(예: orderChanges)을 updateGroupId로 지정하면 변경 요청이 큐에 쌓이기만 하고, submitBatch("orderChanges")를 호출하는 순간 하나의 $batch로 전송됩니다. 즉 "저장 버튼을 눌러야 반영되는 화면"은 V4에서 그룹 ID 설계로 구현합니다. 이것이 V4 CRUD의 트랜잭션 단위이며, 이 글의 3단계 패턴 전체를 관통하는 개념입니다.
실전 코드 3단계
1단계 — 기본: 생성/수정/삭제 한 사이클
먼저 즉시 전송 모드($auto)로 최소 동작을 확인합니다. XML View의 테이블은 다음과 같습니다.
<Table id="ordersTable" items="{path: '/SalesOrders', parameters: {$count: true}}">
<columns>
<Column><Text text="주문번호"/></Column>
<Column><Text text="고객사"/></Column>
<Column><Text text="금액"/></Column>
</columns>
<ColumnListItem>
<Text text="{OrderID}"/>
<Input value="{CustomerName}"/>
<Input value="{GrossAmount}"/>
</ColumnListItem>
</Table>
컨트롤러에서 바인딩을 얻어 CRUD를 수행합니다.
onCreateOrder: function () {
const oBinding = this.byId("ordersTable").getBinding("items");
// transient context 생성 — 화면에 즉시 행 추가
const oContext = oBinding.create({
CustomerName: "한빛물산",
GrossAmount: 0,
Currency: "KRW"
});
// 서버 확정 시점을 알고 싶다면 created() Promise 사용
oContext.created().then(() => {
sap.m.MessageToast.show("주문 " + oContext.getProperty("OrderID") + " 생성 완료");
});
},
onDeleteOrder: function () {
const oItem = this.byId("ordersTable").getSelectedItem();
if (!oItem) { return; }
oItem.getBindingContext().delete(); // 컨텍스트가 스스로 삭제 요청
}
수정은 별도 코드가 필요 없습니다. Input의 양방향 바인딩이 값을 바꾸는 순간 PATCH가 updateGroupId 규칙에 따라 처리됩니다. 여기까지가 V4 CRUD의 최소 골격입니다.
2단계 — 실무: 에러 처리와 로깅
실무에서는 생성 실패, 백엔드 검증 오류, 삭제 거부가 일상입니다. V4 모델은 실패한 변경을 큐에 남겨두고 재시도하는 특성이 있으므로, 실패 시 명시적으로 정리하는 코드가 필요합니다.
sap.ui.define([
"sap/base/Log",
"sap/m/MessageBox"
], function (Log, MessageBox) {
"use strict";
return {
onCreateOrder: async function () {
const oBinding = this.byId("ordersTable").getBinding("items");
const oContext = oBinding.create({
CustomerName: "두리테크",
GrossAmount: 1500000,
Currency: "KRW"
});
try {
await oContext.created();
Log.info("SalesOrder created", oContext.getPath(), "order.controller");
} catch (oError) {
if (oError.canceled) {
// 사용자가 resetChanges 등으로 취소한 경우 — 오류 아님
Log.info("생성 취소됨", "", "order.controller");
return;
}
Log.error("SalesOrder 생성 실패", oError.message, "order.controller");
MessageBox.error("주문 생성에 실패했습니다: " + oError.message);
// 실패한 transient 행 정리 (남겨두면 재전송 시도됨)
oContext.delete();
}
},
onDeleteOrder: async function () {
const oContext = this.byId("ordersTable")
.getSelectedItem().getBindingContext();
const sOrderId = oContext.getProperty("OrderID");
try {
await oContext.delete();
Log.info("SalesOrder deleted: " + sOrderId, "", "order.controller");
} catch (oError) {
MessageBox.error("삭제 실패(" + sOrderId + "): " + oError.message);
}
}
};
});
백엔드가 메시지(예: RAP 검증 메시지)를 돌려주면 UI5의 메시지 모델에 자동 반영됩니다. sap.ui.core.Messaging(구 MessageManager)의 메시지 모델을 다이얼로그나 MessagePopover에 바인딩해 두면 필드 단위 오류까지 사용자에게 노출할 수 있어, try/catch만으로 커버되지 않는 검증 오류를 잡아내는 데 효과적입니다.
3단계 — 프로덕션: 지연 그룹 + submitBatch 트랜잭션
프로덕션 화면에서는 "여러 건 편집 후 저장 버튼 한 번"이 일반적입니다. manifest에 지정한 updateGroupId: "orderChanges"가 여기서 힘을 발휘합니다. 모든 PATCH/POST/DELETE가 큐에 쌓이고, 저장 시점에 한 번의 $batch로 전송되므로 네트워크 왕복이 줄고(성능), 원자적 처리에 가까운 UX를 만들 수 있습니다.
onSave: async function () {
const oModel = this.getView().getModel();
if (!oModel.hasPendingChanges("orderChanges")) {
sap.m.MessageToast.show("변경 사항이 없습니다");
return;
}
this.byId("saveBtn").setBusy(true);
try {
await oModel.submitBatch("orderChanges");
// submitBatch는 개별 요청 실패 시에도 resolve될 수 있으므로
// 보류 변경 잔존 여부로 성공을 판정하는 것이 안전합니다
if (oModel.hasPendingChanges("orderChanges")) {
throw new Error("일부 변경이 반영되지 않았습니다");
}
sap.m.MessageToast.show("저장 완료");
} catch (oError) {
sap.m.MessageBox.error(oError.message);
} finally {
this.byId("saveBtn").setBusy(false);
}
},
onCancel: function () {
// 큐에 쌓인 모든 변경 + transient 행을 롤백
this.getView().getModel().resetChanges("orderChanges");
}
프로덕션 체크포인트를 함께 점검하세요.
- 성능:
autoExpandSelect: true로 바인딩에 필요한 필드만$select되게 하고, 테이블에는growing과 서버 페이징을 적용합니다. - 보안: CSRF 토큰은 V4 모델이 자동 처리하지만, BTP 배포 시 라우트가 승인된 destination을 거치는지, 엔터티 단위 권한(예: RAP의 authorization)이 걸려 있는지 확인이 필요합니다. 삭제는 UI 비활성화가 아니라 서버 권한으로 막아야 합니다.
- 테스트: OPA5로 "행 추가 → 저장 → 토스트 확인" 여정을 검증하고, 로컬에서는 CAP 목 서버나
ui5-middleware-simpleproxy로 실제 V4 응답 형태를 재현하는 것이 권장됩니다. transient 행의 롤백 시나리오(resetChanges)는 반드시 테스트 케이스에 포함하세요.
흔한 실수와 트러블슈팅
Q1. oModel.create is not a function 오류가 납니다.
V2 습관입니다. V4 모델에는 create/update/remove 메서드가 없습니다. 생성은 ListBinding.create(), 삭제는 Context.delete()로 옮겨야 합니다. manifest의 odataVersion이 "4.0"인지도 함께 확인하세요.
Q2. 저장 버튼을 눌러도 아무 요청이 나가지 않습니다.
사용자 정의 updateGroupId를 지정했는데 submitBatch()를 같은 그룹 이름으로 호출하지 않은 경우가 대부분입니다. 반대로 즉시 반영을 원하는데 요청이 안 나간다면 그룹이 $auto인지 확인하세요. 개발자 도구 네트워크 탭에서 $batch 요청 유무로 진단할 수 있습니다.
Q3. 생성 실패한 빈 행이 화면에 계속 남고, 다른 저장 때마다 재전송됩니다.
transient context는 실패해도 자동으로 사라지지 않습니다. created()의 catch에서 oContext.delete()로 정리하거나, 취소 버튼에서 resetChanges()를 호출해야 합니다. 이때 oError.canceled가 true인 reject는 정상 취소이므로 오류로 로깅하지 않도록 분기하세요.
Q4. 수정 중 페이지를 떠나면 변경이 유실됩니다.
라우트 이탈 시 hasPendingChanges()를 검사해 확인 다이얼로그를 띄우는 가드를 넣는 것이 일반적입니다. Fiori Elements가 제공하는 discard 확인 UX를 프리스타일에서도 직접 구현해 주는 셈입니다.
여기서 더 나아가기
이 패턴을 익혔다면 다음 주제로 확장해 보세요. 첫째, Draft 처리 — RAP/CAP의 draft 활성 엔터티와 결합하면 서버 측 임시 저장까지 트랜잭션에 포함됩니다. 둘째, 바운드 액션/펑션 — ODataContextBinding의 execute()(신규 버전은 invoke())로 승인·반려 같은 비즈니스 오퍼레이션을 호출합니다. 셋째, 깊은 생성(deep create) — 헤더+아이템을 한 번에 생성하는 시나리오는 UI5 1.107 이후 버전대에서 지원 범위가 넓어졌으니 사용 중인 버전의 릴리스 노트를 확인하세요.
더 읽어볼 문서
- SAPUI5 개발자 가이드 — OData V4 Model 개요 (help.sap.com)
- SAPUI5 — Creating an Entity (ListBinding.create) (help.sap.com)
- SAPUI5 — Batch Control과 그룹 ID (help.sap.com)
- API Reference — sap.ui.model.odata.v4.ODataModel
- API Reference — sap.ui.model.odata.v4.Context (delete/setProperty)
- OData V4 — Consuming OData V4 Services 샘플 모음 (ui5.sap.com)
댓글 0
아직 댓글이 없습니다.