📖 개요: 이 글에서 다루는 내용
비용 센터 계층, 예산 항목 트리, 프로젝트 WBS처럼 부모-자식 관계가 재귀적으로 반복되는 데이터는 엔터프라이즈 애플리케이션에서 가장 자주 등장하는 구조 중 하나입니다. 그런데 RAP(RESTful ABAP Programming Model)은 기본적으로 평면(flat) 리스트 중심으로 설계되어 있어, 트리 구조를 표현하려면 별도의 접근이 필요합니다. 이 글에서는 RAP 환경에서 계층 데이터를 구현하는 3가지 방법을 실무 시나리오(비용 센터/예산 트리) 기반 예제로 비교합니다.
- CDS Hierarchy(DEFINE HIERARCHY + Hierarchy 어노테이션) 기반 PARENT_CHILD 구현 원리 이해
- Self-Join CDS View로 고정 깊이 계층을 평면화하는 패턴 습득
- Custom Entity + ABAP 클래스로 임의 깊이 트리를 직접 구성하는 방법 확인
- 세 방법의 성능·유연성 트레이드오프를 기준으로 선택 기준 정리
📚 사전에 알아두면 좋은 내용
CDS View Entity 작성 경험(association, cardinality)과 RAP의 기본 구성(Interface View → Projection → Service Definition/Binding 흐름)을 알고 있다면 수월하게 따라올 수 있습니다. 3단계 예제에서는 if_rap_query_provider 인터페이스와 ABAP SQL의 CTE(WITH) 문법이 등장하므로, ABAP SQL 신문법에 대한 기초가 있으면 좋습니다.
🔧 환경 · 버전 · 준비물
이 글의 예제는 다음 환경을 기준으로 작성했습니다.
- SAP BTP ABAP Environment 또는 SAP S/4HANA 2022 이상 (ABAP Platform 2022+, ABAP Language Version: ABAP for Cloud Development)
DEFINE HIERARCHY(CDS Hierarchy Entity)는 ABAP Platform 7.54 이상에서 사용 가능하며, 클라우드 개발에서는 일반적으로 제한 없이 활용됩니다- 개발 도구: ADT(ABAP Development Tools) for Eclipse — CDS/RAP 개발은 SE80이 아닌 ADT에서만 가능합니다
- UI 확인용: SAP Fiori elements for OData V4 (Tree Table은 OData V4 List Report에서 지원)
- 예제 테이블:
zbudg_node(예산 노드), 필드:node_id,parent_id,node_name,budget_amount,currency
💡 핵심 개념: 재귀 데이터와 RAP의 만남
트리 구조를 조직도에 비유하면 이해가 쉽습니다. 한 명의 CEO(루트 노드) 아래 본부장, 그 아래 팀장, 팀원이 이어지는데, 테이블에는 단지 "나의 ID"와 "내 상사의 ID" 두 컬럼만 있을 뿐입니다. 즉 데이터 자체는 평면이고, 계층은 조회 시점에 재구성됩니다. 이 재구성을 누가 담당하느냐에 따라 3가지 방법이 갈립니다.
- 방법 1 — 데이터베이스가 계층을 계산: CDS Hierarchy Entity(
DEFINE HIERARCHY)는 SAP HANA의 계층 엔진에 위임하여$node.hierarchy_level,hierarchy_rank같은 계층 속성을 즉시 계산합니다. 여기에@Hierarchy.parentChild어노테이션을 결합하면 Fiori elements Tree Table이 OData V4의 계층 확장 프로토콜로 노드를 지연 로딩(lazy expand)합니다. - 방법 2 — 뷰 정의 시점에 계층을 고정: Self-Join은 "최대 3단계까지만 보여주면 된다" 같은 요건에서 뷰를 레벨 수만큼 조인해 평면 컬럼으로 펼칩니다. 재귀가 아니라 반복이므로 단순하지만, 깊이가 바뀌면 뷰를 수정해야 합니다.
- 방법 3 — ABAP 코드가 계층을 조립: Custom Entity와 Query Provider 클래스에서 CTE 기반 계층 SQL 또는 재귀 메서드로 트리를 직접 만듭니다. 정렬 규칙, 권한 필터, 집계 롤업(하위 예산 합산) 같은 커스텀 로직을 자유롭게 넣을 수 있습니다.
도식으로 정리하면 다음과 같습니다.
"[평면 테이블 zbudg_node]
" node_id | parent_id | budget_amount
" --------+-----------+--------------
" B100 | (초기값) | 1,000,000 ← 루트
" B110 | B100 | 400,000
" B111 | B110 | 150,000
"
" 방법1: DB 계층 엔진 → 임의 깊이, 표준 UI 연동
" 방법2: Self-Join → 고정 깊이, 가장 단순
" 방법3: ABAP 클래스 → 임의 깊이 + 커스텀 로직
선택 기준은 일반적으로 "표준 Tree Table UI가 필요하면 방법 1, 리포팅용 고정 깊이면 방법 2, 계층 계산에 비즈니스 로직이 섞이면 방법 3"으로 권장됩니다.
💻 실전 코드: 3단계로 구현하는 예산 트리
1단계 — 기본: CDS Hierarchy Annotation 기반 PARENT_CHILD (방법 1)
먼저 소스 뷰에 부모/자식 association을 정의합니다. 핵심은 자기 자신을 참조하는 association입니다.
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: '예산 노드 - 계층 소스 뷰'
define view entity ZI_BudgetNode
as select from zbudg_node
association [0..1] to ZI_BudgetNode as _Parent
on $projection.ParentId = _Parent.NodeId
association [0..*] to ZI_BudgetNode as _Children
on $projection.NodeId = _Children.ParentId
{
key node_id as NodeId,
parent_id as ParentId,
node_name as NodeName,
@Semantics.amount.currencyCode: 'Currency'
budget_amount as BudgetAmount,
currency as Currency,
_Parent,
_Children
}
이제 계층 엔터티를 정의합니다. child to parent association으로 방향을 지정하고, $node 의사 컬럼으로 레벨과 순번을 얻습니다.
@EndUserText.label: '예산 트리 - 계층 엔터티'
define hierarchy ZI_BudgetNodeHier
as parent child hierarchy(
source ZI_BudgetNode
child to parent association _Parent
start where ParentId is initial
siblings order by NodeId ascending
)
{
key NodeId,
ParentId,
NodeName,
BudgetAmount,
Currency,
$node.hierarchy_level as HierarchyLevel,
$node.hierarchy_rank as HierarchyRank
}
Fiori elements Tree Table 연동을 위해 소스 뷰(또는 Projection)에 재귀 어노테이션을 부여합니다.
@Hierarchy.parentChild: [{ name: 'BudgetHier',
recurse: { parent: [ 'ParentId' ],
child: [ 'NodeId' ] } }]
OData V4 서비스 바인딩 후, Fiori elements 앱의 manifest.json에서 테이블 타입과 계층 qualifier를 지정하면 확장/축소 가능한 트리가 렌더링됩니다.
"controlConfiguration": {
"@com.sap.vocabularies.UI.v1.LineItem": {
"tableSettings": {
"type": "TreeTable",
"hierarchyQualifier": "BudgetHier"
}
}
}
2단계 — 실무 시나리오: Self-Join으로 3단계 비용 센터 리포트 (방법 2)
경영 리포트처럼 "본부 → 부서 → 팀" 3단계 고정 구조라면 Self-Join이 가장 빠른 해법입니다. 각 레벨을 별도 alias로 조인하고, 존재하지 않는 하위는 left outer join으로 비워 둡니다.
@EndUserText.label: '비용 센터 3레벨 평면 뷰'
define view entity ZI_CostCenterFlat
as select from ZI_BudgetNode as Div -- 레벨1: 본부
left outer join ZI_BudgetNode as Dept -- 레벨2: 부서
on Dept.ParentId = Div.NodeId
left outer join ZI_BudgetNode as Team -- 레벨3: 팀
on Team.ParentId = Dept.NodeId
{
key Div.NodeId as DivisionId,
Div.NodeName as DivisionName,
key Dept.NodeId as DepartmentId,
Dept.NodeName as DepartmentName,
key Team.NodeId as TeamId,
Team.NodeName as TeamName,
@Semantics.amount.currencyCode: 'Currency'
Team.BudgetAmount as TeamBudget,
Team.Currency as Currency
}
where Div.ParentId is initial -- 루트만 시작점으로
실무에서는 잘못된 데이터(자기 자신을 부모로 갖는 노드)가 유입될 수 있으므로, RAP validation으로 저장 시점에 차단하는 것이 좋습니다. 로깅까지 포함한 예입니다.
METHOD validateParent. " Behavior Pool 내 validation 구현
READ ENTITIES OF zi_budgetnode_tp IN LOCAL MODE
ENTITY BudgetNode
FIELDS ( NodeId ParentId ) WITH CORRESPONDING #( keys )
RESULT DATA(lt_nodes).
LOOP AT lt_nodes INTO DATA(ls_node)
WHERE NodeId = ParentId AND ParentId IS NOT INITIAL.
APPEND VALUE #( %tky = ls_node-%tky ) TO failed-budgetnode.
APPEND VALUE #( %tky = ls_node-%tky
%msg = new_message( id = 'ZBUDG_MSG'
number = '001'
severity = if_abap_behv_message=>severity-error
v1 = ls_node-NodeId )
) TO reported-budgetnode.
" 애플리케이션 로그 기록 (순환 참조 시도 추적)
DATA(lo_log) = cl_bali_log=>create_with_header(
header = cl_bali_header_setter=>create( object = 'ZBUDG' subobject = 'TREE' ) ).
ENDLOOP.
ENDMETHOD.
3단계 — 프로덕션: Custom Entity + ABAP 클래스 계층 조립 (방법 3)
깊이를 알 수 없고, 하위 예산 합산(롤업) 같은 계산이 필요하다면 Custom Entity를 사용합니다. Query Provider에서 CTE 기반 계층 SQL로 성능을 확보하고, 페이징을 반드시 반영합니다.
@EndUserText.label: '예산 트리 - Custom Entity'
@ObjectModel.query.implementedBy: 'ABAP:ZCL_BUDGET_TREE_QUERY'
define custom entity ZC_BudgetTree
{
key NodeId : abap.char(10);
ParentId : abap.char(10);
NodeName : abap.char(60);
TreeLevel : abap.int4;
RolledUpAmt : abap.curr(15,2);
Currency : abap.cuky;
}
CLASS zcl_budget_tree_query DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_rap_query_provider.
ENDCLASS.
CLASS zcl_budget_tree_query IMPLEMENTATION.
METHOD if_rap_query_provider~select.
DATA(lv_top) = io_request->get_paging( )->get_page_size( ).
DATA(lv_skip) = io_request->get_paging( )->get_offset( ).
IF lv_top = if_rap_query_paging=>page_size_unlimited.
lv_top = 500. " 프로덕션 가드: 무제한 요청 방지
ENDIF.
" CTE + 계층 함수로 DB에서 트리 전개 (임의 깊이)
WITH
+tree AS (
SELECT FROM HIERARCHY(
SOURCE zi_budgetnode
CHILD TO PARENT ASSOCIATION _parent
START WHERE parentid IS INITIAL
SIBLINGS ORDER BY nodeid )
FIELDS nodeid, parentid, nodename,
budgetamount, currency,
hierarchy_level AS treelevel )
SELECT FROM +tree
FIELDS nodeid AS NodeId, parentid AS ParentId,
nodename AS NodeName, treelevel AS TreeLevel,
budgetamount AS RolledUpAmt, currency AS Currency
ORDER BY treelevel, nodeid
INTO TABLE @DATA(lt_result)
UP TO @lv_top ROWS OFFSET @lv_skip.
io_response->set_total_number_of_records( lines( lt_result ) ).
io_response->set_data( lt_result ).
ENDMETHOD.
ENDCLASS.
보안 측면에서 Custom Entity는 @AccessControl.authorizationCheck: #NOT_REQUIRED가 아니라 DCL 또는 코드 내 AUTHORITY-CHECK로 노드 접근 권한을 반드시 검증하는 것이 권장됩니다. 테스트는 cl_rap_query_provider_test 계열 더블로 Query Provider를 단위 테스트할 수 있으며, 순환 참조·고아 노드(orphan)·단일 루트 케이스를 테스트 데이터로 고정해 두면 회귀를 예방할 수 있습니다.
⚠️ 흔한 실수와 트러블슈팅
Q1. DEFINE HIERARCHY 활성화 시 "association not usable" 오류가 납니다.
계층 엔터티의
child to parent association은 소스 뷰 안에서 자기 자신([0..1] 카디널리티)을 가리켜야 합니다._Children([0..*])을 지정했거나, association ON 조건에 키가 아닌 필드를 썼는지 확인하세요.
Q2. Fiori elements에서 Tree Table 대신 일반 테이블이 나옵니다.
Tree Table은 OData V4 + Fiori elements에서 동작합니다. V2 서비스 바인딩을 쓰고 있지 않은지,
manifest.json의hierarchyQualifier가@Hierarchy.parentChild의name과 정확히 일치하는지 확인하세요. Draft 기반 편집 트리는 최신 릴리스에서 단계적으로 지원되므로 사용 중인 버전의 제약을 확인해야 합니다.
Q3. 데이터에 순환 참조(A→B→A)가 생기면 어떻게 되나요?
HANA 계층 엔진은 사이클을 감지하면 런타임 오류를 내거나 해당 분기를 잘라냅니다. 근본 대책은 2단계 예제처럼 저장 시점 validation으로 차단하는 것이며, 기존 데이터 정리는 CTE로 사이클 노드를 탐지하는 점검 리포트를 권장합니다.
Q4. Self-Join 뷰가 느립니다.
레벨 수만큼 조인이 늘어나므로
parent_id에 보조 인덱스를 두고, 루트 필터(ParentId is initial)를 뷰 안에 넣어 조인 대상 자체를 줄이는 것이 일반적입니다. 5레벨 이상이면 방법 1 또는 3으로 전환을 검토하세요.
🚀 더 나아가기
트리 조회를 넘어 편집 가능한 계층으로 확장하려면 RAP Draft + Tree Table 편집 시나리오, 노드 이동(재부모화) 액션 설계를 살펴보세요. 또한 @Hierarchy.parentChild와 Analytical Query(@Analytics.query)를 결합하면 예산 롤업 집계를 분석 앱으로 확장할 수 있습니다. 대용량 계층이라면 HANA의 hierarchy 함수(HIERARCHY_DESCENDANTS 등)를 AMDP로 직접 호출하는 패턴도 다음 주제로 좋습니다. 마지막으로 RAP EML 기반 단위 테스트로 트리 액션의 회귀 테스트 체계를 갖추는 것을 권장합니다.
📚 함께 보면 좋은 자료
- ABAP CDS - DEFINE HIERARCHY (SAP Help Portal, ABAP Keyword Documentation)
- ABAP SQL - SELECT FROM HIERARCHY / CTE 계층 조회 (SAP Help Portal)
- ABAP RESTful Application Programming Model 개발 가이드 (SAP Help Portal)
- CDS Hierarchy Annotations (@Hierarchy.parentChild) 레퍼런스 (SAP Help Portal)
- SAPUI5 - Tree Table 문서 (Fiori elements OData V4)
- SAP Community - ABAP/RAP 계층 구현 관련 블로그 및 Q&A
댓글 0
아직 댓글이 없습니다.