News

CDS Hierarchy 계층 정의 실수 3가지 #shorts #SAP #ABAP

▶ YouTube에서 보기

1. 개요 및 이 글의 목표

조직도, 자재명세서(BOM), 비용 센터, 카테고리 트리처럼 "한 행이 다른 행을 부모로 가리키는" 데이터는 전통적인 SQL JOIN만으로 다루기 매우 까다롭습니다. 깊이가 가변적이고, 재귀적으로 모든 후손을 찾거나 특정 노드의 모든 조상을 끌어와야 하는 요구사항이 끊임없이 등장하기 때문입니다. ABAP CDS는 이런 문제를 정면으로 해결하기 위해 DEFINE HIERARCHY 구문과 HIERARCHY_ANCESTORS, HIERARCHY_DESCENDANTS 같은 계층 내비게이션 함수를 제공합니다.

본 글에서는 ABAP CDS Hierarchy를 처음 다루는 개발자가 실무에 즉시 적용할 수 있도록 다음을 차례로 살펴봅니다.

  • CDS Hierarchy가 일반 CDS 뷰와 어떻게 다른지, 어떤 문제를 해결하는지
  • DEFINE HIERARCHY 구문으로 부모-자식 연관(association)을 선언하는 방법
  • START WHERE, SIBLINGS ORDER BY, DEPTH 등 핵심 파라미터의 의미
  • HANA에서 제공하는 계층 순회 함수들을 ABAP SQL 또는 CDS 뷰 엔티티에서 호출하는 패턴
  • 순환 참조, NULL 부모 처리 같은 함정과 회피 전략
  • 조직도/BOM/비용 센터 시나리오의 실전 응용 예제

2. 핵심 개념 — CDS Hierarchy란 무엇인가

일반적인 CDS 뷰는 테이블이나 다른 뷰의 행을 평면적으로 SELECT 하는 SQL의 추상화입니다. JOIN으로 여러 테이블을 묶을 수 있지만, "이 사원의 모든 상위 관리자" 같은 가변 깊이 질의는 표현 자체가 불가능합니다. 반면 CDS Hierarchy는 평면 테이블(예: employee_id, manager_id 두 컬럼을 가진 직원 테이블)을 입력으로 받아, 그 안의 부모-자식 관계를 트리 구조로 인식하는 별도의 엔티티 종류입니다.

비유하자면 일반 CDS 뷰가 "엑셀 표"라면, CDS Hierarchy는 그 엑셀의 두 컬럼을 보고 자동으로 만든 "윈도우 탐색기 트리"와 같습니다. 트리로 인식되고 나면 노드별 레벨(hierarchy_level), 부모 ID(hierarchy_parent_node), 루트로부터의 거리, 형제 순서 등의 메타 정보가 자동으로 부여됩니다.

도식:
Flat Table (node, parent_node)DEFINE HIERARCHY → Tree (root → children → grandchildren …) → HIERARCHY_ANCESTORS / HIERARCHY_DESCENDANTS 함수로 부분 트리 추출

CDS Hierarchy는 SAP HANA의 계층 처리 엔진을 ABAP 레이어에서 사용할 수 있도록 노출한 것이며, S/4HANA 환경(일반적으로 7.55 이상의 ABAP Platform, ABAP 1909 이상)에서 권장됩니다. 평면 테이블이 가진 부모-자식 관계를 한 번 선언해 두면, 이후 어떤 ABAP SQL이든 그 트리를 표준 SQL 인터페이스로 질의할 수 있다는 점이 가장 큰 장점입니다.

3. 1단계: DEFINE HIERARCHY 구문으로 소스 엔티티와 부모-자식 연관 선언

가장 단순한 시나리오로 직원 테이블 ZEMPLOYEE가 있다고 가정합니다. 컬럼은 employee_id(PK), manager_id(자기 자신을 참조), name, dept입니다. 이 평면 테이블을 트리로 인식하려면 부모-자식 연관을 가진 베이스 CDS 뷰가 필요합니다.

@AccessControl.authorizationCheck: #NOT_REQUIRED
define view entity ZI_Employee
  as select from zemployee as e
  association [0..1] to ZI_Employee as _Manager
    on $projection.manager_id = _Manager.employee_id
{
  key e.employee_id,
      e.manager_id,
      e.name,
      e.dept,
      _Manager
}

핵심은 _Manager 셀프 연관입니다. 한 행이 자기 자신과 같은 엔티티의 다른 행(상위 관리자)을 가리키는 것을 명시했고, 이 연관이 곧 트리의 간선(edge)이 됩니다. 이제 이 베이스 뷰를 소스로 CDS Hierarchy를 정의합니다.

define hierarchy ZH_EmployeeOrgChart
  with parameters
    p_root_dept : abap.char(10)
  as parent child hierarchy(
    source ZI_Employee
    child to parent association _Manager
    start where dept = :p_root_dept
              and manager_id is initial
    siblings order by name ascending
  )
{
  key employee_id,
      manager_id,
      name,
      dept
}

이 구문 하나에서 다음이 동시에 일어납니다. 첫째, source로 어떤 평면 엔티티를 트리로 해석할지 지정합니다. 둘째, child to parent association으로 어느 셀프 연관이 "자식 → 부모" 방향인지 지정합니다(반대 방향이라면 parent to child association을 씁니다). 셋째, start where로 루트 노드 조건을 지정합니다. 넷째, siblings order by로 같은 부모를 가진 형제 노드의 순서를 정의합니다.

4. 2단계: HIERARCHY 파라미터 — START WHERE와 PARENT CHILD ASSOCIATION 설정

START WHERE는 트리의 뿌리(root)를 결정하는 가장 중요한 파라미터입니다. 일반적으로는 "부모가 비어 있는 노드"를 루트로 삼지만, 시나리오에 따라 특정 부서/회사 코드/날짜 범위로 좁힐 수 있습니다.

define hierarchy ZH_CostCenter
  with parameters
    p_controlling_area : kokrs
  as parent child hierarchy(
    source ZI_CostCenter
    child to parent association _ParentCC
    start where controlling_area = :p_controlling_area
              and parent_cc is initial
    siblings order by cost_center ascending
    multiple parents not allowed
  )
{
  key controlling_area,
  key cost_center,
      parent_cc,
      description
}

multiple parents not allowed 옵션은 한 노드가 여러 부모를 가질 수 없도록 강제합니다. 비용 센터처럼 엄격한 단일 부모 모델에 적합합니다. 반대로 BOM처럼 한 부품이 여러 상위 제품에 속하는 경우는 이 옵션을 생략해 다중 부모를 허용해야 합니다.

DEPTH 파라미터는 최대 깊이를 제한합니다. 예를 들어 깊이가 매우 큰 카테고리 트리에서 일정 레벨까지만 가져오고 싶을 때 사용합니다. 또한 CYCLES BREAKUP 또는 CYCLES ERROR로 순환 참조 발견 시 처리 정책을 선언할 수 있습니다. CYCLES BREAKUP은 순환을 만나면 해당 간선을 끊고 계속 진행하며, CYCLES ERROR는 런타임 예외를 발생시킵니다. 데이터 품질을 보장하기 어려운 마스터 데이터에는 BREAKUP이 안전하고, 무결성이 보장된 환경에서는 ERROR가 디버깅에 유리합니다.

5. 3단계: SIBLINGS ORDER BY로 같은 레벨 노드 정렬

트리를 화면에 표시할 때 가장 흔히 마주치는 요구는 "같은 부모를 가진 자식들을 일정 기준으로 정렬"하는 것입니다. SIBLINGS ORDER BY는 이 요구를 트리 정의 자체에 박아 둡니다.

define hierarchy ZH_BomExplosion
  with parameters
    p_plant : werks_d
  as parent child hierarchy(
    source ZI_BomItem
    child to parent association _ParentBom
    start where plant = :p_plant
              and parent_material is initial
    siblings order by sort_string ascending,
                      component_qty descending
    depth 20
    cycles breakup
  )
{
  key plant,
  key bom_node,
      parent_material,
      component,
      component_qty,
      sort_string
}

정렬 키를 여러 개 지정하면 1순위, 2순위 순으로 적용됩니다. 위 예에서는 BOM 정렬 문자열을 우선하고, 같은 정렬 문자열 안에서는 수량이 큰 부품을 위로 올립니다. 이렇게 트리 정의에 정렬을 포함시키면, 화면 컴포넌트가 별도로 정렬 로직을 가질 필요가 없고 OData 응답 순서도 일관됩니다.

siblings order byHIERARCHY_DESCENDANTS 함수가 결과를 반환할 때 같은 부모를 가진 형제들의 순서에 영향을 줍니다. 단, 서로 다른 부모를 가진 노드 사이의 절대 순서는 트리 구조에 따라 결정되며 임의로 흔들 수 없다는 점을 기억해야 합니다.

6. 계층 순회 — HIERARCHY_ANCESTORS, HIERARCHY_DESCENDANTS 함수 활용

계층을 선언했다면 이제 ABAP SQL에서 트리의 부분을 잘라내 사용합니다. 가장 자주 쓰는 두 함수는 HIERARCHY_DESCENDANTS(특정 노드 아래 후손)와 HIERARCHY_ANCESTORS(특정 노드 위 조상)입니다.

SELECT employee_id, manager_id, name, dept,
       hierarchy_rank,
       hierarchy_tree_size,
       hierarchy_parent_rank,
       hierarchy_level,
       hierarchy_is_cycle,
       hierarchy_is_orphan
  FROM HIERARCHY_DESCENDANTS(
         SOURCE ZH_EmployeeOrgChart( p_root_dept = 'SALES' )
         START WHERE employee_id = @iv_manager_id
         DISTANCE FROM 0 TO 3 )
  INTO TABLE @DATA(lt_team).

DISTANCE FROM 0 TO 3는 시작 노드 자기 자신(0)부터 3단계 아래까지만 가져오라는 의미입니다. 결과 행에는 평면 컬럼과 함께 hierarchy_rank(트리 깊이 우선 순회 순번), hierarchy_level(루트 기준 레벨), hierarchy_is_cycle(순환 간선 여부) 같은 계층 메타 정보가 함께 따라옵니다.

SELECT employee_id, name, dept, hierarchy_level
  FROM HIERARCHY_ANCESTORS(
         SOURCE ZH_EmployeeOrgChart( p_root_dept = 'SALES' )
         START WHERE employee_id = @iv_employee_id
         DISTANCE FROM 1 TO 99 )
  ORDER BY hierarchy_level DESCENDING
  INTO TABLE @DATA(lt_reporting_line).

조상 함수는 보고 라인(reporting line) 표시, 권한 상속 계산, 비용 합산 시 상위 비용센터 식별 등에 활용됩니다. 또한 HIERARCHY_SIBLINGS를 사용하면 특정 노드와 같은 부모를 가진 노드들만 추려낼 수 있고, HIERARCHY 함수 자체로는 트리 전체를 평면화해 반환받을 수 있습니다.

SELECT node_id, parent_id, hierarchy_level, hierarchy_rank
  FROM HIERARCHY( SOURCE ZH_CostCenter( p_controlling_area = '1000' ) )
  WHERE hierarchy_level <= 4
  ORDER BY hierarchy_rank;

7. 자주 겪는 함정 — 순환 참조, NULL 부모 처리, DEPTH 제한

실무에서 CDS Hierarchy를 도입할 때 마주치는 함정은 거의 정해져 있습니다.

Q1. "Hierarchy not directed acyclic" 같은 런타임 오류가 납니다.
원본 데이터에 순환 참조가 있다는 신호입니다. 예를 들어 A의 부모가 B인데 B의 부모가 다시 A로 설정된 경우입니다. CYCLES BREAKUP을 선언하면 런타임이 순환 간선을 끊고 진행하지만, 근본적으로는 데이터 정합성 점검 리포트를 함께 운영하는 것이 권장됩니다. hierarchy_is_cycle 컬럼을 SELECT 해서 어떤 노드가 순환 후보인지 식별할 수 있습니다.

Q2. 루트가 잡히지 않습니다.
START WHERE manager_id IS INITIAL로 선언했는데 결과가 비어 있다면, 원본 컬럼이 실제로 빈 문자열이 아니라 공백 한 칸, 더미 값('00000000') 같은 형태로 들어가 있을 가능성이 큽니다. 일반적으로 ETL 단계에서 NULL/공백/0을 표준화해 두는 것을 권장합니다.

Q3. DEPTH가 깊을 때 성능이 떨어집니다.
가능한 한 DEPTHDISTANCE FROM ... TO ...로 필요한 깊이만 자르고, START WHERE를 좁게 두는 것이 성능에 결정적입니다. 또한 베이스 뷰의 부모-자식 컬럼에 인덱스가 있어야 HANA 옵티마이저가 효율적으로 동작합니다.

Q4. 다중 부모 노드가 중복 집계됩니다.
BOM처럼 다중 부모를 허용하면 한 부품이 두 번 이상 나타날 수 있습니다. 비용 합계를 낼 때는 hierarchy_rank 기준으로 중복 제거를 하거나, 분기별로 가중치를 부여해야 합니다.

8. 응용 패턴 — 조직도, BOM, 비용 센터 계층 구조 실전 예제

마지막으로 세 가지 대표 시나리오를 한데 모아 봅니다. 첫째, 조직도(employee → manager)에서 특정 임원 아래 전체 인원을 카운트하는 패턴입니다.

SELECT COUNT(*) AS team_size
  FROM HIERARCHY_DESCENDANTS(
         SOURCE ZH_EmployeeOrgChart( p_root_dept = 'ALL' )
         START WHERE employee_id = @iv_vp_id )
  INTO @DATA(lv_team_size).

둘째, BOM 폭발(BOM explosion)을 통한 부품 총 소요량 계산입니다. 다중 부모를 허용하고, 부모 노드에서 자식 노드로 내려가며 수량을 누적합니다.

SELECT component,
       SUM( component_qty ) AS total_qty
  FROM HIERARCHY_DESCENDANTS(
         SOURCE ZH_BomExplosion( p_plant = '1000' )
         START WHERE component = @iv_finished_product )
  WHERE hierarchy_is_cycle = ' '
  GROUP BY component
  INTO TABLE @DATA(lt_required_parts).

셋째, 비용 센터 계층에서 특정 비용 센터의 모든 조상을 끌어와 "롤업 대상" 비용 센터를 식별하는 패턴입니다. 이는 권한 점검이나 예산 합산에 흔히 쓰입니다.

SELECT cost_center, parent_cc, hierarchy_level
  FROM HIERARCHY_ANCESTORS(
         SOURCE ZH_CostCenter( p_controlling_area = '1000' )
         START WHERE cost_center = @iv_cost_center
         DISTANCE FROM 0 TO 99 )
  ORDER BY hierarchy_level
  INTO TABLE @DATA(lt_rollup_chain).

세 시나리오 모두 트리 정의는 한 번뿐이고, 호출 측은 START WHEREDISTANCE만 바꿔 가며 다양한 질의를 표현합니다. 이것이 CDS Hierarchy가 가져다 주는 가장 큰 실무 이점입니다. 평면 테이블을 트리로 한 번 "선언"해 두면, 이후의 모든 트리 질의는 표준 SQL의 어휘로 표현되고, 옵티마이저는 HANA의 계층 처리 엔진을 활용해 효율적으로 실행합니다.

조직 변경이 잦은 환경에서는 START WHERE 파라미터에 유효일자(valid_from, valid_to)를 포함시켜 시점별 트리를 만들 수도 있습니다. 동일한 원본 테이블 위에 여러 Hierarchy 엔티티를 정의해, 시나리오마다 다른 루트 조건, 다른 정렬, 다른 깊이 정책을 가진 트리를 공존시키는 것이 일반적인 모델링 전략입니다. 이로써 화면 컴포넌트, OData 서비스, 분석 리포트가 모두 동일한 트리 정의를 재사용하며 일관된 결과를 보장하게 됩니다.

댓글 0

아직 댓글이 없습니다.