RAP

F4 Value Help Association 없이 실패 3가지 #shorts #SAP #RAP

▶ YouTube에서 보기

RAP F4 Value Help가 UX와 데이터 무결성에 미치는 영향

Fiori Elements 화면에서 사용자가 입력 필드를 클릭했을 때 뜨는 조회 팝업, 이른바 F4 Help(Value Help)는 단순한 편의 기능이 아닙니다. 이 팝업은 사용자가 잘못된 코드를 입력해서 서버 왕복 이후에 검증 실패로 되돌아오는 경험을 원천적으로 차단하는 입력 시점 게이트키퍼 역할을 합니다. RAP(ABAP RESTful Application Programming Model) 환경에서 F4 Value Help는 CDS View의 어노테이션과 Association 그래프를 통해 선언적으로 구성됩니다. 즉 화면 개발자가 별도 코드를 짜지 않아도, CDS 계층에서 @Consumption.valueHelpDefinitionassociation만 올바르게 설계하면 Fiori Elements 앱이 알아서 팝업을 그려줍니다.

이 글에서 확인해 볼 항목을 먼저 정리하면 다음과 같습니다.

  • Association 없이 valueHelpDefinition을 걸었을 때 왜 팝업이 비어 있거나 활성화되지 않는지 이해
  • @Consumption.valueHelpDefinitionentity.element, additionalBinding, useForValidation 속성 해부
  • 구매 발주(PurchaseOrder) 시나리오로 Supplier / Plant / Material Value Help를 CDS와 RAP BO에 연결
  • OData V4 서비스로 노출하고 Fiori Elements List Report에서 실제로 F4가 뜨는지 확인
  • 실패 로그 해석과 SADL 런타임에서 자주 뱉는 에러 메시지 대처

이 글을 편하게 따라오려면 알고 있어야 할 배경

ABAP CDS View의 기본 구문(define view entity, association to, @Metadata.allowExtensions)에 어느 정도 익숙해야 합니다. RAP Managed BO 골격(Behavior Definition, Behavior Implementation Class)을 한 번쯤 만들어 본 경험, 그리고 Service Definition / Service Binding으로 OData V4 서비스를 노출해 본 경험이 있으면 이해가 훨씬 수월합니다. Fiori Elements List Report와 Object Page의 기본 구성(@UI.lineItem, @UI.identification)에 대한 감도 필요합니다.

실습 환경과 준비물

이 글의 코드는 ABAP Cloud(SAP BTP, ABAP Environment) 기준이며, ABAP Platform 2023 On-Premise 및 S/4HANA 2023 이상에서도 대부분 동일하게 동작합니다. 개발 도구는 ADT(ABAP Development Tools) 3.36 이상, Eclipse 2023-12 이상을 권장합니다. OData 버전은 V4를 가정합니다(V2 서비스는 일부 어노테이션 해석이 다르므로 주의).

  • ABAP Cloud Tier 3 / ABAP Platform 2023 이상
  • ADT 3.36+, Java 17+
  • Service Binding 유형: OData V4 - UI
  • 테스트: Fiori Elements Preview(우클릭 > Open Fiori Elements App Preview)

테이블은 ZTB_PO_HEAD(구매 발주 헤더), ZTB_SUPPLIER(공급업체 마스터), ZTB_PLANT(플랜트 마스터)를 가정합니다. 실제 시스템에서는 명명 규칙에 맞게 접두어를 조정하면 됩니다.

선언형 Value Help의 동작 원리와 흔한 오해

RAP에서 F4 Value Help는 크게 세 가지 방식으로 구현할 수 있습니다. 첫째, CDS Association을 사용한 암묵적(implicit) Value Help — Foreign Key 관계를 CDS로 표현하면 Fiori Elements가 자동으로 팝업을 만듭니다. 둘째, @Consumption.valueHelpDefinition을 사용한 명시적(explicit) Value Help — 어떤 CDS View의 어떤 필드를 사용해 팝업을 그릴지 개발자가 지정합니다. 셋째, Search Help 객체(SH)를 CDS에 붙이는 레거시 방식입니다.

흔한 오해: "어노테이션만 붙이면 알아서 되겠지"라는 접근. 실상은 어노테이션이 참조할 대상 CDS View가 존재해야 하고, 소스 필드와 대상 필드 간의 바인딩이 명시되어야 하며, 다중 조건 필터가 필요한 경우 Association이 반드시 필요합니다.

비유하자면, valueHelpDefinition은 "이 우편함에서 편지를 꺼내라"는 지시이고, Association은 "그 우편함이 어디에 있는지 알려주는 지도"입니다. 지도 없이 지시만 있으면 런타임은 편지함을 찾지 못하거나, 조건에 맞는 편지를 걸러내지 못한 채 전체를 뿌립니다. 결과적으로 팝업이 텅 비거나, 관련 없는 후보가 잔뜩 나오거나, "SADL: Association not found" 계열의 예외가 로그에 남게 됩니다.

또 하나 중요한 개념은 context propagation입니다. PurchaseOrder에서 Supplier를 선택했을 때, 그 다음에 뜨는 Plant Value Help는 방금 선택한 Supplier로 자동 필터링되어야 자연스럽습니다. 이 문맥 전달은 additionalBinding 없이는 절대 자동으로 이뤄지지 않습니다.

1단계 — 마스터 CDS View와 기본 Value Help 뷰 작성

먼저 공급업체 마스터를 위한 Value Help 전용 CDS View를 만듭니다. Value Help 뷰는 팝업에 표시할 컬럼과 검색 가능한 컬럼을 @UI.lineItem, @Search.defaultSearchElement로 명시하는 것이 관건입니다.

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Supplier VH - PurchaseOrder Scenario'
@Search.searchable: true
@ObjectModel.resultSet.sizeCategory: #XS
define view entity ZI_SupplierVH
  as select from ztb_supplier
{
  @UI.lineItem: [{ position: 10, importance: #HIGH }]
  @Search.defaultSearchElement: true
  key supplier_id      as SupplierId,

  @UI.lineItem: [{ position: 20 }]
  @Search.defaultSearchElement: true
  @Search.fuzzinessThreshold: 0.8
      supplier_name    as SupplierName,

  @UI.lineItem: [{ position: 30 }]
      country_code     as CountryCode,

  @UI.lineItem: [{ position: 40 }]
      city             as City,

      is_blocked       as IsBlocked
}
where is_blocked = ' '

여기서 자주 놓치는 포인트가 where is_blocked = ' '입니다. Value Help는 반드시 유효한 후보만 노출해야 합니다. 삭제 예정이거나 블록된 마스터가 팝업에 뜨면 사용자가 선택 후 저장 시점에 검증 실패를 겪게 되어 UX가 무너집니다.

2단계 — Association 기반 실전 Value Help 구성

이제 PurchaseOrder 트랜잭션 CDS View에서 Supplier와 Plant를 Association으로 연결하고, 각 필드에 @Consumption.valueHelpDefinition을 명시적으로 부착합니다. 실무에서는 로깅과 에러 추적을 위해 @Metadata.layer: #CORE와 텍스트 라벨을 함께 관리합니다.

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'PurchaseOrder Transactional View'
@Metadata.allowExtensions: true
define root view entity ZI_PurchaseOrder
  as select from ztb_po_head
  association [0..1] to ZI_SupplierVH as _Supplier
    on  \$projection.SupplierId = _Supplier.SupplierId
  association [0..1] to ZI_PlantVH    as _Plant
    on  \$projection.PlantId    = _Plant.PlantId
   and  \$projection.SupplierId = _Plant.SupplierId
{
  key po_uuid           as PoUuid,

      @Search.defaultSearchElement: true
      po_number         as PoNumber,

      @ObjectModel.foreignKey.association: '_Supplier'
      @Consumption.valueHelpDefinition: [{
        entity: { name: 'ZI_SupplierVH', element: 'SupplierId' },
        useForValidation: true
      }]
      supplier_id       as SupplierId,

      @ObjectModel.foreignKey.association: '_Plant'
      @Consumption.valueHelpDefinition: [{
        entity: { name: 'ZI_PlantVH', element: 'PlantId' },
        additionalBinding: [
          { localElement: 'SupplierId', element: 'SupplierId',
            usage: #FILTER_AND_RESULT }
        ],
        useForValidation: true
      }]
      plant_id          as PlantId,

      order_amount      as OrderAmount,
      currency_code     as CurrencyCode,

      @Semantics.systemDate.createdAt: true
      created_at        as CreatedAt,

      _Supplier,
      _Plant
}

핵심 포인트를 정리하면 다음과 같습니다. 첫째, useForValidation: true는 SADL 프레임워크가 저장 시점에 입력값을 Value Help 후보와 대조해 유효성 검증을 수행하도록 지시합니다. 잘못된 supplier_id가 저장되지 않도록 데이터 무결성을 강제하는 안전장치입니다. 둘째, additionalBindingusage: #FILTER_AND_RESULT는 상위 필드(SupplierId)의 값을 필터로 사용하면서, 사용자가 팝업에서 값을 선택하면 그 값을 결과로도 되돌려주는 양방향 바인딩입니다.

3단계 — 프로덕션 수준의 RAP BO 연결과 서비스 노출

Behavior Definition에서 Value Help는 별도 선언이 없지만, validationdetermination을 조합해 이중 안전망을 구성하는 것이 실무 패턴입니다. Value Help는 UI 시점의 방어선이고, RAP validation은 서비스 계층의 방어선입니다.

managed implementation in class zbp_i_purchaseorder unique;
strict ( 2 );

define behavior for ZI_PurchaseOrder alias PurchaseOrder
persistent table ztb_po_head
lock master
authorization master ( instance )
etag master CreatedAt
{
  field ( readonly ) PoUuid, CreatedAt;
  field ( mandatory ) SupplierId, PlantId, OrderAmount, CurrencyCode;

  create;
  update;
  delete;

  validation validateSupplier on save
    { create; field SupplierId; }
  validation validatePlantForSupplier on save
    { create; update; field PlantId, SupplierId; }

  mapping for ztb_po_head
  {
    PoUuid       = po_uuid;
    PoNumber     = po_number;
    SupplierId   = supplier_id;
    PlantId      = plant_id;
    OrderAmount  = order_amount;
    CurrencyCode = currency_code;
    CreatedAt    = created_at;
  }
}

Behavior Implementation Class의 검증 로직은 로깅까지 포함해 다음처럼 작성합니다.

METHOD validatePlantForSupplier.
  READ ENTITIES OF ZI_PurchaseOrder IN LOCAL MODE
    ENTITY PurchaseOrder
      FIELDS ( SupplierId PlantId )
      WITH CORRESPONDING #( keys )
    RESULT DATA(lt_po).

  SELECT plant_id, supplier_id
    FROM ztb_plant
    FOR ALL ENTRIES IN @lt_po
    WHERE plant_id    = @lt_po-PlantId
      AND supplier_id = @lt_po-SupplierId
    INTO TABLE @DATA(lt_valid).

  LOOP AT lt_po INTO DATA(ls_po).
    IF NOT line_exists( lt_valid[ plant_id    = ls_po-PlantId
                                  supplier_id = ls_po-SupplierId ] ).
      APPEND VALUE #( %tky = ls_po-%tky ) TO failed-purchaseorder.
      APPEND VALUE #( %tky = ls_po-%tky
        %msg = new_message( id = 'ZPO_MSG' number = '015'
                            severity = if_abap_behv_message=>severity-error
                            v1 = ls_po-PlantId v2 = ls_po-SupplierId )
        %element-PlantId    = if_abap_behv=>mk-on
        %element-SupplierId = if_abap_behv=>mk-on
      ) TO reported-purchaseorder.
    ENDIF.
  ENDLOOP.
ENDMETHOD.

마지막으로 Service Definition과 Service Binding입니다.

@EndUserText.label: 'PurchaseOrder OData V4 Service'
define service ZUI_PurchaseOrder {
  expose ZI_PurchaseOrder as PurchaseOrder;
  expose ZI_SupplierVH    as SupplierVH;
  expose ZI_PlantVH       as PlantVH;
}

Service Binding을 OData V4 - UI로 활성화한 뒤 Fiori Elements Preview를 실행하면, SupplierId 필드에 커서를 두었을 때 F4 아이콘이 뜨고 클릭 시 SupplierVH 팝업이 열립니다. Plant 필드에서는 앞서 선택한 Supplier로 자동 필터링된 팝업이 나타나야 정상입니다.

실패 시 확인해야 할 로그와 해결 패턴

Q1. F4 아이콘 자체가 뜨지 않는다. 대개 @ObjectModel.foreignKey.association이 없거나 Association 이름 오타입니다. Association은 _Supplier처럼 언더스코어로 시작하고, 어노테이션 값에서도 정확히 같은 이름을 참조해야 합니다. Service Binding을 다시 Activate하고 브라우저 캐시를 비웠는지도 확인합니다.

Q2. 팝업은 뜨지만 결과가 항상 비어 있다. Value Help View의 where 절이 너무 타이트하거나, additionalBindinglocalElement가 실제 존재하지 않는 필드를 참조하는 경우입니다. ADT의 Data Preview로 Value Help View만 단독 실행해서 데이터가 나오는지 먼저 검증하세요.

Q3. "SADL_MSG/003: Association X is not exposed" 오류가 뜬다. 트랜잭션 View의 _Supplier Association을 SELECT 절 마지막에 노출하지 않은 경우입니다. CDS 마지막 부분에 _Supplier, _Plant처럼 반드시 나열해야 SADL 런타임이 접근할 수 있습니다.

Q4. Value Help에서 선택 후 저장하면 검증 실패가 난다. Value Help View의 필터(예: is_blocked = ' ')와 실제 저장 로직의 검증 규칙이 어긋난 경우입니다. Value Help 필터를 완화하거나, Behavior Validation 로직에 동일한 필터를 반영해 일관성을 맞춥니다.

Q5. Fiori Elements가 예전 어노테이션을 캐싱한다. Service Binding에서 Publish를 다시 누르거나, 브라우저 개발자 도구의 Application 탭에서 Local/Session Storage를 지운 뒤 재접속합니다. ABAP Cloud에서는 Service Binding의 metadata cache를 무효화하는 옵션도 있습니다.

더 파고들어 볼 만한 확장 주제

Value Help가 안정화되면 그 다음 단계로는 Collective Value Help(여러 후보 뷰를 탭으로 묶는 방식), Fixed Values(도메인 고정값에 대한 드롭다운), Text Association(SupplierId 옆에 SupplierName을 자동 표기)까지 확장할 수 있습니다. 또한 Value Help가 대량 마스터를 참조할 때의 성능 튜닝은 @ObjectModel.resultSet.sizeCategory와 CDS Buffer, SAP HANA 컬럼 인덱스 설계로 이어집니다. RAP 확장 관점에서는 Draft 활성화 시 Value Help가 Draft 데이터도 후보로 노출할지 여부, 그리고 사용자별 권한(DCL)에 따라 후보가 필터링되는 시나리오도 흥미로운 주제입니다.

댓글 0

아직 댓글이 없습니다.