ABAP

CDS Currency 변환 30초 만에 — 어노테이션으로 자동화 #shorts #SAP #ABAP

▶ YouTube에서 보기

개요 및 이 글에서 다루는 것

ABAP CDS(Core Data Services)에서 금액 필드를 다룰 때 가장 까다로운 부분은 통화(Currency) 처리입니다. 한 회사가 KRW, USD, EUR을 동시에 다룰 때 단순한 DECIMAL 필드만으로는 의미가 불완전합니다. 이 글에서는 어노테이션 기반 시맨틱 선언으로 금액-통화 필드를 연결하고, Fiori Elements에서 자동으로 통화 드롭다운을 받고, 최종적으로 CURRENCY_CONVERSION 함수를 직접 호출하지 않고도 환율 변환 결과를 뷰에서 노출하는 방법을 단계별로 다룹니다.

  • @Semantics.amount.currencyCode 어노테이션으로 금액-통화 컬럼 연결하기
  • @Consumption.valueHelpDefinition으로 Fiori 통화 F4 검색도움말 생성하기
  • CURRENCY_CONVERSION 함수의 동작 원리와 어노테이션 기반 자동 변환 비교
  • TCURR, TCURX 테이블이 변환에 미치는 영향과 예외 처리

읽기 전에 알고 있으면 좋은 것

이 글은 중급 ABAP 개발자를 대상으로 합니다. CDS View의 define view 문법, SE11에서 도메인/데이터 요소를 다뤄본 경험, ADT(ABAP Development Tools in Eclipse) 사용 경험이 있으면 좋습니다. 또한 TCURR(환율), TCURX(통화별 소수점 자리수), TCURC(통화 코드 마스터) 테이블의 존재만 알고 있어도 충분히 따라올 수 있습니다.

실습 환경 및 준비물

예제는 SAP S/4HANA 2022(또는 ABAP Platform 2022 on-premise) 기준으로 작성했으며, ABAP Cloud(Steampunk) 환경에서도 일부 어노테이션 차이를 제외하면 동일하게 동작하는 것으로 알려져 있습니다. 클라이언트는 ADT 3.36 이상을 권장합니다.

  • SAP NetWeaver 7.55+ 또는 S/4HANA 2020 이상 (CDS amount.currencyCode 어노테이션 지원)
  • ADT(Eclipse) + ABAP Development Tools 플러그인
  • 샘플 데이터: 자체 ZPO_HEADER 테이블 또는 EKKO/EKPO 같은 표준 구매 테이블
  • TCURR 테이블에 환율 데이터가 적재되어 있어야 변환 함수가 정상 동작합니다
  • 권한: S_DEVELOP(CDS DDL 생성), S_RFC(테스트 시)
참고: 일부 어노테이션(@Consumption.valueHelpDefinition)은 OData 노출 시점에 의미가 있으므로, RAP(BO 정의) 또는 클래식 게이트웨이 서비스로 익스포즈한 뒤 Fiori Elements 프리뷰로 검증하는 것이 일반적입니다.

핵심 개념 - 시맨틱 어노테이션이 왜 필요한가

관계형 데이터베이스 입장에서 NET_AMOUNT DECIMAL(15,2)CURRENCY_CODE CHAR(5)는 별개의 컬럼입니다. DB는 둘 사이의 관계를 모릅니다. 그러나 비즈니스 관점에서는 "1000"이라는 숫자가 KRW인지 USD인지에 따라 가치가 1300배 차이날 수 있습니다. ABAP 클래식 환경에서는 데이터 요소(Data Element)의 Reference 탭에서 Reference field를 지정해 이 관계를 표현했습니다. CDS는 이 사고방식을 어노테이션으로 끌어올렸습니다.

비유하자면 금액 필드는 "숫자"가 적힌 영수증이고, 통화 필드는 영수증 상단의 "₩/$/€" 기호입니다. 둘을 떼어 놓으면 영수증이 의미를 잃습니다. @Semantics.amount.currencyCode: 'CURRENCY_FIELD' 어노테이션은 "이 금액의 통화 기호는 저기에 있다"고 시스템에 알려주는 일종의 풀(string)입니다.

이 풀이 연결되면 세 가지 자동화가 따라옵니다. 첫째, Fiori UI에서 금액을 표시할 때 자동으로 통화에 맞는 소수점(JPY는 0자리, KWD는 3자리)이 적용됩니다. 둘째, OData $metadata에 sap:semantics="currency-code"가 노출되어 클라이언트가 별도 매핑 없이 인식합니다. 셋째, 가장 중요한 점인데, CDS 빌트인 함수 CURRENCY_CONVERSION이 의미적으로 정합성을 가집니다. TCURX의 소수점 보정이 자동으로 들어가기 때문에 ABAP 개발자가 직접 곱셈/나눗셈을 하지 않아도 됩니다.

도식으로 표현하면 다음과 같습니다:

[DB Layer]   NET_AMOUNT (1000.00)  +  CURRENCY (KRW)
                  |                          |
                  +---- @Semantics ----------+
                                |
[CDS Layer]   define view ...
                  |
                  +-- CURRENCY_CONVERSION( amount, source, target, rate_date )
                                |
[OData]       sap:semantics="currency-code" + value help
                                |
[Fiori UI]    1,000 KRW  ->  0.77 USD  (드롭다운 + 자동 포맷)

실전 코드 1단계 - 기본 금액-통화 연결 뷰

가장 단순한 형태입니다. 구매 발주 헤더(PurchaseOrder)에서 발주 총액과 통화를 노출하는 뷰를 만듭니다. 자체 테이블 zpo_header가 있다고 가정합니다(필드: po_id, vendor_id, total_amount, doc_currency, posting_date).

@AbapCatalog.sqlViewName: 'ZVPO_BASIC'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: '구매발주 기본 금액 뷰'
define view Z_I_PurchaseOrder_Basic
  as select from zpo_header as po
{
  key po.po_id              as PurchaseOrderId,
      po.vendor_id          as VendorId,
      po.posting_date       as PostingDate,

      @Semantics.amount.currencyCode: 'DocumentCurrency'
      po.total_amount       as TotalAmount,

      @Semantics.currencyCode: true
      po.doc_currency       as DocumentCurrency
}

중요한 두 어노테이션을 보세요. @Semantics.amount.currencyCode: 'DocumentCurrency'는 TotalAmount 위에 붙어 "내 통화 컬럼의 별칭은 DocumentCurrency다"라고 선언합니다. 그리고 통화 필드 위의 @Semantics.currencyCode: true는 "이게 통화 코드다"라고 표시합니다. 별칭(alias) 이름을 참조해야 하며 원본 컬럼명(doc_currency)이 아닙니다. 이 부분을 헷갈리는 개발자가 많습니다.

액티베이트 후 Data Preview로 확인해보면 금액이 통화별 소수점 규칙대로 보입니다. JPY 행은 정수, KRW 행은 소수점 없음, USD는 2자리로 정렬됩니다.

실전 코드 2단계 - 환율 변환과 F4 도움말, 에러 처리

실무에서는 단지 표시만 하는 게 아니라 "원화 환산 금액"을 함께 보여달라는 요구가 흔합니다. CDS 빌트인 CURRENCY_CONVERSION을 사용하되 어노테이션과 결합합니다. 동시에 통화 코드 입력 필드에 F4 검색도움말을 자동 생성합니다.

@AbapCatalog.sqlViewName: 'ZVPO_CONV'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: '구매발주 환율 변환 뷰'
define view Z_I_PurchaseOrder_Converted
  as select from zpo_header as po
{
  key po.po_id              as PurchaseOrderId,
      po.vendor_id          as VendorId,
      po.posting_date       as PostingDate,

      @Semantics.amount.currencyCode: 'DocumentCurrency'
      po.total_amount       as TotalAmount,

      @Consumption.valueHelpDefinition: [{ entity: {
          name: 'I_Currency', element: 'Currency' } }]
      @Semantics.currencyCode: true
      po.doc_currency       as DocumentCurrency,

      // KRW 환산 금액 - 빌트인 함수 사용
      @Semantics.amount.currencyCode: 'LocalCurrency'
      currency_conversion(
          amount             => po.total_amount,
          source_currency    => po.doc_currency,
          target_currency    => cast( 'KRW' as abap.cuky( 5 ) ),
          exchange_rate_date => po.posting_date,
          exchange_rate_type => cast( 'M' as abap.char( 4 ) ),
          error_handling     => 'SET_TO_NULL'
      )                      as AmountInKRW,

      @Semantics.currencyCode: true
      cast( 'KRW' as abap.cuky( 5 ) ) as LocalCurrency
}

핵심 포인트를 짚어봅니다. error_handling 파라미터를 'SET_TO_NULL'로 지정한 이유는 TCURR에 해당 일자/통화쌍의 환율이 없을 때 덤프(예외)가 발생하면 뷰 전체 조회가 실패하기 때문입니다. 대안은 'KEEP_AMOUNT'(원금 그대로 유지)와 'FAIL_ON_ERROR'가 있습니다. 운영 환경에서는 SET_TO_NULL을 쓰고 UI에서 별도 표시하는 패턴이 일반적입니다.

exchange_rate_type 'M'은 평균환율을 의미하며, B(매입), G(매도) 등이 있습니다. 회계 정책에 따라 결정합니다. @Consumption.valueHelpDefinition은 표준 뷰 I_Currency를 참조해 Fiori 필터바에 자동으로 드롭다운을 생성합니다. 이 어노테이션은 OData V2/V4 어느 쪽이든 게이트웨이 노출 시 인식됩니다.

실전 코드 3단계 - 프로덕션 패턴(파라미터화, 권한, 성능)

실제 운영에서는 환산 통화/환율 타입/기준일을 호출 측에서 지정할 수 있어야 재사용성이 올라갑니다. 파라미터화된 뷰로 발전시키고 액세스 컨트롤(DCL)까지 결합합니다.

@AbapCatalog.sqlViewName: 'ZVPO_PROD'
@AbapCatalog.compiler.compareFilter: true
@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: '구매발주 환산 뷰 - 프로덕션'
@VDM.viewType: #COMPOSITE
@ClientHandling.algorithm: #SESSION_VARIABLE
define view Z_C_PurchaseOrder_FX
  with parameters
    p_target_currency : abap.cuky( 5 ),
    p_rate_type       : abap.char( 4 ),
    p_rate_date       : abap.dats
  as select from zpo_header as po
  association [0..1] to I_Currency as _Currency
        on $projection.DocumentCurrency = _Currency.Currency
{
  key po.po_id              as PurchaseOrderId,
      po.vendor_id          as VendorId,
      po.posting_date       as PostingDate,

      @Semantics.amount.currencyCode: 'DocumentCurrency'
      po.total_amount       as TotalAmount,

      @Consumption.valueHelpDefinition: [{ entity: {
          name: 'I_Currency', element: 'Currency' } }]
      @Semantics.currencyCode: true
      po.doc_currency       as DocumentCurrency,

      @Semantics.amount.currencyCode: 'TargetCurrency'
      currency_conversion(
          amount             => po.total_amount,
          source_currency    => po.doc_currency,
          target_currency    => :p_target_currency,
          exchange_rate_date => :p_rate_date,
          exchange_rate_type => :p_rate_type,
          error_handling     => 'SET_TO_NULL',
          round              => 'X',
          decimal_shift      => 'X',
          decimal_shift_back => 'X'
      )                      as AmountConverted,

      @Semantics.currencyCode: true
      :p_target_currency    as TargetCurrency,

      _Currency
}

성능과 정확성에 영향을 주는 세 파라미터를 추가했습니다. round = 'X'는 결과를 통화 소수점 자리수에 맞춰 반올림합니다. decimal_shiftdecimal_shift_back은 TCURX에 등록된 통화별 소수점 보정을 자동 적용합니다. 일본 엔화(JPY)는 내부적으로 100단위로 저장되는 등의 특수성이 있어 이 옵션을 켜지 않으면 100배 차이가 나는 사고가 발생합니다.

@AccessControl.authorizationCheck: #CHECK와 함께 별도 DCL 파일을 만들면 행 수준 보안이 적용됩니다:

@MappingRole: true
define role Z_C_PurchaseOrder_FX {
  grant select on Z_C_PurchaseOrder_FX
    where (VendorId) = aspect pfcg_auth( Z_PO_AUTH, VENDOR, ACTVT = '03' );
}

호출 측 ABAP 코드는 다음과 같이 파라미터를 전달합니다:

SELECT po_id, total_amount, document_currency,
       amount_converted, target_currency
  FROM z_c_purchaseorder_fx(
         p_target_currency = 'KRW',
         p_rate_type       = 'M',
         p_rate_date       = @sy-datum )
  INTO TABLE @DATA(lt_po)
  UP TO 100 ROWS.

테스트 시에는 ADT의 Data Preview에서 파라미터 입력창이 자동 생성되므로 수작업으로 다양한 통화 조합을 검증할 수 있습니다.

자주 발생하는 실수와 트러블슈팅

Q1. "Currency reference is missing" 같은 메시지로 액티베이션이 실패합니다.
A. @Semantics.amount.currencyCode가 가리키는 별칭이 실제 SELECT 리스트의 별칭과 정확히 일치하는지 확인하세요. 원본 컬럼명(doc_currency)이 아니라 별칭(DocumentCurrency)이어야 합니다. 또한 통화 필드 자체에 @Semantics.currencyCode: true가 빠지면 일부 도구가 인식하지 못합니다.

Q2. 환율 변환 결과가 모두 NULL로 나옵니다.
A. 가장 흔한 원인은 TCURR에 해당 날짜/통화쌍의 레코드가 없는 경우입니다. SE16N으로 TCURR을 열어 KURST(환율 타입), FCURR(원천 통화), TCURR(대상 통화), GDATU(역순 날짜)를 확인하세요. 또한 error_handling = 'SET_TO_NULL'로 설정되어 있으면 환율이 없을 때 NULL이 반환되므로, 원인 추적 시 일시적으로 'FAIL_ON_ERROR'로 바꿔 짧은 메시지를 받아보는 것이 디버깅에 유리합니다.

Q3. JPY 환산 결과가 예상보다 100배 큽니다(혹은 작습니다).
A. TCURX 테이블이 원인입니다. 일본 엔화처럼 일부 통화는 ABAP 내부에서 비정상 소수점으로 저장됩니다. currency_conversion 호출 시 decimal_shift = 'X', decimal_shift_back = 'X'를 명시하면 보정이 자동 적용됩니다. 표시 단계에서 보정하려 하면 다른 통화와 로직이 꼬이므로 변환 시점에 처리하는 것이 권장됩니다.

Q4. Fiori Elements에서 통화 드롭다운이 안 나타납니다.
A. @Consumption.valueHelpDefinition이 OData 메타데이터로 노출되는지 확인하세요. 일반적으로 RAP 서비스 정의에서 expose 또는 클래식 게이트웨이의 $metadata 응답에 ValueList 어노테이션이 포함되어야 합니다. 또한 참조하는 I_Currency 뷰가 해당 시스템에 존재하는지(S/4HANA 표준 뷰), 일부 구버전 NetWeaver에서는 별도 커스텀 뷰를 만들어 참조해야 합니다.

이어서 살펴볼 만한 주제

이 글에서 다룬 통화 변환은 시맨틱 어노테이션 패밀리의 한 축입니다. 같은 사고방식이 단위(Unit) 변환에도 동일하게 적용됩니다. @Semantics.quantity.unitOfMeasureUNIT_CONVERSION 빌트인 함수로 KG↔LB 변환이 가능합니다. 또한 RAP(ABAP RESTful Application Programming Model) BO에서 통화 필드를 다룰 때 Behavior Definition의 field ( readonly ) currency_code 같은 선언과 결합되며, Draft 처리 시 환산 값이 어떻게 갱신되는지도 학습 가치가 있습니다. ABAP Cloud(Steampunk)에서는 일부 클래식 어노테이션이 deprecated되고 RAP 중심으로 재편되었으므로 마이그레이션 가이드를 함께 살펴보면 좋습니다.

외부 자료 모음

댓글 0

아직 댓글이 없습니다.