왜 같은 이름의 필드인데도 값이 비어 있을까
ABAP 개발자라면 한 번쯤은 겪었을 상황이다. CORRESPONDING 한 줄로 깔끔하게 구조체를 복사했는데, 디버거를 열어보니 분명히 양쪽에 있는 필드 하나가 비어 있다. 컴파일 에러도 없고, 런타임 덤프도 없다. 그저 조용히 잘못된 값이 흘러간다. SalesOrder 헤더를 OutputDTO에 복사하는 순간에도, DeliveryItem을 BAPI 입력 파라미터로 매핑하는 순간에도 같은 일이 벌어진다. 이 글은 그 침묵의 원인을 추적하고, 실무에서 바로 쓸 수 있는 방어 패턴까지 정리한다.
이 글을 끝까지 따라오면 다음을 분명히 할 수 있다.
CORRESPONDING연산자와MOVE-CORRESPONDING문장의 동작 차이- 필드 이름이 같은데도 값이 전달되지 않는 6가지 대표 함정
- INCLUDE 구조체와 중첩 구조체에서의 매핑 규칙
- MAPPING / EXCEPT / DEEP / BASE 옵션의 실전 사용 시점
- 코드 리뷰 단계에서 잡아낼 수 있는 체크리스트와 단위 테스트 패턴
이 글을 편하게 읽기 위한 배경
구조체와 내부 테이블의 기본 선언, DATA · TYPES 구문에 익숙하다는 전제로 진행한다. BAPI_SALESORDER_CREATEFROMDAT2 같은 표준 BAPI를 한 번이라도 호출해 본 경험이 있다면 함정의 체감도가 훨씬 높다. ABAP Doc 주석, ADT(ABAP Development Tools) 디버거의 구조체 비교 뷰를 다룰 줄 알면 디버깅 단계의 설명이 더 잘 와닿는다.
준비 환경과 버전 차이
CORRESPONDING 연산자는 ABAP 7.40 SP05에서 도입되었다. SAP S/4HANA 1909 이상, ABAP Platform 2022/2023, ABAP Cloud 환경에서 모두 동작하지만 옵션의 세부 동작은 릴리스별로 조금씩 확장되어 왔다. 예를 들어 DEEP, DEEP APPENDING 옵션과 같은 키 기반 깊은 매핑은 7.50 이후에 더 안정화되었고, ABAP Cloud(Steampunk, RAP 환경)에서는 일부 dynamic 옵션이 제한된다.
실습 환경은 일반적으로 다음을 권장한다.
- SAP S/4HANA 2022 온프레미스 또는 ABAP Platform Trial 2022
- ADT(Eclipse 기반) 최신 빌드
- 테스트 프레임워크: ABAP Unit (SE80 또는 ADT 내장)
- 선택: abapGit으로 예제 패키지 관리
SE38 보다는 ADT에서 구조체 정의와 디버깅을 병행하는 편이 함정을 찾기 훨씬 수월하다. 디버거의 "Compare" 기능으로 두 구조체를 좌우로 펼쳐 놓고 비교하면 타입 불일치를 시각적으로 잡을 수 있기 때문이다.
핵심 개념: 같은 이름이라는 착각
CORRESPONDING의 매핑 규칙을 한 문장으로 요약하면 이렇다. "이름이 같고, 호환되는 타입이면 복사한다." 문제는 이 한 문장에 들어있는 두 조건 모두 개발자가 직관적으로 생각하는 것과 다르게 동작한다는 점이다.
먼저 "이름이 같다"는 조건은 컴포넌트 트리의 최상위에서만 평가된다. 중첩 구조체 안쪽의 필드 이름은 비교 대상이 아니다. SalesOrder 구조체의 HEADER-CUSTOMER_ID와 OutputDTO의 CUSTOMER_ID는 사람 눈에는 같아 보이지만, CORRESPONDING 입장에서는 한쪽은 "HEADER"라는 컴포넌트이고 다른 쪽은 "CUSTOMER_ID"이므로 결코 같지 않다.
두 번째로 "호환되는 타입"이라는 조건은 CL_ABAP_TYPEDESCR의 호환성 규칙을 따른다. CHAR(10)과 CHAR(20)은 변환 가능하지만, STRING과 CHAR은 다른 종류이며, P 타입의 소수점 자릿수가 다르면 잘림이 발생한다. 더 미묘한 것은 NUMC(10)과 CHAR(10)처럼 길이가 같아도 도메인 의미가 달라서 의도와 다른 변환이 일어날 수 있다.
비유하자면 CORRESPONDING은 항공사 카운터에서 이름표만 보고 짐을 옮겨 싣는 직원과 같다. 이름표가 같으면 옮기고, 비슷한 모양의 짐이라면 살짝 모양을 줄여서 넣어준다. 그런데 안쪽에 또 다른 가방이 들어 있는 캐리어(중첩 구조체)나, 같은 이름의 짐이 두 비행기에 동시에 실려 있는 경우(INCLUDE)에는 종종 엉뚱한 가방으로 바꿔 싣는다. 이 동작 자체는 "버그"가 아니라 "정의된 규칙"이지만, 그 규칙을 모르면 정확히 우리가 다루는 상황이 된다.
여기에 한 가지 더, MOVE-CORRESPONDING 문장과 CORRESPONDING #( ) 연산자는 모양은 비슷해도 기본 동작이 다르다. 문장 형태는 1990년대부터 존재해 왔고 기본적으로 타겟 필드의 기존 값을 유지하지 않고 덮어쓰지 않은 필드는 그대로 둔다. 반면 연산자 형태는 표현식의 결과로 새 구조체를 생성하므로 명시적으로 BASE 옵션을 주지 않는 한 매핑되지 않은 필드는 초기값으로 초기화된다. 이 차이 하나로 "어제까지 잘 되던 코드가 리팩터링 후 값이 사라지는" 사고가 빈번하게 발생한다.
1단계 예제: 가장 단순한 SalesOrder 매핑
먼저 가장 평범한 시나리오부터 시작한다. 영업 주문 헤더를 출력용 DTO로 옮기는 코드다.
TYPES: BEGIN OF ty_sales_order,
order_id TYPE c LENGTH 10,
customer_id TYPE c LENGTH 10,
order_date TYPE d,
net_amount TYPE p LENGTH 13 DECIMALS 2,
currency_code TYPE c LENGTH 5,
END OF ty_sales_order.
TYPES: BEGIN OF ty_order_dto,
order_id TYPE c LENGTH 10,
customer_id TYPE c LENGTH 10,
order_date TYPE d,
net_amount TYPE p LENGTH 13 DECIMALS 2,
END OF ty_order_dto.
DATA(ls_source) = VALUE ty_sales_order(
order_id = '4500001234'
customer_id = 'CUST00078'
order_date = '20260601'
net_amount = '1500.50'
currency_code = 'EUR' ).
DATA(ls_target) = CORRESPONDING ty_order_dto( ls_source ).
여기서 currency_code는 타겟에 없으므로 자연스럽게 버려지고, 나머지 네 개의 필드는 그대로 옮겨진다. 일반적으로 기대하는 동작과 일치한다. 문제는 다음 단계부터다.
2단계 예제: 실무에서 만나는 6가지 함정
이제 동일한 코드 베이스에 작은 변경 하나씩만 가한 예제를 보자. 각 변경이 매핑 결과를 어떻게 바꾸는지가 핵심이다.
첫 번째 함정은 INCLUDE 구조체다. 표준 BAPI에서 매우 흔한 패턴이다.
TYPES: BEGIN OF ty_audit_fields,
created_by TYPE c LENGTH 12,
created_at TYPE timestampl,
END OF ty_audit_fields.
TYPES: BEGIN OF ty_delivery_item.
INCLUDE TYPE ty_audit_fields.
TYPES: item_no TYPE n LENGTH 6,
material TYPE c LENGTH 40,
quantity TYPE p LENGTH 13 DECIMALS 3,
END OF ty_delivery_item.
INCLUDE된 필드들은 트리의 최상위로 평탄화되어 마치 직접 선언된 것처럼 동작한다. 따라서 CORRESPONDING이 정상적으로 인식한다. 반면 같은 의도로 작성한 다음 코드는 동작이 전혀 다르다.
TYPES: BEGIN OF ty_delivery_item_nested,
audit TYPE ty_audit_fields,
item_no TYPE n LENGTH 6,
material TYPE c LENGTH 40,
quantity TYPE p LENGTH 13 DECIMALS 3,
END OF ty_delivery_item_nested.
DATA(ls_target_nested) = CORRESPONDING ty_delivery_item_nested( ls_source_flat ).
여기서 created_by가 소스에 평탄하게 있고 타겟이 중첩 구조체라면, 기본 매핑은 두 필드를 같다고 보지 않는다. audit이라는 컴포넌트가 통째로 비어 나온다. 해결책은 DEEP 옵션을 명시하거나, 매핑을 분리하는 것이다.
두 번째 함정은 길이 차이다. 회사 코드(BUKRS, 4자리)와 사내 통합 코드(8자리)가 같은 이름으로 정의되어 있는 경우, 잘림이 조용히 발생한다. ABAP은 이 경우 경고를 띄우지 않는다.
세 번째 함정은 도메인 타입 차이다. NUMC(10) 필드를 CHAR(10) 필드로 옮기면 앞자리 0이 그대로 살아남지만, 반대로 옮기면 변환 과정에서 좌측 0으로 패딩되거나 잘리는 경우가 있다. SAP 표준 자재번호 MATNR(CHAR 40, 이전 18) 변경의 영향도 비슷한 맥락이다.
네 번째 함정은 참조 변수다. REF TO 타입을 가진 컴포넌트는 호환성 검사가 더 엄격해서, 같은 이름이라도 참조 클래스가 다르면 런타임 예외 CX_SY_MOVE_CAST_ERROR가 발생한다.
다섯 번째는 내부 테이블 컴포넌트다. 헤더에 아이템 테이블이 포함된 구조체를 매핑할 때, 행 타입이 다르면 매핑이 빈 테이블로 끝날 수 있다. 이때 필요한 것이 DEEP 옵션과 함께 사용하는 매핑 규칙이다.
여섯 번째는 가장 빈도가 높은 사고다. 바로 MOVE-CORRESPONDING과 연산자의 초기화 동작 차이다.
DATA: ls_target TYPE ty_order_dto.
ls_target-customer_id = 'CUST00099'.
MOVE-CORRESPONDING ls_source TO ls_target.
" customer_id는 'CUST00078'로 덮어쓰기 됨, 나머지는 유지
ls_target = CORRESPONDING #( ls_source ).
" 매핑되지 않은 모든 필드가 초기값으로 리셋됨
ls_target = CORRESPONDING #( BASE ( ls_target ) ls_source ).
" BASE 옵션: 기존 값 유지하면서 덮어쓰기
로깅을 추가해 어떤 필드가 어떻게 옮겨졌는지 명시적으로 확인하는 패턴을 함께 두면 운영 중 문제 해결이 훨씬 빨라진다.
TRY.
DATA(ls_target) = CORRESPONDING ty_order_dto(
ls_source MAPPING net_amount = total_amount ).
CATCH cx_sy_conversion_error INTO DATA(lx_conv).
" 변환 실패 시 어떤 필드인지 식별해 로그 기록
MESSAGE |Mapping failed: { lx_conv->get_text( ) }| TYPE 'E'.
ENDTRY.
3단계 예제: 프로덕션 수준의 방어적 패턴
실무에서는 매핑 로직 자체를 별도 클래스로 분리하고, ABAP Unit으로 회귀 테스트를 걸어두는 것이 안전하다. 다음은 SalesOrder 헤더를 RAP 비즈니스 객체의 입력 구조로 매핑하는 클래스 예시다.
CLASS zcl_sales_order_mapper DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
TYPES: BEGIN OF ty_input,
vbeln TYPE c LENGTH 10,
kunnr TYPE c LENGTH 10,
erdat TYPE d,
netwr TYPE p LENGTH 13 DECIMALS 2,
waerk TYPE c LENGTH 5,
items TYPE STANDARD TABLE OF ty_delivery_item
WITH EMPTY KEY,
END OF ty_input.
TYPES: BEGIN OF ty_rap_create,
order_id TYPE c LENGTH 10,
customer_id TYPE c LENGTH 10,
order_date TYPE d,
net_amount TYPE p LENGTH 13 DECIMALS 2,
currency TYPE c LENGTH 5,
item_list TYPE STANDARD TABLE OF ty_delivery_item
WITH EMPTY KEY,
END OF ty_rap_create.
METHODS to_rap_create
IMPORTING is_input TYPE ty_input
RETURNING VALUE(rs_result) TYPE ty_rap_create.
ENDCLASS.
CLASS zcl_sales_order_mapper IMPLEMENTATION.
METHOD to_rap_create.
rs_result = CORRESPONDING #(
is_input MAPPING order_id = vbeln
customer_id = kunnr
order_date = erdat
net_amount = netwr
currency = waerk
item_list = items ).
ENDMETHOD.
ENDCLASS.
이 패턴의 장점은 두 가지다. 첫째, 도메인 이름과 기술 필드명(vbeln, kunnr 같은 SAP 표준 약어)을 한곳에서 변환하므로 호출 측 코드가 깔끔해진다. 둘째, 매핑 규칙이 메서드 시그니처로 캡슐화되어 있어서 단위 테스트를 붙이기 좋다.
CLASS ltc_mapper DEFINITION FOR TESTING
DURATION SHORT RISK LEVEL HARMLESS.
PRIVATE SECTION.
METHODS map_header_fields FOR TESTING.
METHODS map_keeps_currency FOR TESTING.
ENDCLASS.
CLASS ltc_mapper IMPLEMENTATION.
METHOD map_header_fields.
DATA(lo_cut) = NEW zcl_sales_order_mapper( ).
DATA(ls_in) = VALUE zcl_sales_order_mapper=>ty_input(
vbeln = '4500009999' kunnr = 'CUST00001'
erdat = '20260101' netwr = '999.99' waerk = 'USD' ).
DATA(ls_out) = lo_cut->to_rap_create( ls_in ).
cl_abap_unit_assert=>assert_equals(
act = ls_out-order_id exp = '4500009999' ).
cl_abap_unit_assert=>assert_equals(
act = ls_out-customer_id exp = 'CUST00001' ).
ENDMETHOD.
METHOD map_keeps_currency.
" currency가 비지 않는지 회귀 테스트
ENDMETHOD.
ENDCLASS.
성능 측면에서는 CORRESPONDING이 핸드 코딩한 필드 단위 할당보다 약간 느릴 수 있다. 다만 대부분의 OLTP 시나리오에서는 차이가 측정 불가 수준이며, 가독성과 유지보수성의 이득이 훨씬 크다. 다만 수십만 건의 행을 처리하는 일괄 변환에서는 매핑이 빈번한 핵심 루프 안쪽이라면 사전 컴파일된 명시 할당을 고려할 수 있다.
보안 관점에서는, 매핑 대상에 비밀번호 해시나 토큰 같은 민감 필드가 같은 이름으로 존재한다면 의도치 않게 외부 응답 DTO로 흘러갈 수 있다. EXCEPT 옵션을 통해 화이트리스트가 아닌 블랙리스트 방식으로 차단해 두는 것이 안전하다.
DATA(ls_safe) = CORRESPONDING ty_public_dto(
ls_internal EXCEPT password_hash auth_token ).
현장에서 자주 묻는 질문
Q1. "필드 이름이 분명히 같은데 디버거에서 빈 값으로 보입니다."
가장 흔한 원인은 한쪽이 중첩 구조체 내부 필드라는 점이다. ADT 디버거에서 두 구조체의 컴포넌트 트리를 펼쳐서 진짜 최상위 이름을 비교해 보자. 필요하면 DEEP 옵션을 추가하거나 명시 매핑으로 풀어준다.
Q2. "이전 값이 사라집니다. MOVE-CORRESPONDING에서는 안 그랬는데요."
CORRESPONDING #( ) 연산자는 새 구조체를 만들어 할당하므로 매핑되지 않은 필드는 초기화된다. 기존 값을 보존하려면 CORRESPONDING #( BASE ( ls_target ) ls_source ) 형태로 BASE 옵션을 명시해야 한다.
Q3. "내부 테이블 컴포넌트가 빈 채로 나옵니다."
행 타입이 다르면 자동으로 채워지지 않는다. DEEP 또는 DEEP APPENDING 옵션을 통해 각 행에도 재귀적으로 CORRESPONDING이 적용되도록 지정하고, 필요하면 행 단위 매핑 규칙을 함께 작성한다.
Q4. "CX_SY_CONVERSION_ERROR가 갑자기 떨어집니다."
도메인 검사가 활성화된 필드(예: 화폐 코드, 단위)에 부적절한 값이 들어오면 변환 단계에서 예외가 발생한다. 매핑 호출을 TRY ... CATCH로 감싸고, 검증 로직을 매핑 이전에 두는 것이 권장된다.
Q5. "동적 구조체끼리 매핑하고 싶습니다."
RTTS(Run-Time Type Services)와 결합한 CORRESPONDING TYPE 동적 폼이 존재하지만, ABAP Cloud에서는 제한이 있다. 일반적으로는 명시적 구조체 타입을 정의하고, 빌더 패턴으로 매핑 단계를 분리하는 편이 안전하다.
여기서 한 걸음 더 나아가려면
이 글의 내용을 확실히 자기 것으로 만들고 싶다면, 우선 자신이 자주 다루는 BAPI나 RAP 비즈니스 객체의 입력 구조체를 하나 골라서 직접 매핑 클래스를 작성해 보길 권한다. 그다음 VALUE, REDUCE, FILTER 같은 다른 표현식 기반 연산자와 결합해 보면 ABAP의 표현식 스타일이 한층 자연스러워진다. RAP 환경에서는 BO 동작 구현부에서 MAPPING 옵션 사용 빈도가 매우 높으니, 표준 RAP 데모(RAP100, RAP110) 패키지의 매핑 코드를 읽어 보는 것도 좋다.
장기적으로는 ABAP Cleaner와 ATC(ABAP Test Cockpit) 규칙으로 매핑 패턴을 팀 표준으로 굳혀 두면, 신규 입사자가 같은 실수를 반복하는 일을 크게 줄일 수 있다.
더 깊이 파고들 때 보면 좋은 자료
댓글 0
아직 댓글이 없습니다.