ABAP

WBS 연결 실수 3가지 #shorts #SAP #ABAP

▶ YouTube에서 보기

개요 및 이 글에서 다루는 범위

SAP S/4HANA의 프로젝트 시스템(PS, Project System) 모듈은 대형 투자 프로젝트, R&D 프로젝트, 서비스 프로젝트 관리의 핵심입니다. 그 중심에는 PROJ 테이블에 저장되는 프로젝트 정의(Project Definition)가 있으며, ABAP CDS 뷰 I_ProjectDefinition은 이 데이터를 표준화된 방식으로 노출하는 Basic Interface View입니다. 이 글에서는 I_ProjectDefinition의 구조, PROJ와의 매핑, WBS 요소(I_WBSElement)와의 연결 관계, 그리고 실제 리포트·OData 서비스에서 활용하는 방법을 단계적으로 살펴봅니다.

  • I_ProjectDefinition의 필드 구성과 PROJ 원본 필드 매핑 이해
  • 프로젝트 헤더와 WBS 계층 구조의 조인 패턴 습득
  • CDS Consumption View 작성 및 어노테이션 적용 실전 예제
  • OData 서비스 노출과 성능 고려사항 확인
  • PS 데이터 조회 시 흔히 발생하는 권한·필터 이슈 대응

사전에 알고 있어야 할 배경 지식

이 글을 따라가려면 ABAP CDS 뷰 정의 문법(DEFINE VIEW, association, @ObjectModel 어노테이션)에 대한 기본적인 이해가 필요합니다. 또한 SAP PS 모듈의 개념 — 프로젝트 정의(Project Definition), WBS Element, Network, Activity의 위계 — 를 알고 있으면 흐름을 잡기 쉽습니다. Eclipse ADT(ABAP Development Tools) 사용 경험과 SE11로 PROJ, PRPS 테이블 구조를 확인해본 경험이 있다면 이상적입니다.

환경 및 시스템 요구사항

I_ProjectDefinition은 S/4HANA 릴리스에서 표준으로 제공되는 Foundation Layer의 Basic Interface View입니다. 아래 조건을 권장합니다.

  • SAP S/4HANA: 1809 이상 (2020, 2021, 2022, 2023 릴리스에서 안정적으로 제공되며, 릴리스별 필드 추가·비활성화가 있을 수 있음)
  • ABAP Platform: 7.54 이상 권장 (CDS Entity, Released API 어노테이션 지원)
  • 개발 도구: Eclipse 2022-06 이상 + ABAP Development Tools 3.28+
  • PS 모듈 활성화: SPRO에서 Project System 기본 설정이 완료된 클라이언트
  • 테스트 데이터: T-code CJ20N 또는 CJ01로 최소 1건 이상의 프로젝트 정의가 생성되어 있어야 함
  • 권한: S_PROJECT, C_PROJECT 등 PS 관련 권한 오브젝트에 대한 표시 권한(ACTVT 03)

또한 릴리스에 따라 I_ProjectDefinition과 별개로 I_EnterpriseProject(Enterprise Project 관리용) 뷰가 병존합니다. 이 글에서는 클래식 PS 기반의 I_ProjectDefinition에 초점을 맞춥니다.

핵심 개념: PROJ 테이블과 CDS Interface View의 관계

SAP PS의 데이터 모델은 크게 3개 계층으로 구성됩니다. 이 계층 구조를 이해하면 I_ProjectDefinition이 왜 "헤더" 역할을 하는지 명확해집니다.

비유하자면: 프로젝트 정의는 책의 표지, WBS는 목차, Network/Activity는 각 챕터의 세부 절과 같습니다. 표지(PROJ)가 있어야 목차(PRPS)가 존재할 수 있고, 목차가 있어야 세부 절(AFVC)이 자리 잡습니다.

클래식 PS의 물리 테이블은 다음과 같이 분포합니다.

  • PROJ: 프로젝트 정의 헤더. 프로젝트 코드(PSPID), 내부 번호(PSPNR), 프로젝트 프로파일(PROFL), 계획 시작·종료일, 담당자, 통화 등을 저장
  • PRPS: WBS Element 마스터. 내부 번호(PSPNR)와 프로젝트 헤더의 PSPNR이 조인 키
  • PRHI: WBS 계층 관계 (상위-하위)
  • PROJS: 프로젝트 상태 (시스템/사용자 상태 프로파일)

I_ProjectDefinition은 이 중 PROJ를 데이터 소스로 삼아 필드명을 시맨틱하게 재명명하고, 관련 마스터(회사 코드, 통제 영역, 프로젝트 프로파일 등)로 향하는 association을 정의합니다. 대표적인 필드 매핑은 아래와 같습니다.

  • Project (외부 프로젝트 ID) ← PROJ.PSPID
  • ProjectInternalIDPROJ.PSPNR
  • ProjectDescriptionPROJ.POST1
  • ResponsibleCostCenterPROJ.KOSTV
  • CompanyCodePROJ.PBUKR
  • ControllingAreaPROJ.PKOKR
  • PlannedStartDatePROJ.PLSEZ
  • PlannedEndDatePROJ.PLEDD

WBS 연결은 association _WBSElement를 통해 이뤄지며, 조인 키는 프로젝트 내부 번호(ProjectInternalID = PRPS.PSPHI)입니다. 즉 사용자가 보는 프로젝트 코드(PSPID)와 내부 번호(PSPNR)가 분리되어 있는 구조를 이해해야 조인이 정확해집니다.

또한 I_ProjectDefinition은 Basic Interface View 계층이기 때문에 UI 어노테이션이 최소화되어 있습니다. 실제 앱이나 리포트에서는 이 뷰 위에 Consumption View(C_...) 또는 Custom CDS View를 쌓아 UI/OData 어노테이션을 부여하는 것이 일반적입니다. VDM(Virtual Data Model)의 계층 원칙 — Basic → Composite → Consumption — 을 따르는 것이 유지보수 관점에서 권장됩니다.

단계별 실전 예제 1: 기본 조회 CDS 뷰 작성

첫 번째 예제는 I_ProjectDefinition을 소스로 하여, 특정 회사 코드에 속한 프로젝트 목록을 조회하는 간단한 CDS 뷰입니다. 필드명을 실무 리포트에 적합한 이름으로 재명명하고, 프로젝트 설명이 비어있지 않은 건만 노출합니다.

@AbapCatalog.sqlViewName: 'ZCV_PRJHDR_BAS'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Project Header - Basic View'
define view Z_ProjectHeaderBasic
  as select from I_ProjectDefinition
{
  key Project                as ProjectCode,
      ProjectInternalID      as InternalID,
      ProjectDescription     as Description,
      CompanyCode            as CompanyCode,
      ControllingArea        as ControllingArea,
      ResponsibleCostCenter  as OwnerCostCenter,
      PlannedStartDate       as StartDate,
      PlannedEndDate         as EndDate,
      ProjectProfileCode     as ProfileCode
}
where ProjectDescription <> ''
  and CompanyCode = '1010';

이 뷰를 Eclipse ADT에서 활성화한 뒤 F8(Data Preview)로 실행하면, 회사 코드 1010에 등록된 프로젝트 헤더만 나열됩니다. 여기서 Project 필드는 사용자가 화면에서 입력하는 외부 프로젝트 ID(예: E-2026-001)이고, ProjectInternalID는 8자리 내부 넘버(예: 00000123)입니다. 두 값의 구분을 명확히 유지하는 것이 이후 WBS 조인에서 오류를 예방합니다.

단계별 실전 예제 2: WBS 조인 및 유효 프로젝트 필터링 (실무 시나리오)

실무에서는 프로젝트 헤더만 봐서는 의미가 부족합니다. WBS Element별 계획 원가, 상태, 담당자를 함께 보고 싶은 요구가 많습니다. 아래 예제는 I_ProjectDefinitionI_WBSElement를 association으로 연결하고, 현재 시점에 유효한(계획 종료일이 오늘 이후인) 프로젝트만 필터링합니다.

@AbapCatalog.sqlViewName: 'ZCV_PRJWBS_CMP'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Active Project with WBS Composite'
@VDM.viewType: #COMPOSITE
define view Z_ActiveProjectWithWBS
  as select from I_ProjectDefinition as Header
    association [0..*] to I_WBSElement as _WBS
      on \$projection.InternalID = _WBS.ProjectInternalID
{
  key Header.Project                as ProjectCode,
      Header.ProjectInternalID      as InternalID,
      Header.ProjectDescription     as Description,
      Header.CompanyCode            as CompanyCode,
      Header.PlannedStartDate       as StartDate,
      Header.PlannedEndDate         as EndDate,
      Header.ResponsibleCostCenter  as OwnerCostCenter,
      cast( '' as abap.char( 1 ) )  as ProjectStatus,
      _WBS
}
where Header.PlannedEndDate >= \$session.system_date
  and Header.ProjectDescription <> '';

이제 이 뷰를 소비하는 ABAP 프로그램에서 특정 프로젝트의 WBS를 함께 읽어올 수 있습니다. 예를 들어, 코스트 센터 K-4200에 할당된 유효 프로젝트와 그 하위 WBS를 SELECT하는 코드는 다음과 같습니다.

REPORT z_active_project_report.

DATA: lt_projects TYPE STANDARD TABLE OF z_active_project_with_wbs,
      lv_target   TYPE kostl VALUE 'K-4200'.

TRY.
    SELECT ProjectCode,
           Description,
           StartDate,
           EndDate,
           OwnerCostCenter
      FROM z_active_project_with_wbs
      WHERE OwnerCostCenter = @lv_target
      ORDER BY ProjectCode
      INTO TABLE @DATA(lt_hdr).

    LOOP AT lt_hdr INTO DATA(ls_hdr).
      SELECT WBSElement,
             WBSDescription,
             ResponsibleCostCenter,
             PlannedTotalCosts
        FROM I_WBSElement
        WHERE ProjectInternalID =
              ( SELECT ProjectInternalID FROM I_ProjectDefinition
                WHERE Project = @ls_hdr-ProjectCode )
        INTO TABLE @DATA(lt_wbs).

      cl_demo_output=>write( ls_hdr ).
      cl_demo_output=>write( lt_wbs ).
    ENDLOOP.

    cl_demo_output=>display( ).

  CATCH cx_sy_open_sql_db INTO DATA(lx_sql).
    MESSAGE lx_sql->get_text( ) TYPE 'E'.
ENDTRY.

여기서 주의할 점은 PlannedEndDate가 초기값(0000-00-00 또는 공란)일 때 필터링에서 누락될 수 있다는 것입니다. 실무에서는 coalesce() 처리나 별도 뷰 확장을 통해 방어 로직을 추가하는 편이 안전합니다.

단계별 실전 예제 3: OData 서비스 노출과 성능 고려

프로덕션 환경에서는 CDS 뷰를 OData 서비스로 노출하여 Fiori 앱, 외부 시스템, SAP BTP 서비스에서 소비하는 사례가 많습니다. 아래는 Consumption View에 OData 어노테이션을 부여하고 Service Binding을 준비하는 예입니다.

@AbapCatalog.sqlViewName: 'ZCV_PRJOD_CON'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Project Header Consumption'
@OData.publish: true
@UI.headerInfo: { typeName: 'Project',
                  typeNamePlural: 'Projects',
                  title: { value: 'ProjectCode' } }
@VDM.viewType: #CONSUMPTION
@Search.searchable: true
define view entity Z_C_ProjectOverview
  as select from Z_ActiveProjectWithWBS
{
  @UI.lineItem: [{ position: 10, label: 'Project' }]
  @UI.selectionField: [{ position: 10 }]
  @Search.defaultSearchElement: true
  key ProjectCode,

  @UI.lineItem: [{ position: 20, label: 'Description' }]
  Description,

  @UI.lineItem: [{ position: 30, label: 'Cost Center' }]
  @UI.selectionField: [{ position: 20 }]
  OwnerCostCenter,

  @UI.lineItem: [{ position: 40, label: 'Start' }]
  StartDate,

  @UI.lineItem: [{ position: 50, label: 'End' }]
  EndDate,

  CompanyCode,
  InternalID
}

이후 SEGW 또는 RAP(Service Definition + Service Binding)를 통해 /sap/opu/odata/sap/Z_C_PROJECTOVERVIEW_CDS/ 형태로 노출하면 됩니다. RAP 방식에서는 다음과 같은 Service Definition을 작성합니다.

@EndUserText.label: 'Project Overview Service'
define service Z_SD_ProjectOverview {
  expose Z_C_ProjectOverview as ProjectOverview;
}

성능 관점에서 몇 가지를 짚고 넘어갑니다. 첫째, I_ProjectDefinition은 클라이언트 필드(MANDT)를 내부적으로 처리하지만, PROJ는 대형 사이트에서 수십만 건 이상이 될 수 있습니다. WHERE 조건 없이 전체 조회하면 응답 시간이 급격히 늘어나므로 CompanyCode, ProjectProfileCode, 날짜 범위 등의 필터를 기본값으로 요구하는 것이 일반적으로 권장됩니다. 둘째, WBS 조인은 카티전 확장이 크기 때문에 OData의 \$expand=_WBS 사용 시 페이징(\$top, \$skip)을 반드시 병행해야 합니다. 셋째, 반복 조회가 많은 API라면 CDS View Entity + Buffer는 지원되지 않으므로, 별도 캐시 계층(예: AMDP 프로시저 + Shared Memory)을 고려할 수 있습니다.

흔한 실수와 대응 방법

PS 데이터를 다루면서 반복적으로 발생하는 이슈들을 정리합니다.

  • PSPID vs PSPNR 혼동: PSPID는 사용자 입력 코드, PSPNR은 내부 넘버입니다. WBS(PRPS.PSPHI)와 조인할 때는 반드시 PSPNR을 사용해야 합니다. PSPID로 조인하면 데이터가 아예 나오지 않거나 잘못 매칭됩니다.
  • 권한 부족으로 빈 결과: @AccessControl.authorizationCheck: #CHECK 상태에서 사용자에게 프로젝트 프로파일이나 회사 코드 권한이 없으면 SELECT는 정상 종료되지만 결과가 0건입니다. ST01 트레이스나 SU53으로 확인하세요.
  • 클라이언트 종속성: PROJ는 mandant 종속 테이블이므로 시스템 간 데이터 이관 시 클라이언트 매핑을 반드시 확인해야 합니다.
  • 날짜 초기값 필터 실패: PlannedEndDate = '00000000'인 프로젝트가 >= sy-datum 조건에서 누락되지 않도록 별도 처리 필요.

FAQ

  • Q1. I_ProjectDefinition과 I_EnterpriseProject의 차이는?
    전자는 클래식 PS(PROJ 기반), 후자는 S/4HANA의 Enterprise Project Management(EPM, /EPM/ 네임스페이스) 기반입니다. 조직이 어떤 프로젝트 모델을 채택했는지에 따라 선택합니다.
  • Q2. Custom 필드를 추가하려면?
    PROJ에 CI_INCLUDE로 append 구조를 만들고, Custom CDS View에서 I_ProjectDefinitionPROJ를 조인해 확장 필드를 노출하는 패턴이 일반적입니다. S/4HANA Key User Extensibility에서는 Custom Field App으로 자동 확장도 가능합니다.
  • Q3. WBS 계층(부모-자식)을 재귀적으로 조회하려면?
    I_WBSElementParentObjectInternalID 필드와 PRHI 테이블을 활용하거나, CDS의 계층(hierarchy) 어노테이션 및 HIERARCHY() 함수를 사용합니다.

확장 학습 방향과 관련 주제

이 글에서 다룬 I_ProjectDefinition은 시작점입니다. 다음 주제로 확장하면 실무 역량이 크게 향상됩니다.

  • I_WBSElement, I_ProjectNetwork, I_ProjectNetworkActivity 뷰를 조합한 프로젝트 전체 뷰 구성
  • PS와 CO(Controlling)의 연결: I_ActualCostsByProject, 계획 대비 실적 리포팅
  • RAP(RESTful ABAP Programming Model)로 프로젝트 CRUD 서비스 구현 (Behavior Definition + Behavior Implementation)
  • SAP Analytics Cloud에서 @Analytics.dataCategory: #CUBE 어노테이션을 적용한 프로젝트 KPI 대시보드
  • Enterprise Project(EPM) 모델로의 마이그레이션 검토

댓글 0

아직 댓글이 없습니다.