UI5

전체 컬럼 금지 — $select 최적화 3가지 패턴 #shorts #SAP #UI5

▶ YouTube에서 보기

OData 전체 컬럼 요청의 문제점

SAPUI5 애플리케이션을 운영하다 보면 "왜 이렇게 느리지?"라는 질문을 자주 듣게 됩니다. 네트워크 탭을 열어보면 답이 보이는 경우가 많습니다. 판매주문(SalesOrder) 목록을 단순히 ID, 고객명, 금액 세 컬럼만 화면에 표시하는데도 서버는 50개가 넘는 필드(생성자, 변경시각, 통화 상세, 결제조건, 주소 라인 1~5 등)를 통째로 응답합니다. 한 건당 5KB라면 100건 조회 시 500KB가 전송되며, 이 중 실제로 사용자가 보는 데이터는 5% 미만인 경우가 흔합니다.

이 글에서는 OData V2/V4에서 제공하는 $select 시스템 쿼리 파라미터를 활용하여 페이로드를 최소화하고, UI5 바인딩 레이어에서 이를 안전하게 적용하는 실전 패턴을 다룹니다. 이 글을 마치면 다음을 수행할 수 있게 됩니다.

  • OData $select의 동작 원리와 백엔드 부하 감소 메커니즘을 이해
  • urlParameters 옵션을 통한 매니페스트/JS 레벨 적용
  • $expand와 결합한 연관 엔티티 필드 정밀 제어
  • XML View의 bindItems에서 parameters/select 선언
  • 컬럼 표시 설정에 연동되는 동적 $select 구성

이 글을 읽기 전에 알아두면 좋은 것들

이 글은 SAPUI5의 모델/바인딩 개념(sap.ui.model.odata.v2.ODataModel 또는 v4 ODataModel)에 대한 기초 이해를 전제로 합니다. XML View에서 Table/List 컨트롤에 items="{path: ...}" 형태로 aggregation 바인딩을 해본 경험, 그리고 manifest.json의 dataSourcesmodels 섹션을 수정해본 경험이 있다면 충분합니다. OData 프로토콜의 EntitySet, Navigation Property 용어에 익숙하면 4번 섹션을 빠르게 따라올 수 있습니다.

실습 환경과 준비물

이 글의 예제는 다음 환경을 기준으로 작성되었습니다.

  • SAPUI5 1.120 LTS (1.108 이상이면 동일하게 동작)
  • OData V2 백엔드: SAP S/4HANA 2023 또는 SAP Gateway 7.5 이상
  • OData V4 예시 일부: SAP Cloud Application Programming Model(CAP) 7.x
  • 개발 도구: SAP Business Application Studio 또는 VS Code + ui5-tooling
  • 브라우저: Chromium 기반(네트워크 페이로드 비교용)

백엔드가 $select를 지원하는지 먼저 확인이 필요합니다. SAP Gateway 서비스의 경우 $metadata에 정의된 EntitySet은 일반적으로 $select를 지원하지만, 일부 커스텀 CDS View나 Function Import는 무시되거나 부분 지원될 수 있습니다. 실 운영 적용 전 Postman으로 직접 호출하여 응답 필드가 줄어드는지 검증하는 것을 권장합니다.

$select 시스템 쿼리 파라미터의 동작 원리

OData 사양에서 $select는 서버가 응답에 포함할 프로퍼티를 클라이언트가 명시적으로 선택할 수 있게 해주는 시스템 쿼리 옵션입니다. URL 예시는 다음과 같습니다.

GET /sap/opu/odata/sap/ZSALES_SRV/SalesOrderSet?$select=SalesOrderID,CustomerName,GrossAmount&$top=50

비유하자면, 백엔드는 거대한 뷔페 테이블입니다. 기본 호출은 "1번 손님 전체 테이블 한 번에 주세요"라고 외치는 것과 같고, $select는 "샐러드, 스테이크, 와인만 주세요"라고 정확히 주문하는 것과 같습니다. 주방(DB) → 서빙(네트워크) → 식탁(클라이언트 메모리) 모든 단계에서 부하가 줄어듭니다.

특히 SAP Gateway의 경우 $select가 DB 레벨까지 전파되는지는 DPC_EXT 클래스의 GET_ENTITYSET 구현에 달려 있습니다. CDS View 기반 서비스는 보통 SELECT 절 최적화가 자동으로 이루어지며, 컬럼 수가 줄어들면 인덱스 활용도 향상으로 응답 시간이 단축됩니다. UI5 클라이언트 측에서는 모델 캐시 메모리 점유율 감소, 데이터 바인딩 디퍼링 시간 단축, JSON 파싱 비용 절감의 세 가지 이득이 있습니다.

주의할 점은 $select로 제외된 필드는 모델에 존재하지 않는다는 사실입니다. 이후 컨트롤러에서 oContext.getProperty("DeliveryDate")처럼 호출하면 undefined가 반환됩니다. 즉, 필드 사용처를 코드 전체에서 추적해야 안전하게 적용할 수 있습니다.

1단계 — urlParameters로 기본 적용하기

가장 단순한 형태는 모델 인스턴스 생성 시 또는 read 호출 시 urlParameters$select를 넣는 방식입니다. 구매주문(PurchaseOrder) 목록을 조회하는 컨트롤러 예제를 보겠습니다.

sap.ui.define([
  "sap/ui/core/mvc/Controller"
], function (Controller) {
  "use strict";

  return Controller.extend("com.acme.po.controller.PoList", {
    onInit: function () {
      const oModel = this.getOwnerComponent().getModel();
      oModel.read("/PurchaseOrderSet", {
        urlParameters: {
          "$select": "PurchaseOrderID,SupplierName,OrderDate,NetAmount,Status",
          "$top": "100",
          "$orderby": "OrderDate desc"
        },
        success: (oData) => {
          this.getView().setModel(
            new sap.ui.model.json.JSONModel(oData.results),
            "poList"
          );
        },
        error: (oError) => {
          sap.ui.require(["sap/m/MessageToast"], (MessageToast) => {
            MessageToast.show("주문 조회 실패: " + oError.message);
          });
        }
      });
    }
  });
});

위 코드는 5개 필드만 요청합니다. 동일한 EntitySet이 백엔드에서 30개 필드를 가진다면 페이로드는 약 1/6 수준으로 줄어듭니다. 적용 후 반드시 Chrome DevTools의 Network 탭에서 응답 본문 크기를 측정하여 효과를 확인하세요.

2단계 — $expand와 결합한 정밀 제어 + 로깅

실무에서는 단일 엔티티만 조회하는 경우가 드뭅니다. 판매주문과 그 하위 아이템(SalesOrderItem), 그리고 고객 정보(Customer)까지 한 번에 가져오는 시나리오를 보겠습니다. $expand로 연관 엔티티를 펼치면서, $select로 각 엔티티의 필드도 동시에 제한해야 진짜 최적화가 됩니다.

sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/base/Log"
], function (Controller, Log) {
  "use strict";

  return Controller.extend("com.acme.so.controller.SoDetail", {
    _loadSalesOrder: function (sSalesOrderId) {
      const oModel = this.getOwnerComponent().getModel();
      const sPath = `/SalesOrderSet('${sSalesOrderId}')`;

      const aSelectFields = [
        "SalesOrderID",
        "GrossAmount",
        "CurrencyCode",
        "ToCustomer/CustomerID",
        "ToCustomer/CompanyName",
        "ToCustomer/CountryCode",
        "ToItems/ItemPosition",
        "ToItems/ProductID",
        "ToItems/Quantity",
        "ToItems/NetAmount"
      ];

      const tStart = performance.now();

      oModel.read(sPath, {
        urlParameters: {
          "$expand": "ToCustomer,ToItems",
          "$select": aSelectFields.join(",")
        },
        success: (oData) => {
          const tElapsed = (performance.now() - tStart).toFixed(1);
          Log.info(
            `[SoDetail] ${sSalesOrderId} 로드 완료 (${tElapsed}ms, items=${oData.ToItems.results.length})`,
            null,
            "com.acme.so"
          );
          this.getView().setModel(
            new sap.ui.model.json.JSONModel(oData),
            "soDetail"
          );
        },
        error: (oError) => {
          Log.error(
            `[SoDetail] 조회 실패: ${oError.statusCode} ${oError.message}`,
            oError.responseText,
            "com.acme.so"
          );
        }
      });
    }
  });
});

핵심은 ToCustomer/CustomerID처럼 슬래시 표기로 Navigation Property의 필드까지 명시한다는 점입니다. $expand=ToCustomer만 쓰고 $select에 고객 필드를 명시하지 않으면 고객 엔티티는 여전히 전체 필드가 반환되어 최적화 효과가 반감됩니다. performance.now()로 시간 측정 로그를 남겨두면, 운영 환경에서 회귀 발생 시 디버깅이 빠릅니다.

3단계 — bindItems와 동적 select(프로덕션 패턴)

XML View에서 List/Table을 선언적으로 바인딩할 때는 parameters 블록에 select를 넣습니다. 더 나아가 사용자 개인화 설정(p13n)에 따라 표시 컬럼이 동적으로 바뀌는 경우, JS에서 컬럼 정의를 읽어 $select를 재구성하는 패턴이 필요합니다.

<mvc:View
  xmlns:mvc="sap.ui.core.mvc"
  xmlns="sap.m"
  controllerName="com.acme.so.controller.SoList">
  <Table id="soTable"
    items="{
      path: '/SalesOrderSet',
      parameters: {
        select: 'SalesOrderID,CustomerName,GrossAmount,Status',
        expand: 'ToCustomer',
        $count: true
      },
      length: 50
    }">
    <columns>
      <Column><Text text="주문번호"/></Column>
      <Column><Text text="고객"/></Column>
      <Column><Text text="금액"/></Column>
      <Column><Text text="상태"/></Column>
    </columns>
    <items>
      <ColumnListItem>
        <cells>
          <Text text="{SalesOrderID}"/>
          <Text text="{CustomerName}"/>
          <ObjectNumber number="{GrossAmount}" unit="{CurrencyCode}"/>
          <ObjectStatus text="{Status}"/>
        </cells>
      </ColumnListItem>
    </items>
  </Table>
</mvc:View>

이어서 사용자가 컬럼 표시 설정을 바꾸면 동적으로 바인딩을 재구성합니다. 다음은 보안과 성능을 함께 고려한 패턴입니다.

sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/ui/model/Sorter"
], function (Controller, Sorter) {
  "use strict";

  // 화이트리스트 — 임의 컬럼명 주입 방지
  const ALLOWED_FIELDS = new Set([
    "SalesOrderID", "CustomerName", "GrossAmount",
    "CurrencyCode", "Status", "OrderDate", "DeliveryStatus"
  ]);
  const MANDATORY_FIELDS = ["SalesOrderID"]; // 키 필드는 항상 포함

  return Controller.extend("com.acme.so.controller.SoList", {
    applyVisibleColumns: function (aUserColumns) {
      const aSafeFields = aUserColumns.filter(f => ALLOWED_FIELDS.has(f));
      const aFinal = Array.from(new Set([...MANDATORY_FIELDS, ...aSafeFields]));

      const oTable = this.byId("soTable");
      const oBinding = oTable.getBinding("items");

      oBinding.changeParameters({
        select: aFinal.join(","),
        expand: "ToCustomer"
      });
    }
  });
});

핵심 포인트는 세 가지입니다. 첫째, changeParameters는 기존 바인딩을 파괴하지 않고 쿼리만 갱신하므로 스크롤 위치 등 UI 상태가 유지됩니다. 둘째, ALLOWED_FIELDS 화이트리스트로 사용자 입력이 그대로 OData URL에 흘러들어가는 것을 차단합니다. 셋째, 키 필드(SalesOrderID)는 강제로 포함시켜야 OData 모델이 엔티티를 식별할 수 있습니다 — 이를 누락하면 "Cannot find entity" 류의 오류가 발생합니다.

실제로 부딪히는 문제와 해결

Q1. $select를 적용했는데 응답 크기가 그대로입니다. 백엔드가 무시한 경우입니다. CDS View가 아닌 클래식 BAPI 래퍼 서비스이거나, DPC 클래스가 사양을 구현하지 않은 경우 발생합니다. Postman으로 직접 ?$select=Field1을 호출해 검증하세요. 일부 SAP Gateway 서비스는 $select 무시가 명시되어 있을 수 있습니다.

Q2. 컨트롤러에서 oContext.getProperty("DeliveryDate")가 undefined를 반환합니다. 해당 필드가 $select에서 누락된 것입니다. 모든 사용처를 검색해 select 목록을 보정하거나, 디테일 화면에서 다시 read하는 방식으로 분리하세요. 목록 화면은 가볍게, 상세 화면에서 풀 페치 패턴이 일반적으로 권장됩니다.

Q3. $expand를 추가했더니 timeout이 발생합니다. 펼치는 연관이 많으면 백엔드 JOIN 비용이 폭증합니다. $expand는 꼭 필요한 경우에만 쓰고, 1:N 관계에서 N이 큰 경우 별도 호출이 더 빠를 수 있습니다. 백엔드 ST05 트레이스로 SQL을 확인하는 것을 권장합니다.

Q4. v4 모델에서 동일하게 동작하나요? OData V4에서는 $expand 내부에 $select를 중첩하는 문법이 정식입니다 — 예: $expand=ToItems($select=ItemPosition,Quantity). UI5 v4 ODataModel은 자동 처리해주는 부분이 있으므로 직접 URL을 구성하기보다 바인딩 파라미터를 사용하는 편이 안전합니다.

여기서 더 깊이 들어가려면

이 글을 마쳤다면 다음 주제로 확장해보길 권합니다. (1) $filter와 결합한 서버 사이드 필터링 전략, (2) $count=true$inlinecount를 활용한 페이지네이션 정확도 개선, (3) sap-value-list 어노테이션과 ValueHelp의 select 튜닝, (4) sap.ui.model.odata.v4의 그룹 요청(Batch + GroupId)으로 다중 호출 묶기. 특히 그룹 요청은 select 최적화와 시너지가 매우 큽니다 — 작은 요청 여러 개를 묶어 하나의 HTTP 왕복으로 처리하면 select가 줄여준 페이로드 효과가 배가됩니다.

더 읽어볼 만한 자료

댓글 0

아직 댓글이 없습니다.