개요 및 이 글에서 얻어갈 것
ABAP 7.40 이후 도입된 CONV와 EXACT 연산자는 인라인 타입 변환을 가능하게 만들었지만, 두 연산자의 차이를 정확히 이해하지 못하고 사용하면 프로덕션에서 소수점 절삭, 문자열 잘림, 통화 금액 손실 같은 치명적 버그로 이어집니다. 이 글은 재무 정산 시스템이나 판매 오더 처리 같은 실무 상황을 기준으로 두 연산자의 차이를 코드 레벨에서 완전히 파헤칩니다.
- CONV와 EXACT의 내부 동작 원리와 차이점 이해
- 어떤 상황에서 EXACT가 필수이고, 언제 CONV로 충분한지 판단 기준 습득
- CX_SY_CONVERSION_DATA_LOSS 예외 처리 패턴 익히기
- 레거시 MOVE 문 대비 신규 연산자의 장단점 파악
- 단위 테스트 가능한 안전 변환 로직 설계
알아두면 좋은 배경 지식
ABAP 7.40 SP08 이상 문법(인라인 선언 DATA(...), 생성자 표현식)에 대한 기본 이해가 필요합니다. 또한 ABAP 기본 데이터 타입(P, I, C, N, D, T, STRING)의 내부 표현 방식과 소수점 자리수(DECIMALS) 개념을 알고 있어야 예제를 완전히 소화할 수 있습니다.
환경 및 버전 준비
이 글의 코드는 다음 환경에서 검증하는 것을 권장합니다.
- ABAP Platform 2022 이상 (또는 NetWeaver 7.52 이상)
- ABAP Language Version: Standard ABAP 또는 ABAP for Cloud Development
- ADT (ABAP Development Tools) in Eclipse 3.34 이상
- 테스트를 위한 ABAP Unit 활성화 상태
S/4HANA Cloud Public Edition에서는 두 연산자 모두 릴리즈된 상태로 사용할 수 있으며, 온프레미스 ECC 6.0 EHP7 이하 버전에서는 CONV/EXACT를 지원하지 않으므로 MOVE와 명시적 오프셋 처리가 필요합니다. 예외 클래스 CX_SY_CONVERSION_DATA_LOSS는 7.40 SP08부터 사용 가능합니다.
핵심 개념 — 왜 두 개의 연산자가 필요한가
ABAP에서 값을 다른 타입으로 옮길 때 발생하는 상황을 세관 통과에 비유해봅시다. CONV는 "그냥 통과시켜, 짐이 좀 잘려나가도 상관없어"라고 말하는 관대한 세관입니다. 반면 EXACT는 "짐이 하나라도 손상되면 즉시 반송한다"고 선언하는 엄격한 세관입니다.
기술적으로 CONV #( ... )는 대상 타입으로의 암묵적 변환을 수행합니다. 소스 값이 대상 타입의 표현 범위를 초과하면 잘라내거나 반올림하며, 예외를 던지지 않습니다. 이는 기존 MOVE 문의 동작과 완전히 동일합니다.
반면 EXACT #( ... )는 손실 없는 변환(lossless assignment)을 요구합니다. 다음 세 가지 상황에서 CX_SY_CONVERSION_DATA_LOSS 계열 예외를 발생시킵니다.
- 소수부 절삭:
DECIMALS 2인 필드에123.4567을 대입하면0.0067이 사라짐 → 예외 - 문자열 잘림:
C LENGTH 5에'HELLO WORLD'를 대입 → 예외 - 정수 오버플로:
I(32비트)에INT8범위 값 대입 → 예외
일반적으로
CONV는 변환 결과를 신뢰할 수 있는 문맥에서 사용하고,EXACT는 금융/회계/외부 인터페이스처럼 데이터 무결성이 계약된 문맥에서 사용하는 것이 권장됩니다.
또한 두 연산자 모두 #는 "대상 타입에서 추론"을 의미합니다. 대입되는 변수의 타입이 컴파일 타임에 결정되면 #을 쓰고, 그렇지 않으면 명시적으로 CONV string( ... )처럼 타입을 지정합니다.
실전 예제 1단계 — 기본 동작 비교
가장 먼저 판매 오더의 금액을 문자열에서 패킹 수치(P)로 변환하는 상황을 봅시다. 외부 시스템(예: 크레딧 카드 게이트웨이 응답)에서 문자열로 들어온 금액을 내부 DB 컬럼에 저장하는 시나리오입니다.
TYPES: p_decimals_2 TYPE p LENGTH 8 DECIMALS 2.
DATA lv_raw_amount TYPE string VALUE '1580.457'.
" CONV: 소수 3자리가 잘려도 조용히 통과
DATA(lv_amount_conv) = CONV p_decimals_2( lv_raw_amount ).
" lv_amount_conv = 1580.46 (반올림, 예외 없음)
" EXACT: 소수 3자리 손실을 감지하고 예외 발생
TRY.
DATA(lv_amount_exact) = EXACT p_decimals_2( lv_raw_amount ).
CATCH cx_sy_conversion_data_loss INTO DATA(lo_ex).
WRITE: / '변환 실패:', lo_ex->get_text( ).
ENDTRY.
" 출력: 변환 실패: 데이터 손실이 발생했습니다
핵심은 CONV가 1580.457 → 1580.46으로 말없이 반올림한다는 것입니다. 정산 시스템이라면 이 0.003의 차이가 수백만 건 누적 시 큰 금액이 됩니다. 반대로 문자열 길이 검증을 볼까요.
DATA lv_product_code TYPE string VALUE 'MAT-2026-EU-0001'.
TYPES tc_short TYPE c LENGTH 8.
DATA(lv_short_conv) = CONV tc_short( lv_product_code ).
" lv_short_conv = 'MAT-2026' (뒤 잘림, 예외 없음)
TRY.
DATA(lv_short_exact) = EXACT tc_short( lv_product_code ).
CATCH cx_sy_conversion_data_loss.
" 여기로 진입 — 문자열이 잘렸음을 즉시 인지
ENDTRY.
실전 예제 2단계 — 실무 시나리오와 예외 처리
실무에서는 단순한 변환이 아니라, 여러 소스 시스템에서 온 데이터를 표준 인보이스 구조체로 매핑하는 상황이 많습니다. B2B EDI로 들어온 세금계산서 데이터를 내부 ZINVOICE_LINE 구조로 변환한다고 가정합시다.
TYPES: BEGIN OF ty_invoice_line,
line_id TYPE n LENGTH 6,
material_id TYPE c LENGTH 18,
quantity TYPE p LENGTH 7 DECIMALS 3,
unit_price TYPE p LENGTH 11 DECIMALS 2,
net_amount TYPE p LENGTH 13 DECIMALS 2,
END OF ty_invoice_line.
TYPES: BEGIN OF ty_edi_source,
line TYPE string,
mat TYPE string,
qty TYPE string,
price TYPE string,
END OF ty_edi_source.
CLASS lcl_invoice_mapper DEFINITION.
PUBLIC SECTION.
METHODS map_line
IMPORTING is_source TYPE ty_edi_source
RETURNING VALUE(rs_line) TYPE ty_invoice_line
RAISING cx_sy_conversion_data_loss.
ENDCLASS.
CLASS lcl_invoice_mapper IMPLEMENTATION.
METHOD map_line.
" 라인 번호와 자재 코드는 CONV 로 충분 (EDI 스펙상 자릿수 계약됨)
rs_line-line_id = CONV #( is_source-line ).
rs_line-material_id = CONV #( is_source-mat ).
" 수량과 금액은 EXACT 필수 — 세금계산서 금액 손실은 법적 문제
rs_line-quantity = EXACT #( is_source-qty ).
rs_line-unit_price = EXACT #( is_source-price ).
rs_line-net_amount = EXACT #( rs_line-quantity * rs_line-unit_price ).
ENDMETHOD.
ENDCLASS.
여기서 중요한 설계 결정은 "어떤 필드에 EXACT를 쓸지 정책적으로 결정"한다는 점입니다. 라인 번호는 EDI 스펙에서 이미 6자리로 강제되므로 손실이 나면 상위 시스템 문제이고, 굳이 EXACT로 검사할 필요가 없습니다. 반면 금액과 수량은 세금 신고에 직접 영향을 주므로 반드시 EXACT입니다.
DATA(lo_mapper) = NEW lcl_invoice_mapper( ).
LOOP AT lt_edi_lines INTO DATA(ls_edi).
TRY.
APPEND lo_mapper->map_line( ls_edi ) TO lt_invoice_lines.
CATCH cx_sy_conversion_data_loss INTO DATA(lo_conv_ex).
MESSAGE ID 'ZINV' TYPE 'E' NUMBER '001'
WITH ls_edi-line lo_conv_ex->get_text( )
INTO DATA(lv_msg).
APPEND VALUE #( line = ls_edi-line
msg = lv_msg ) TO lt_errors.
ENDTRY.
ENDLOOP.
실전 예제 3단계 — 프로덕션 품질 (테스트 · 성능 · 보안)
프로덕션 코드에서는 세 가지 요소가 추가됩니다. 첫째, ABAP Unit 테스트로 손실 감지 로직을 검증합니다. 둘째, 대량 배치에서 예외 처리 오버헤드를 최소화합니다. 셋째, 사용자 입력을 신뢰하지 않습니다.
CLASS ltc_invoice_mapper DEFINITION
FOR TESTING RISK LEVEL HARMLESS DURATION SHORT.
PRIVATE SECTION.
DATA mo_cut TYPE REF TO lcl_invoice_mapper.
METHODS setup.
METHODS map_valid_line FOR TESTING.
METHODS map_qty_rounding FOR TESTING RAISING cx_static_check.
ENDCLASS.
CLASS ltc_invoice_mapper IMPLEMENTATION.
METHOD setup.
mo_cut = NEW #( ).
ENDMETHOD.
METHOD map_valid_line.
DATA(ls_result) = mo_cut->map_line(
VALUE #( line = '000001' mat = 'MAT-A'
qty = '10.500' price = '250.00' ) ).
cl_abap_unit_assert=>assert_equals(
exp = '2625.00' act = ls_result-net_amount ).
ENDMETHOD.
METHOD map_qty_rounding.
TRY.
mo_cut->map_line(
VALUE #( line = '000002' mat = 'MAT-B'
qty = '10.5001' price = '100.00' ) ).
cl_abap_unit_assert=>fail( '예외가 발생해야 함' ).
CATCH cx_sy_conversion_data_loss.
" 기대 동작
ENDTRY.
ENDMETHOD.
ENDCLASS.
배치 성능 관점에서, TRY-CATCH는 예외가 실제로 던져질 때만 비용이 큽니다. 정상 경로에서는 거의 무료이므로 안심하고 사용해도 됩니다. 다만 사전 검증 후 CONV 사용 패턴이 유용한 경우가 있습니다.
METHOD safe_convert_amount.
DATA(lv_decimals) = find_decimal_places( iv_input ).
IF lv_decimals > 2.
IF mv_allow_rounding = abap_true.
rv_amount = CONV #( iv_input ).
log_rounding( iv_input ).
ELSE.
RAISE EXCEPTION TYPE zcx_precision_violation
EXPORTING iv_value = iv_input.
ENDIF.
ELSE.
rv_amount = EXACT #( iv_input ).
ENDIF.
ENDMETHOD.
보안 측면에서 사용자 UI에서 들어온 문자열을 CONV로 무조건 숫자 필드에 넣으면, 악의적 입력(예: 매우 긴 문자열)이 절삭되어 우회 공격에 활용될 수 있습니다. 외부 경계에서는 EXACT로 시작해서 예외를 400 Bad Request로 매핑하는 방식을 권장합니다.
흔한 실수와 트러블슈팅
Q1. CONV #( 100 )에서 컴파일 오류가 납니다.
#은 대상 타입 추론입니다. 좌변이 없는 위치(예: WRITE CONV #( 100 ))에서는 추론할 문맥이 없어 실패합니다. CONV string( 100 )처럼 명시하거나 좌변에 타입이 있는 변수를 사용하세요.
Q2. EXACT를 썼는데 왜 예외가 안 뜨죠?
가장 흔한 원인은 두 필드가 구조적으로 동일한 타입인 경우입니다. 예를 들어 P LENGTH 8 DECIMALS 2에서 P LENGTH 10 DECIMALS 2로의 변환은 손실이 없으므로 EXACT도 조용히 통과합니다. 반드시 소스 값의 실제 자릿수를 확인하세요. 또한 STRING에서 숫자 타입으로 변환할 때 앞뒤 공백만 잘리는 것은 손실로 간주되지 않습니다.
Q3. CX_SY_CONVERSION_DATA_LOSS가 CATCH되지 않고 덤프가 납니다.
이 예외는 CX_DYNAMIC_CHECK 계열입니다. 시그니처에 RAISING cx_sy_conversion_data_loss를 명시하지 않고 메서드 밖으로 던지면 런타임 오류가 됩니다. 반드시 발생 지점 근처에서 CATCH하거나 RAISING 절에 선언하세요.
Q4. 날짜 변환에서도 EXACT가 필요한가요?
D 타입은 8자 고정이므로 손실 개념이 적용되지 않습니다. 잘못된 형식('2026-13-45')은 CONV/EXACT 모두 무효 날짜로 저장되거나 예외를 던집니다. 날짜는 별도로 CL_ABAP_DATFM 검증기를 쓰는 것이 일반적으로 안전합니다.
EXACT/CONV 이후에 살펴볼 심화 주제
두 연산자를 완전히 이해했다면 다음 주제로 확장하는 것을 권장합니다. 첫째, REDUCE와 FOR를 결합한 함수형 변환 파이프라인 — 대량 데이터에서 EXACT를 안전하게 적용하는 패턴입니다. 둘째, CAST 연산자와의 조합 — 클래스 계층에서 손실 없는 다운캐스트를 표현할 때 유용합니다. 셋째, RAP(Restful ABAP Programming) 모델의 검증 단계에서 EXACT 활용 — Behavior Definition의 determine on save 로직에서 데이터 무결성 계약을 표현하는 방법입니다. 마지막으로 CDS Cast Expression과 ABAP EXACT의 차이를 비교해두면 데이터 계층부터 애플리케이션 계층까지 일관된 정밀도 정책을 설계할 수 있습니다.
댓글 0
아직 댓글이 없습니다.