개요와 이 글에서 다루는 범위
SAP UI5 애플리케이션이 OData 서비스를 호출할 때, 마스터-디테일 화면에서 가장 자주 발생하는 성능 병목이 바로 N+1 쿼리 문제입니다. 판매 오더 목록을 가져온 뒤 각 오더의 고객 정보를 따로따로 조회하면 100건 목록에 101번의 HTTP 요청이 발생합니다. 이 글에서는 OData의 $expand 시스템 쿼리 옵션을 활용해 Navigation Property를 인라인으로 가져오는 방법을 다룹니다.
- N+1 문제가 UI5 컨트롤러에서 어떻게 발생하는지 진단
oModel.read()의urlParameters로$expand전달- 복수·중첩 Navigation Property를 한 번에 가져오는 패턴
bindItems의parameters.expand선언 방식- 판매 오더 + 라인 아이템 + 고객의 3단 구조 실전 적용
- 페이로드 폭증을 막는
$select조합 전략
먼저 알아두면 좋은 사전 지식
이 글은 UI5의 sap.ui.model.odata.v2.ODataModel 또는 v4 ODataModel을 manifest.json에서 dataSource로 선언해 사용해 본 경험을 전제로 합니다. JSONModel과 ODataModel의 차이, XML View에서 바인딩하는 방식, Navigation Property가 OData EDM 메타데이터에 정의된다는 점 정도를 알고 있으면 충분합니다.
환경과 버전 정보
예제는 SAP UI5 1.120.x LTS와 OData V2 서비스(ABAP SEGW 또는 RAP 노출)를 기준으로 합니다.
- UI5 라이브러리 1.120 LTS (1.108, 1.96에서도 동작)
- OData V2 (예제 코드 기준), V4 차이점 별도 명시
- BAS(Business Application Studio) 또는 VS Code + ui5-cli
- 네트워크 트래픽 진단용 Chrome DevTools Network 탭
핵심 개념: N+1과 $expand가 동작하는 원리
N+1 쿼리는 ORM·OData 어디서나 나타나는 고전적 안티패턴입니다. 부모 컬렉션 1번을 조회한 뒤 자식 정보를 N번 추가 조회하므로 총 N+1번의 왕복이 발생합니다.
$expand는 OData 표준의 시스템 쿼리 옵션으로, "이 엔티티를 가져올 때 연관 Navigation Property를 같은 응답 본문에 포함시켜라"고 서버에 지시합니다.
GET /sap/opu/odata/sap/ZSALES_SRV/SalesOrderSet?$expand=ToCustomer,ToItems&$select=OrderID,OrderDate,ToCustomer/CustomerName,ToItems/Material
서버는 단일 SQL JOIN 또는 후속 fetch를 내부적으로 수행하고, 응답은 부모 엔티티 안에 ToCustomer 객체와 ToItems 배열이 인라인된 형태로 돌아옵니다.
1단계: 단일 엔티티에 $expand 적용하기
// controller/OrderDetail.controller.js
return Controller.extend("acme.sales.controller.OrderDetail", {
_loadOrderWithCustomer: function (sOrderId) {
var oModel = this.getView().getModel();
var sPath = "/SalesOrderSet('" + sOrderId + "')";
oModel.read(sPath, {
urlParameters: {
"$expand": "ToCustomer"
},
success: function (oData) {
// oData.ToCustomer 가 인라인으로 들어 있음
console.log("주문 고객:", oData.ToCustomer.CustomerName);
},
error: function (oError) {
sap.m.MessageToast.show("주문 조회 실패");
}
});
}
});
핵심은 urlParameters 객체의 키가 반드시 달러 기호를 포함한 "$expand" 문자열이어야 한다는 점입니다. ODataModel은 이를 URL 쿼리스트링으로 직렬화하면서 자동 URI 인코딩을 처리합니다.
2단계: 복수·중첩 Navigation으로 확장하고 에러 처리 추가
_loadOrderTree: function (sOrderId) {
var oView = this.getView();
var oModel = oView.getModel();
var sPath = "/SalesOrderSet('" + sOrderId + "')";
oView.setBusy(true);
oModel.read(sPath, {
urlParameters: {
// 형제: ToCustomer, ToItems / 중첩: ToItems 아래 ToProduct
"$expand": "ToCustomer,ToItems,ToItems/ToProduct",
"$select": [
"OrderID", "OrderDate", "TotalAmount", "Currency",
"ToCustomer/CustomerID", "ToCustomer/CustomerName",
"ToItems/ItemPosition", "ToItems/Quantity", "ToItems/NetPrice",
"ToItems/ToProduct/ProductID", "ToItems/ToProduct/ProductName"
].join(",")
},
success: function (oData) {
oView.setBusy(false);
console.log("아이템 수:", oData.ToItems.results.length);
},
error: function (oError) {
oView.setBusy(false);
sap.m.MessageBox.error("주문 트리 조회 실패");
}
});
},
주의할 점은 $select를 함께 쓰지 않으면 모든 컬럼이 따라온다는 사실입니다. 항상 화면이 실제로 표시하는 필드만 명시적으로 선언하는 습관이 필요합니다.
3단계: XML View 바인딩으로 선언적 $expand 적용 + 프로덕션 튜닝
<Table id="orderTable"
items="{
path: '/SalesOrderSet',
parameters: {
expand: 'ToCustomer,ToItems/ToProduct',
select: 'OrderID,OrderDate,TotalAmount,ToCustomer/CustomerName,ToItems/Quantity'
},
sorter: { path: 'OrderDate', descending: true }
}">
<items>
<ColumnListItem>
<cells>
<Text text="{OrderID}"/>
<Text text="{ToCustomer/CustomerName}"/>
<ObjectNumber number="{TotalAmount}" unit="KRW"/>
</cells>
</ColumnListItem>
</items>
</Table>
{
"sap.ui5": {
"models": {
"": {
"dataSource": "mainService",
"settings": {
"defaultCountMode": "Inline",
"useBatch": true
}
}
}
}
}
흔히 마주치는 함정과 FAQ
Q1. expand를 걸었는데 응답에 Navigation Property가 비어 있다.
백엔드 SEGW 프로젝트에서 Association의 Navigation이 정의돼 있고 Data Provider 클래스의 EXPAND_ENTITYSET 메서드가 구현돼 있어야 합니다. RAP의 경우 Behavior Definition에서 Association이 노출돼 있는지 확인합니다.
Q2. 슬래시 깊이를 늘렸더니 500 에러가 발생한다.
SAP Gateway 기본 정책상 expand 깊이는 무제한이 아닙니다. 가능하면 깊이 3 이상은 피하는 편을 권장합니다.
Q3. expand를 썼는데도 네트워크 탭에 요청이 여전히 많이 보인다.
ODataModel V2의 batch 그룹화 타이밍 문제일 가능성이 큽니다. 동일 tick 내에 호출돼야 하나의 $batch로 묶이므로, setTimeout으로 분리된 read 호출은 별도 요청이 됩니다.
Q4. 모바일에서 응답이 느리다.
expand 결과 페이로드를 Chrome DevTools에서 측정하세요. 300KB를 넘으면 $select로 필드를 줄이고, 상세 화면에서만 필요한 Navigation은 진입 시점으로 지연시키는 분할이 효과적입니다.
이어서 살펴보면 좋은 관련 주제
OData V4의 $expand+$apply(GroupBy 집계 인라인), $filter를 expand 내부에 거는 nested filter 문법, Smart Table·List Report에서 자동으로 생성되는 expand 동작과의 상호작용을 살펴보면 좋습니다. CAP 백엔드를 다룬다면 CDS에서 Association이 어떻게 OData Navigation Property로 변환되는지 이해해 두면 클라이언트·서버 양쪽을 일관되게 설계할 수 있습니다.
댓글 0
아직 댓글이 없습니다.