개요 및 핵심 포인트
구매·조달 모듈에서 공급사(Vendor/Supplier) 정보를 가져올 때 여전히 SELECT * FROM lfa1 코드를 그대로 유지하고 있다면, S/4HANA 전환 시점에 큰 리팩터링 부담을 떠안게 됩니다. SAP는 S/4HANA 이후 Virtual Data Model(VDM)을 표준으로 제시하고 있으며, 공급사 마스터의 대표 뷰가 바로 I_Supplier CDS View입니다. 이 글에서는 LFA1 직접 조회의 위험을 정리하고, I_Supplier로 안전하게 마이그레이션하는 실전 코드 3단계를 다룹니다.
- LFA1 직접 SELECT가 왜 위험한지 이해하기
- I_Supplier CDS View의 구조와 필드 매핑 파악하기
- ABAP 코드에서 I_Supplier를 활용해 공급사 정보 조회하기
- 구매오더(EKKO)와 JOIN하여 실무 시나리오 구현하기
- 성능·에러 처리·인증 정보를 반영한 프로덕션 코드 작성하기
사전 이해가 필요한 개념
이 글은 ABAP OpenSQL의 SELECT ... FROM ... INTO TABLE 문법과 CDS View의 개념(DEFINE VIEW ENTITY, Annotation, Association)을 최소한으로 알고 있다고 가정합니다. 또한 SAP ERP의 공급사 마스터 테이블인 LFA1, LFB1, LFM1의 존재를 알고 있으면 매핑 이해가 훨씬 쉽습니다. ADT(Eclipse ABAP Development Tools) 사용 경험이 있으면 CDS View 정의 확인이 편리합니다.
환경 및 준비물
본 예제는 다음 환경에서 검증되었습니다. 릴리스별로 뷰 필드가 조금씩 다를 수 있으므로, ADT의 Data Preview로 실제 필드를 확인해 주세요.
- SAP S/4HANA 2022 FPS02 이상 (On-Premise) 또는 SAP S/4HANA Cloud Public Edition 2402 이상
- ABAP Platform 7.58 이상 (Cloud Development 모델은
ABAP Cloud권장) - ADT(Eclipse ABAP Development Tools) 3.36 이상
- 권한 오브젝트
S_DEVELOP, 공급사 데이터 조회용M_LFM1_EKO등 - 참고 패키지:
APPL_MM_PUR_VDM_BE,APPL_MDG_BS_BP_VDM
ABAP Cloud(RAP, Steampunk) 환경에서는 LFA1 직접 접근이 아예 차단되어 있으므로, I_Supplier 등 릴리스된 CDS View만이 유일한 진입점입니다.
핵심 개념: LFA1 직접 조회의 문제와 VDM
고전적인 ABAP 코드는 공급사 이름·주소를 조회하기 위해 LFA1을 직접 SELECT합니다. 언뜻 간단해 보이지만, 다음과 같은 구조적 문제가 있습니다.
- 비즈니스 시맨틱 부재: LFA1은 물리 테이블일 뿐, 어떤 필드가 활성 공급사인지, Central Block 여부는 어디를 봐야 하는지 코드에 하드코딩됩니다.
- Business Partner(BP) 통합 이후 데이터 정합성 이슈: S/4HANA는 공급사를 Business Partner로 통합했습니다. LFA1은 여전히 존재하지만, 최신 필드(예: 국제 주소)는 BP 테이블에 있습니다.
- Deprecation 위험: SAP는 릴리스별 노트에서 "핵심 마스터는 VDM을 사용하라"고 권장합니다. LFA1 구조는 유지되지만, 신규 필드는 CDS View로만 제공되는 경우가 늘고 있습니다.
- 테스트·재사용성: CDS View는 SADL/OData/RAP과 자연스럽게 연동되지만, 원시 테이블은 그렇지 않습니다.
VDM은 3계층 구조로 구성됩니다. 도식으로 표현하면 다음과 같습니다.
[Interface Layer] I_Supplier <- 재사용 가능한 표준 뷰 (개발자가 SELECT)
|
[Basic Layer] I_SupplierBasic <- 순수 데이터 조합
|
[Physical Table] LFA1 / BUT000 ... <- 물리 테이블 (직접 접근 금지)
개발자는 최상위 Consumption 뷰이거나 Interface 뷰인 I_Supplier를 사용하면, 하부 물리 구조가 바뀌어도 코드가 깨지지 않습니다. 이는 마치 REST API와 DB 스키마의 관계와 유사합니다. DB 스키마를 직접 건드리지 말고, API 계층을 통해 접근하라는 원칙과 같습니다.
I_Supplier의 주요 필드는 Supplier(공급사 코드), SupplierName, SupplierAccountGroup, Country, PostalCode, CityName, TaxNumber1, IsBlockedForPosting, IsMarkedForDeletion 등이며, LFA1과의 매핑은 아래 표를 참고하세요.
| LFA1 컬럼 | I_Supplier 필드 | 비고 |
|---|---|---|
| LIFNR | Supplier | 공급사 코드 (Key) |
| NAME1 | SupplierName | Business Partner 이름과 연동 |
| LAND1 | Country | ISO 국가코드 |
| ORT01 | CityName | 주소 뷰 통합 반영 |
| PSTLZ | PostalCode | - |
| STCD1 | TaxNumber1 | - |
| SPERR | IsBlockedForPosting | Boolean 형태로 변환 |
| LOEVM | IsMarkedForDeletion | Boolean 형태로 변환 |
| KTOKK | SupplierAccountGroup | - |
실전 코드 1단계: 기본 조회
가장 단순한 시나리오부터 시작합니다. 특정 공급사 코드로 이름과 국가를 조회합니다. 아래는 리팩터링 이전 코드입니다.
" 안티 패턴: LFA1 직접 접근
DATA: lv_name TYPE lfa1-name1,
lv_land TYPE lfa1-land1.
SELECT SINGLE name1 land1
FROM lfa1
INTO ( lv_name, lv_land )
WHERE lifnr = '0000100234'.
이 코드를 I_Supplier 기반으로 바꾸면 다음과 같습니다.
" 권장 패턴: I_Supplier CDS View 사용
DATA ls_supplier TYPE i_supplier.
SELECT SINGLE
Supplier,
SupplierName,
Country,
CityName,
IsBlockedForPosting
FROM I_Supplier
WHERE Supplier = @( CONV bu_partner( '0000100234' ) )
INTO @ls_supplier.
IF sy-subrc = 0.
WRITE: / ls_supplier-SupplierName,
ls_supplier-Country,
ls_supplier-CityName.
ENDIF.
변경 포인트는 세 가지입니다. 첫째, FROM 절이 물리 테이블이 아닌 CDS 엔티티입니다. 둘째, 필드명이 비즈니스 시맨틱을 따릅니다. 셋째, 필터에 인라인 선언(@( ... ))과 이스케이프(@)를 사용해 최신 OpenSQL 스타일을 반영합니다.
실전 코드 2단계: 구매오더 JOIN 및 에러 로깅
실무에서는 구매오더 헤더(I_PurchaseOrder)와 공급사 정보를 함께 가져오는 경우가 많습니다. 예를 들어, 특정 회사코드의 지난 30일간 구매오더를 공급사 이름과 함께 리포트하는 시나리오를 가정합니다.
CLASS zcl_po_supplier_report DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
TYPES: BEGIN OF ty_po_line,
purchase_order TYPE ekko-ebeln,
supplier_id TYPE bu_partner,
supplier_name TYPE string,
supplier_city TYPE string,
order_date TYPE d,
END OF ty_po_line,
tt_po_lines TYPE STANDARD TABLE OF ty_po_line WITH EMPTY KEY.
METHODS get_recent_pos
IMPORTING iv_company_code TYPE bukrs
iv_from_date TYPE d
RETURNING VALUE(rt_lines) TYPE tt_po_lines
RAISING cx_sy_open_sql_db.
ENDCLASS.
CLASS zcl_po_supplier_report IMPLEMENTATION.
METHOD get_recent_pos.
TRY.
SELECT po~PurchaseOrder AS purchase_order,
po~Supplier AS supplier_id,
sup~SupplierName AS supplier_name,
sup~CityName AS supplier_city,
po~PurchaseOrderDate AS order_date
FROM I_PurchaseOrder AS po
INNER JOIN I_Supplier AS sup
ON sup~Supplier = po~Supplier
WHERE po~CompanyCode = @iv_company_code
AND po~PurchaseOrderDate >= @iv_from_date
AND sup~IsMarkedForDeletion = @abap_false
INTO TABLE @rt_lines.
CATCH cx_sy_open_sql_db INTO DATA(lx_sql).
" 로깅 - 실제 프로젝트는 Application Log(BAL)로 연동
MESSAGE lx_sql->get_text( ) TYPE 'I'.
RAISE EXCEPTION lx_sql.
ENDTRY.
ENDMETHOD.
ENDCLASS.
여기서 눈여겨볼 점은 IsMarkedForDeletion으로 삭제 표시된 공급사를 자연스럽게 걸러낼 수 있다는 점입니다. LFA1로 하려면 LOEVM <> 'X'처럼 도메인 값을 직접 알아야 하지만, CDS는 Boolean 시맨틱으로 노출합니다.
실전 코드 3단계: 프로덕션 품질(성능, 페이징, 권한, 테스트)
대량 데이터·권한·유닛테스트를 고려한 프로덕션 형태입니다. 페이징에 UP TO N ROWS + OFFSET을, 권한에는 WITH PRIVILEGED ACCESS가 아닌 명시적 체크를 사용합니다.
CLASS zcl_supplier_finder DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
TYPES: BEGIN OF ty_result,
supplier TYPE bu_partner,
supplier_name TYPE string,
country TYPE land1,
city TYPE string,
tax_number TYPE stcd1,
END OF ty_result,
tt_result TYPE STANDARD TABLE OF ty_result WITH EMPTY KEY.
METHODS search
IMPORTING iv_country TYPE land1
iv_name_pattern TYPE string DEFAULT '%'
iv_page_size TYPE i DEFAULT 50
iv_page_index TYPE i DEFAULT 0
RETURNING VALUE(rt_data) TYPE tt_result
RAISING cx_abap_invalid_value.
ENDCLASS.
CLASS zcl_supplier_finder IMPLEMENTATION.
METHOD search.
" 1) 입력 검증
IF iv_page_size <= 0 OR iv_page_size > 500.
RAISE EXCEPTION TYPE cx_abap_invalid_value.
ENDIF.
DATA(lv_offset) = iv_page_size * iv_page_index.
" 2) 권한 체크 - 국가별 공급사 조회 권한 예시
AUTHORITY-CHECK OBJECT 'F_LFA1_BUK'
ID 'BUKRS' DUMMY
ID 'ACTVT' FIELD '03'.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE cx_abap_invalid_value.
ENDIF.
" 3) I_Supplier + 페이징 + 정렬
SELECT Supplier,
SupplierName,
Country,
CityName,
TaxNumber1
FROM I_Supplier
WHERE Country = @iv_country
AND SupplierName LIKE @iv_name_pattern
AND IsBlockedForPosting = @abap_false
AND IsMarkedForDeletion = @abap_false
ORDER BY SupplierName
INTO CORRESPONDING FIELDS OF TABLE @rt_data
UP TO @iv_page_size ROWS
OFFSET @lv_offset.
ENDMETHOD.
ENDCLASS.
ABAP Unit 테스트도 함께 작성하면, 향후 CDS View 필드 변경이 있을 때 회귀 테스트로 안전망이 됩니다.
CLASS ltc_supplier_finder DEFINITION FINAL FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS test_pagination FOR TESTING.
ENDCLASS.
CLASS ltc_supplier_finder IMPLEMENTATION.
METHOD test_pagination.
DATA(lo_finder) = NEW zcl_supplier_finder( ).
DATA(lt_page1) = lo_finder->search(
iv_country = 'DE'
iv_page_size = 10
iv_page_index = 0 ).
cl_abap_unit_assert=>assert_true(
act = xsdbool( lines( lt_page1 ) <= 10 ) ).
ENDMETHOD.
ENDCLASS.
흔한 실수와 트러블슈팅
실제 프로젝트에서 반복적으로 마주치는 문제와 해결책을 정리합니다.
- Q1.
SELECT SINGLE ... FROM I_Supplier WHERE Supplier = '100234'가 값을 찾지 못합니다.
Supplier필드는BU_PARTNER타입으로 10자리 패딩된 문자열입니다. Literal을 그대로 넣으면 매칭이 실패할 수 있으니,CONV bu_partner( '100234' )로 변환하거나, ALPHA conversion routine을 명시적으로 태우세요. - Q2. ABAP Cloud에서
SELECT ... FROM lfa1이 컴파일 에러를 냅니다.
정상 동작입니다. ABAP Cloud는 릴리스된 CDS View만 허용합니다.I_Supplier,I_SupplierPurchasingOrg,I_SupplierCompany중 시나리오에 맞는 뷰로 대체하세요. - Q3. LFA1에 있던 사용자 정의 컬럼(CI include)이 I_Supplier에 안 보입니다.
표준 릴리스 뷰는 커스텀 필드를 자동으로 노출하지 않습니다. Key User Extensibility로 필드를 확장하거나,Z_Supplier_Ext형태의 커스텀 CDS를 만들어I_Supplier와 Association으로 연결하는 방식이 권장됩니다. - Q4. JOIN 성능이 예상보다 느립니다.
CDS View는 내부적으로 여러 물리 테이블을 조인하므로, WHERE 절에 Key 필드(Supplier, CompanyCode 등)를 반드시 포함시켜 조기 필터링을 유도하세요. ST05 SQL Trace로 실행 계획을 확인하는 습관이 중요합니다. - Q5.
SupplierName이 예상과 다른 값을 반환합니다.
BP 통합 이후 공급사 이름의 실제 저장 위치는BUT000입니다. LFA1-NAME1을 갱신해도 BP 이름과 다를 수 있으니, 항상I_Supplier가 반환하는 값을 신뢰하는 것이 원칙입니다.
이어서 살펴볼 만한 주제
I_Supplier에 익숙해졌다면 다음 주제로 확장해 보세요. 공급사와 조직 데이터를 결합한 I_SupplierPurchasingOrg, 회사코드 단위 정보는 I_SupplierCompany, 은행 정보는 I_SupplierBankDetails로 세분화되어 있습니다. RAP(RESTful ABAP Programming Model)에서 이 뷰들을 Projection View로 감싸 Fiori 앱을 만드는 실습도 좋은 다음 걸음입니다. 또한 커스텀 필드가 필요한 경우 ABAP Cloud Extensibility와 Custom CDS View 개발 흐름을 학습하면 실무 활용 폭이 크게 넓어집니다.
더 읽어볼 자료
댓글 0
아직 댓글이 없습니다.