레거시 시스템(MES·WMS·Java 백오피스 등) 이 SAP 의 RFC 펑션을 호출해 PR·PO 를 생성하거나 마스터를 업데이트하는 인터페이스가 늘어나면, 어느 순간 "어제 14시쯤 들어온 호출에서 어떤 값이 넘어왔길래 결과가 이상한가요" 같은 문의가 일상이 됩니다. SAP 표준 트랜잭션 SMQ1·SM58·ST22 만으로는 IMPORTING / EXPORTING 값을 완전히 복원할 수 없는 경우가 많아 자체 로그 테이블이 필요해집니다.
해결책은 단순한 패턴입니다. RFC 펑션의 첫 줄에 "호출자 정보 + IMPORTING 값" 을 저장하는 INCLUDE, 마지막 줄에 "EXPORTING 값 + 실행 시간 + 결과 메시지" 를 저장하는 INCLUDE 두 개만 넣어두면, 그 펑션이 호출될 때마다 자동으로 로그 테이블에 흔적이 남습니다. 핵심은 SYSTEM_CALLSTACK 으로 호출자 식별 + ABAP 의 동적 ASSIGN (parameter_name) TO <fs> 로 펑션 안의 변수를 메타데이터 기반으로 모두 캡처하는 것입니다.
이 글은 3개 CBO 로그 테이블(헤더 / 파라미터 / 필드 디테일) + 2개 INCLUDE(START / END) + 동적 Field Symbol 패턴으로 만든 RFC 호출 추적 도구를 한 흐름으로 정리한 실무 메모입니다. FUPARAREF · DD03L 같은 ABAP 메타데이터 테이블을 활용해 펑션 시그니처를 런타임에 분석하는 부분이 핵심입니다.
핵심 — 전체 구조 한눈 비교
3개 테이블 + 2개 INCLUDE + 1개 Number Range 가 한 세트로 동작합니다.
| 구성 요소 | 이름 (마스킹 후) | 역할 |
|---|---|---|
| 로그 헤더 | ZTXX2300 |
로그번호·일시·사용자·호출타입(RFC/SAP)·펑션명·실행시간·결과 메시지 |
| 파라미터 | ZTXX2301 |
파라미터별 메타정보 — 이름·타입(I/E/C/T)·구조체·단일값 |
| 필드 디테일 | ZTXX2302 |
구조체/테이블 파라미터의 라인별 필드값 |
| Number Range | ZXX_RFC (Object) |
로그번호 채번 (날짜+시간+시퀀스 조합) |
| START INCLUDE | ZIXX_TRACE_RFC_START |
RFC 펑션 첫 줄 — 호출자 식별 + IMPORTING 캡처 |
| END INCLUDE | ZIXX_TRACE_RFC_END |
RFC 펑션 마지막 줄 — EXPORTING + 실행시간 + 결과 메시지 캡처 |
핵심 한 줄: 모니터링 대상 RFC 펑션 첫·마지막 줄에 INCLUDE 두 개만 추가. 그 외 기존 코드는 변경 불필요. INCLUDE 안의 로직이 ABAP Metadata(FUPARAREF·DD03L) 를 보고 파라미터를 자동으로 모두 캡처하기 때문에, 펑션 시그니처가 변경되어도 INCLUDE 는 그대로 동작합니다.
1단계 — 로그 테이블 3종 정의
CBO 테이블 3개를 만듭니다. 헤더-파라미터-필드 디테일의 1:N:N 관계입니다.
ZTXX2300 (로그 헤더)
LOGNO CHAR(20) KEY 로그번호 (날짜6 + 시간6 + 시퀀스6 + α)
ERDAT DATS 생성일
ERZET TIMS 생성시간
EUSER CHAR(12) 호출 사용자 (sy-uname)
CALLTYPE CHAR(3) 호출 타입 RFC / SAP
DEVCLASS CHAR(30) 개발 클래스
FUNCNAME CHAR(30) 펑션 모듈명
STEXT CHAR(80) 펑션 설명
F_RESULT CHAR(1) 결과 메시지 타입 (E/S/W)
F_MESSAGE CHAR(220) 결과 메시지 본문
F_RUNTIME INT4 실행 시간 (마이크로초)
ZTXX2301 (파라미터 메타정보)
LOGNO CHAR(20) KEY 로그번호
PARAMCALL CHAR(1) KEY 파라미터 호출 단계 I(시작) / E(종료)
PARAMNAME CHAR(30) KEY 파라미터 이름
PARAMTYPE CHAR(1) 타입 I / E / C / T
STRUCTURE CHAR(30) 참조 구조체 / 테이블 타입
REFERENCE CHAR(1) 구조체/테이블 여부 (X)
P_VALUE CHAR(255) 단순 파라미터일 때 값
ZTXX2302 (구조체/테이블 필드 디테일)
LOGNO CHAR(20) KEY 로그번호
PARAMCALL CHAR(1) KEY I / E
TABNAME CHAR(30) KEY 필드의 테이블/구조체 이름
FIELDNAME CHAR(30) KEY 필드 이름
TABLINE INT4 KEY 라인 번호 (테이블의 경우)
P_VALUE CHAR(255) 필드값



키 컬럼 길이는 환경에 맞춰 조정. P_VALUE 는 모든 ABAP 타입을 CHAR 로 변환해 담을 공간이므로 255 정도면 일반적인 코드/번호/금액 표현에 충분합니다.
2단계 — Number Range Object 생성
로그번호는 같은 시간대에 동시 호출이 들어오면 충돌하므로 NRO(Number Range Object) 로 채번합니다.
SNRO 트랜잭션 → Object: ZXX_RFC 생성
→ Number length: 6
→ Number ranges 정의:
Interval 01: 000001 ~ 899999 (운영 사용)
Initialization 시점: 900000 도달 시 자동 리셋
START INCLUDE 안에서 NUMBER_GET_NEXT FM 으로 채번하고, 900000 에 가까워지면 NUMBER_RANGE_INTERVAL_INIT 으로 초기화하는 패턴.
3단계 — START INCLUDE: 호출자 식별
INCLUDE 의 첫 단계는 "누가 이 펑션을 어떻게 호출했는지" 식별 입니다. SYSTEM_CALLSTACK 표준 FM 으로 호출 스택을 가져옵니다.
DATA: gt_callstack TYPE abap_callstack,
gt_cstack TYPE sys_callst WITH HEADER LINE,
gv_calltype TYPE ztxx2300-calltype,
gv_xprog TYPE syst_xprog,
gv_xform TYPE syst_xform.
CALL FUNCTION 'SYSTEM_CALLSTACK'
EXPORTING
max_level = 0
IMPORTING
callstack = gt_callstack
et_callstack = gt_cstack[].
READ TABLE gt_cstack INDEX 1.
IF sy-subrc = 0 AND gt_cstack-eventtype = 'FUNC'.
gv_xprog = gt_cstack-progname.
gv_xform = gt_cstack-eventname.
IF sy-tcode IS INITIAL.
" 외부 시스템(레거시) 에서 들어온 호출
gv_calltype = 'RFC'.
ELSEIF sy-tcode = 'ZXX_RFC'
OR sy-tcode = 'SE38'
OR sy-tcode = 'SE37'.
" 개발자가 테스트 실행한 경우 — 로그 남기지 않음
CLEAR: gv_xprog, gv_xform.
ELSE.
" 같은 SAP 내부 다른 트랜잭션에서 호출
gv_calltype = 'SAP'.
ENDIF.
ENDIF.
sy-tcode 분기의 의미: 외부 시스템에서 RFC 로 들어오면 sy-tcode 가 빈 값입니다. SE37 같은 개발자 테스트는 트랜잭션 코드가 있어서 분기로 구분합니다. 개발 테스트까지 로그에 남기면 운영 데이터가 오염되므로 SE37/SE38 호출은 의도적으로 스킵.
4단계 — 동적 ASSIGN: 파라미터 자동 캡처
핵심 트릭입니다. RFC 펑션의 IMPORTING 파라미터는 펑션마다 다르지만, FUPARAREF 메타테이블로 시그니처를 조회하고 ASSIGN (parameter_name) TO <fs> 동적 할당으로 런타임에 값을 끌어옵니다.
" 1) 펑션의 IMPORTING 파라미터 목록 조회 (메타테이블)
SELECT parameter, paramtype, structure
FROM fupararef
INTO TABLE @DATA(lt_fupararef)
WHERE funcname = @gv_xform
AND paramtype IN ('I', 'C', 'T') " I=Import, C=Changing, T=Tables
ORDER BY pposition ASCENDING.
" 2) 각 파라미터의 구조체 필드 조회
SELECT tabname, fieldname, position, inttype
FROM dd03l
INTO TABLE @DATA(lt_dd03l)
FOR ALL ENTRIES IN @lt_fupararef
WHERE tabname = @lt_fupararef-structure.
" 3) 동적 ASSIGN 으로 각 파라미터에 접근
LOOP AT lt_fupararef ASSIGNING FIELD-SYMBOL(<fs_param>).
IF <fs_param>-paramtype = 'T'.
" 테이블 파라미터 — 이름 뒤에 [] 붙여 ASSIGN
DATA(lv_tabnm) = |{ <fs_param>-parameter }[]|.
ASSIGN (lv_tabnm) TO <fs_tab>.
" <fs_tab> 가 펑션의 IT_DATA 같은 테이블 파라미터를 가리킴
ELSE.
" 구조체 / 단일 변수 파라미터
ASSIGN (<fs_param>-parameter) TO <fs_wa>.
" <fs_wa> 가 펑션의 IS_HEAD / IV_MATNR 같은 변수를 가리킴
ENDIF.
" <fs_tab> / <fs_wa> 에서 각 필드 추출해 ZTXX2302 에 저장
" ...
ENDLOOP.
ASSIGN ('변수명') TO <fs> 는 문자열로 표현된 변수명을 런타임에 해석해 그 변수에 Field Symbol 을 연결하는 ABAP 의 동적 기능입니다. RFC 펑션의 파라미터는 호출 시점에 그 펑션의 글로벌 데이터 영역에 존재하므로, 펑션 안에서 호출된 INCLUDE 코드에서도 같은 이름으로 접근 가능합니다.
각 필드는 ASSIGN COMPONENT fieldname OF STRUCTURE <fs_wa> TO <fs_fld> 로 다시 동적 접근해 ZTXX2302 에 INSERT 합니다.
5단계 — END INCLUDE: EXPORTING + 실행시간
END INCLUDE 는 START 의 미러 이미지에 가깝습니다. 차이는 PARAMTYPE IN ('E', 'C', 'T') 으로 EXPORTING 만 가져온다는 점과, 결과 메시지 + 실행시간 측정 부분이 추가됩니다.
DATA: gr_runtime TYPE REF TO if_abap_runtime,
gv_time_start TYPE i,
gv_time_end TYPE i.
" START INCLUDE 안에서 측정 시작
gr_runtime = cl_abap_runtime=>create_hr_timer( ).
gv_time_start = gr_runtime->get_runtime( ).
" END INCLUDE 안에서 측정 종료
gv_time_end = gr_runtime->get_runtime( ).
gs_t2300-f_runtime = gv_time_end - gv_time_start.
" 결과 메시지 동적 ASSIGN
FIELD-SYMBOLS: <fs_mtype> TYPE any, <fs_mtext> TYPE any.
CASE gt_fupararef-parameter.
WHEN 'E_MTYPE'. ASSIGN (gt_fupararef-parameter) TO <fs_mtype>.
WHEN 'E_MTEXT'. ASSIGN (gt_fupararef-parameter) TO <fs_mtext>.
ENDCASE.
IF <fs_mtype> IS ASSIGNED.
gs_t2300-f_result = <fs_mtype>. " E / S / W
ENDIF.
IF <fs_mtext> IS ASSIGNED.
gs_t2300-f_message = <fs_mtext>. " 사용자에게 표시될 메시지
ENDIF.
MODIFY ztxx2300 FROM gs_t2300.
COMMIT WORK.

실행 시간 측정: CL_ABAP_RUNTIME=>CREATE_HR_TIMER 는 마이크로초 단위 고정밀 타이머. 옛 코드의 GET RUN TIME FIELD 와 동일 결과지만 클래스 기반이라 더 안정적입니다.
결과 메시지 규약: 자사 RFC 펑션의 EXPORTING 에 E_MTYPE(메시지 타입) · E_MTEXT(메시지 본문) 을 표준으로 정의해 두면 END INCLUDE 가 그것만 따로 빼서 헤더에 저장합니다. 어느 펑션이든 같은 규약이라 모니터링 화면에서 한 컬럼으로 결과 확인 가능.
6단계 — RFC 펑션에 INCLUDE 부착
기존 RFC 펑션은 두 줄만 추가하면 됩니다.
FUNCTION z_xx_create_po.
*"----------------------------------------------------------------------
*"*"Local Interface:
*" IMPORTING
*" VALUE(IS_HEAD) TYPE ZSXX_PO_HEAD
*" VALUE(IT_ITEM) TYPE ZTTXX_PO_ITEM
*" EXPORTING
*" VALUE(E_MTYPE) TYPE CHAR1
*" VALUE(E_MTEXT) TYPE CHAR220
*" VALUE(E_PONO) TYPE EBELN
*"----------------------------------------------------------------------
INCLUDE zixx_trace_rfc_start. " ★ 첫 줄
" === 기존 RFC 비즈니스 로직 ===
" PO 생성 BAPI 호출 등 ...
" 결과를 E_MTYPE / E_MTEXT 에 채움
INCLUDE zixx_trace_rfc_end. " ★ 마지막 줄
ENDFUNCTION.
이 패턴이 깔끔한 이유는 INCLUDE 가 펑션의 글로벌 데이터 영역을 그대로 공유 한다는 점입니다. INCLUDE 안에서 ASSIGN ('IS_HEAD') TO <fs> 가 동작해 IMPORTING 파라미터를 그대로 캡처 가능. 별도 PERFORM 으로 분리하면 파라미터 전달 코드를 또 작성해야 하지만 INCLUDE 라서 불필요.
흔히 빠뜨리는 함정
TRY-CATCH 누락
" ❌ TRY 없이 작성
CALL FUNCTION 'NUMBER_GET_NEXT' ...
SELECT ... FROM fupararef ...
로그 코드 자체가 RFC 흐름을 깨면 비즈니스 영향이 큽니다. INCLUDE 전체를 TRY ... CATCH cx_root 로 감싸 어떤 에러가 나도 비즈니스 흐름은 통과되도록.
TRY.
" 로그 저장 로직 전부
CATCH cx_root INTO gc_exc.
" 의도적으로 무시 — 로그 실패가 RFC 실패가 되면 안 됨
ENDTRY.
sy-tcode 분기 빠뜨림
" ❌ 모든 경우에 로그 남김 — SE37 테스트 데이터까지 운영 테이블에 쌓임
gv_calltype = 'RFC'.
개발자 단위 테스트(SE37·SE38) 호출은 로그에서 제외해야 운영 데이터가 깨끗합니다. sy-tcode 분기로 명확히 구분.
COMMIT WORK 위치
START INCLUDE 가 COMMIT WORK 를 호출하면 그 시점에 비즈니스 로직 중간 상태도 같이 커밋될 위험이 있습니다. 가능하면 END INCLUDE 끝에서만 한 번 커밋. 또는 RFC 펑션 내부에 별도 LUW 가 있으면 운영 환경 영향 확인 필요.
동적 ASSIGN 실패 시 처리
ASSIGN ('IS_HEAD') TO <fs_wa>.
" <fs_wa> IS ASSIGNED 체크 없이 사용하면 ASSIGN 실패 시 STATEMENT_TYPE_CONFLICT 덤프
IF NOT <fs_wa> IS ASSIGNED.
EXIT.
ENDIF.
파라미터 이름이 잘못됐거나 펑션 시그니처가 변경된 경우 동적 ASSIGN 이 실패합니다. 항상 IS ASSIGNED 체크 후 사용.
로그 테이블 인덱스 누락
운영 환경에서 로그 데이터가 쌓이면 ZTXX2300 조회가 느려집니다. ERDAT + FUNCNAME·EUSER + ERDAT 같은 보조 인덱스를 SE11 에서 추가해 두는 것이 좋습니다.
Number Range 초기화 누락
900000 한도를 넘으면 NUMBER_RANGE_OVERFLOW 에러로 채번이 막힙니다. NUMBER_RANGE_INTERVAL_INIT 으로 한 바퀴 돌리는 로직 + 로그 KEY 에 날짜·시간이 포함되어 있어 시퀀스 중복이 즉시 문제가 되지 않는 점이 안전장치입니다.
로그 데이터 정리 미흡
로그 테이블이 무한정 쌓이면 DB 용량 압박. 주기적 아카이빙 잡(예: 90일 이상 데이터 삭제 또는 별도 아카이브 테이블 이동) 을 설계해 두는 것이 안전합니다.
전체 코드 — 복사용 통합본
두 INCLUDE 전체 코드를 그대로 옮겨 둡니다. 회사 식별 가능 prefix 는 일반화한 상태이며, 사내 환경에 맞춰 테이블명·Number Range 객체명만 바꿔 사용하면 됩니다.
ZIXX_TRACE_RFC_START — RFC 시작 시 호출자·IMPORTING 캡처
*&---------------------------------------------------------------------*
*& Include ZIXX_TRACE_RFC_START
*&---------------------------------------------------------------------*
*& RFC 시작 - RFC 기본 정보 및 Import 내역 저장
*&---------------------------------------------------------------------*
DATA: gc_exc TYPE REF TO cx_root,
gr_runtime TYPE REF TO if_abap_runtime.
DATA: BEGIN OF gt_fupararef OCCURS 0,
parameter LIKE fupararef-parameter,
paramtype LIKE fupararef-paramtype,
structure LIKE fupararef-structure,
END OF gt_fupararef.
DATA: BEGIN OF gt_dd03l OCCURS 0,
tabname LIKE dd03l-tabname,
fieldname LIKE dd03l-fieldname,
position LIKE dd03l-position,
inttype LIKE dd03l-inttype,
END OF gt_dd03l.
DATA: gt_t2301 TYPE TABLE OF ztxx2301 WITH HEADER LINE,
gt_t2302 TYPE TABLE OF ztxx2302 WITH HEADER LINE.
DATA: gt_callstack TYPE abap_callstack,
gt_cstack TYPE sys_callst WITH HEADER LINE.
DATA: gs_t2300 TYPE ztxx2300.
DATA: gv_logno TYPE ztxx2300-logno,
gv_rowtype TYPE dd40l-rowtype,
gv_seq(6) TYPE n,
gv_idx TYPE i,
gv_typ TYPE c,
gv_flag TYPE c,
gv_tabnm TYPE string,
gv_xprog TYPE syst_xprog,
gv_xform TYPE syst_xform,
gv_calltype TYPE ztxx2300-calltype,
gv_paramcall TYPE ztxx2301-paramcall,
gv_time_start TYPE i,
gv_time_end TYPE i.
FIELD-SYMBOLS: <fs_tab> TYPE STANDARD TABLE,
<fs_wa> TYPE any,
<fs_fld> TYPE any,
<fs_mtype> TYPE any,
<fs_mtext> TYPE any.
DEFINE clear_t.
REFRESH &1. CLEAR &1.
END-OF-DEFINITION.
TRY.
clear_t: gt_fupararef, gt_dd03l, gt_t2301, gt_t2302, gt_cstack.
CLEAR: gt_callstack[], gs_t2300, gv_flag, gv_tabnm, gv_logno,
gv_seq, gv_xprog, gv_xform, gv_time_start, gv_time_end.
" 1) 호출 스택 분석
CALL FUNCTION 'SYSTEM_CALLSTACK'
EXPORTING
max_level = 0
IMPORTING
callstack = gt_callstack
et_callstack = gt_cstack[].
READ TABLE gt_cstack INDEX 1.
IF sy-subrc EQ 0 AND gt_cstack-eventtype EQ 'FUNC'.
gv_xprog = gt_cstack-progname.
gv_xform = gt_cstack-eventname.
IF sy-tcode IS INITIAL. " 외부 호출(레거시 → RFC)
gv_calltype = 'RFC'.
ELSEIF sy-tcode EQ 'ZXX_RFC' " RFC 모니터 화면에서 실행
OR sy-tcode EQ 'SE38'
OR sy-tcode EQ 'SE37'.
CLEAR: gv_xprog, gv_xform. " 개발자 테스트는 로그 제외
ELSE.
gv_calltype = 'SAP'. " 같은 SAP 내부 호출
ENDIF.
ENDIF.
IF gv_xprog IS NOT INITIAL AND gv_xform IS NOT INITIAL.
gv_paramcall = 'I'.
" 2) 로그 KEY 채번
CALL FUNCTION 'NUMBER_GET_NEXT'
EXPORTING
nr_range_nr = '01'
object = 'ZXX_RFC'
IMPORTING
number = gv_seq
EXCEPTIONS
interval_not_found = 1
number_range_not_intern = 2
object_not_found = 3
quantity_is_0 = 4
quantity_is_not_1 = 5
interval_overflow = 6
buffer_overflow = 7
OTHERS = 8.
IF gv_seq EQ '900000'. " Number Range 한 바퀴 돌면 초기화
CALL FUNCTION 'NUMBER_RANGE_INTERVAL_INIT'
EXPORTING
object = 'ZXX_RFC'
EXCEPTIONS
object_not_found = 1
OTHERS = 2.
ENDIF.
IF sy-subrc EQ 0.
gv_logno = sy-datum+2(6) && sy-uzeit && gv_seq.
* ***************** HEADER 세팅 시작 *****************
gs_t2300-logno = gv_logno. " 로그번호 (KEY)
gs_t2300-erdat = sy-datum.
gs_t2300-erzet = sy-uzeit.
gs_t2300-euser = sy-uname.
gs_t2300-calltype = gv_calltype.
gr_runtime = cl_abap_runtime=>create_hr_timer( ).
gv_time_start = gr_runtime->get_runtime( ).
" 펑션 개발 클래스
SELECT SINGLE devclass
FROM tadir
INTO gs_t2300-devclass
WHERE object EQ 'FUGR'
AND obj_name EQ gv_xprog+4.
" 펑션 모듈명 + 설명
gs_t2300-funcname = gv_xform.
SELECT SINGLE stext
FROM tftit
INTO gs_t2300-stext
WHERE spras EQ sy-langu
AND funcname EQ gs_t2300-funcname.
* ***************** HEADER 세팅 종료 *****************
* ***************** ITEM(IMPORTING) 세팅 시작 *****************
SELECT parameter paramtype structure
FROM fupararef
INTO TABLE gt_fupararef
WHERE funcname = gv_xform
AND paramtype IN ('I', 'C', 'T')
ORDER BY pposition ASCENDING.
" 구조체/테이블 타입 풀어내기
LOOP AT gt_fupararef.
CLEAR: gt_dd03l, gv_rowtype.
IF gt_fupararef-paramtype EQ 'T'.
SELECT SINGLE rowtype
FROM dd40l
INTO gv_rowtype
WHERE typename EQ gt_fupararef-structure.
IF gv_rowtype IS NOT INITIAL.
gt_fupararef-structure = gv_rowtype.
MODIFY gt_fupararef.
ENDIF.
ENDIF.
gt_dd03l-tabname = gt_fupararef-structure.
APPEND gt_dd03l.
CLEAR gt_fupararef.
ENDLOOP.
SORT gt_dd03l BY tabname position.
DELETE ADJACENT DUPLICATES FROM gt_dd03l COMPARING tabname.
IF NOT gt_dd03l[] IS INITIAL.
SELECT tabname fieldname position inttype
FROM dd03l
INTO TABLE gt_dd03l
FOR ALL ENTRIES IN gt_dd03l
WHERE tabname EQ gt_dd03l-tabname.
LOOP AT gt_dd03l WHERE fieldname EQ '.INCLUDE'.
DELETE gt_dd03l INDEX sy-tabix.
ENDLOOP.
ENDIF.
CLEAR gt_fupararef.
" 각 IMPORTING 파라미터 캡처
LOOP AT gt_fupararef.
IF gv_flag EQ 'X'.
EXIT.
ENDIF.
CLEAR: gv_typ, gt_t2301.
gt_t2301-logno = gv_logno.
gt_t2301-paramcall = gv_paramcall.
gt_t2301-paramname = gt_fupararef-parameter.
gt_t2301-paramtype = gt_fupararef-paramtype.
gt_t2301-structure = gt_fupararef-structure.
CLEAR gt_dd03l.
READ TABLE gt_dd03l WITH KEY tabname = gt_fupararef-structure.
IF sy-subrc EQ 0.
gt_t2301-reference = 'X'.
IF gt_fupararef-paramtype EQ 'T'.
" 테이블 파라미터 — 동적 ASSIGN 으로 캡처
CLEAR gv_tabnm.
gv_tabnm = gt_fupararef-parameter && |[]|.
ASSIGN (gv_tabnm) TO <fs_tab>.
IF NOT <fs_tab> IS ASSIGNED.
gv_flag = 'X'.
EXIT.
ENDIF.
IF <fs_tab>[] IS INITIAL.
UNASSIGN <fs_tab>.
CONTINUE.
ENDIF.
CLEAR gv_idx.
LOOP AT <fs_tab> ASSIGNING <fs_wa>.
IF NOT <fs_wa> IS ASSIGNED.
gv_flag = 'X'.
EXIT.
ENDIF.
IF gv_flag EQ 'X'.
EXIT.
ENDIF.
gv_idx = gv_idx + 1.
LOOP AT gt_dd03l WHERE tabname EQ gt_fupararef-structure.
gt_t2302-logno = gv_logno.
gt_t2302-paramcall = gv_paramcall.
gt_t2302-tabname = gt_dd03l-tabname.
gt_t2302-fieldname = gt_dd03l-fieldname.
gt_t2302-tabline = gv_idx.
ASSIGN COMPONENT gt_dd03l-fieldname
OF STRUCTURE <fs_wa> TO <fs_fld>.
IF NOT <fs_fld> IS ASSIGNED.
gv_flag = 'X'.
EXIT.
ENDIF.
IF NOT <fs_fld> IS INITIAL.
gt_t2302-p_value = <fs_fld>.
APPEND gt_t2302.
ENDIF.
CLEAR gt_dd03l.
ENDLOOP.
ENDLOOP.
ELSE.
" 구조체 파라미터
ASSIGN (gt_fupararef-parameter) TO <fs_wa>.
IF NOT <fs_wa> IS ASSIGNED.
gv_flag = 'X'.
EXIT.
ENDIF.
IF <fs_wa> IS INITIAL.
UNASSIGN <fs_wa>.
CONTINUE.
ENDIF.
LOOP AT gt_dd03l.
CHECK gt_dd03l-tabname EQ gt_fupararef-structure.
CLEAR gt_t2302.
gt_t2302-logno = gv_logno.
gt_t2302-paramcall = gv_paramcall.
gt_t2302-tabname = gt_dd03l-tabname.
gt_t2302-fieldname = gt_dd03l-fieldname.
gt_t2302-tabline = 1.
ASSIGN COMPONENT gt_dd03l-fieldname
OF STRUCTURE <fs_wa> TO <fs_fld>.
IF NOT <fs_fld> IS ASSIGNED.
gv_flag = 'X'.
EXIT.
ENDIF.
IF NOT <fs_fld> IS INITIAL.
gt_t2302-p_value = <fs_fld>.
APPEND gt_t2302.
ENDIF.
CLEAR gt_dd03l.
ENDLOOP.
ENDIF.
APPEND gt_t2301.
ELSE.
" 단순(스칼라) 파라미터
ASSIGN (gt_fupararef-parameter) TO <fs_fld>.
IF NOT <fs_fld> IS ASSIGNED.
gv_flag = 'X'.
EXIT.
ENDIF.
IF NOT <fs_fld> IS INITIAL.
gt_t2301-p_value = <fs_fld>.
DESCRIBE FIELD <fs_fld> TYPE gv_typ.
APPEND gt_t2301.
ENDIF.
ENDIF.
CLEAR gt_t2301.
ENDLOOP.
" 헤더 저장
IF gs_t2300-logno IS NOT INITIAL.
MODIFY ztxx2300 FROM gs_t2300.
IF sy-subrc EQ 0.
COMMIT WORK.
ENDIF.
ENDIF.
" 아이템 + 필드 디테일 저장
IF gv_flag IS INITIAL.
IF NOT gt_t2301[] IS INITIAL.
INSERT ztxx2301 FROM TABLE gt_t2301.
IF sy-subrc EQ 0.
COMMIT WORK.
ENDIF.
ENDIF.
IF NOT gt_t2302[] IS INITIAL.
INSERT ztxx2302 FROM TABLE gt_t2302.
IF sy-subrc EQ 0.
COMMIT WORK.
ENDIF.
ENDIF.
ENDIF.
ENDIF.
ENDIF.
CATCH cx_root INTO gc_exc.
" 로그 실패가 비즈니스 실패가 되면 안 됨 — 의도적으로 무시
ENDTRY.
ZIXX_TRACE_RFC_END — RFC 종료 시 EXPORTING·실행시간 캡처
*&---------------------------------------------------------------------*
*& Include ZIXX_TRACE_RFC_END
*&---------------------------------------------------------------------*
*& RFC 종료 - RFC 최종 결과 정보 및 Export 내역 저장
*&---------------------------------------------------------------------*
TRY.
clear_t: gt_fupararef, gt_dd03l, gt_t2301, gt_t2302.
CLEAR: gs_t2300, gv_flag.
IF gv_logno IS NOT INITIAL.
gv_paramcall = 'E'.
SELECT SINGLE * FROM ztxx2300
INTO CORRESPONDING FIELDS OF gs_t2300
WHERE logno EQ gv_logno.
IF sy-subrc EQ 0.
* ***************** ITEM(EXPORTING) 세팅 시작 *****************
SELECT parameter paramtype structure
FROM fupararef
INTO TABLE gt_fupararef
WHERE funcname = gs_t2300-funcname
AND paramtype IN ('E', 'C', 'T')
ORDER BY pposition ASCENDING.
LOOP AT gt_fupararef.
CLEAR: gt_dd03l, gv_rowtype.
IF gt_fupararef-paramtype EQ 'T'.
SELECT SINGLE rowtype
FROM dd40l
INTO gv_rowtype
WHERE typename EQ gt_fupararef-structure.
IF gv_rowtype IS NOT INITIAL.
gt_fupararef-structure = gv_rowtype.
MODIFY gt_fupararef.
ENDIF.
ENDIF.
gt_dd03l-tabname = gt_fupararef-structure.
APPEND gt_dd03l.
CLEAR gt_fupararef.
ENDLOOP.
SORT gt_dd03l BY tabname position.
DELETE ADJACENT DUPLICATES FROM gt_dd03l COMPARING tabname.
IF NOT gt_dd03l[] IS INITIAL.
SELECT tabname fieldname position inttype
FROM dd03l
INTO TABLE gt_dd03l
FOR ALL ENTRIES IN gt_dd03l
WHERE tabname EQ gt_dd03l-tabname.
LOOP AT gt_dd03l WHERE fieldname EQ '.INCLUDE'.
DELETE gt_dd03l INDEX sy-tabix.
ENDLOOP.
ENDIF.
CLEAR gt_fupararef.
LOOP AT gt_fupararef.
IF gv_flag EQ 'X'.
EXIT.
ENDIF.
CLEAR: gv_typ, gt_t2301.
" 결과 메시지 동적 ASSIGN
IF gt_fupararef-paramtype = 'E'.
IF <fs_mtype> IS ASSIGNED.
UNASSIGN <fs_mtype>.
ENDIF.
IF <fs_mtext> IS ASSIGNED.
UNASSIGN <fs_mtext>.
ENDIF.
CASE gt_fupararef-parameter.
WHEN 'E_MTYPE'.
ASSIGN (gt_fupararef-parameter) TO <fs_mtype>.
WHEN 'E_MTEXT'.
ASSIGN (gt_fupararef-parameter) TO <fs_mtext>.
WHEN OTHERS.
ENDCASE.
ENDIF.
gt_t2301-logno = gv_logno.
gt_t2301-paramcall = gv_paramcall.
gt_t2301-paramname = gt_fupararef-parameter.
gt_t2301-paramtype = gt_fupararef-paramtype.
gt_t2301-structure = gt_fupararef-structure.
CLEAR gt_dd03l.
READ TABLE gt_dd03l WITH KEY tabname = gt_fupararef-structure.
IF sy-subrc EQ 0.
gt_t2301-reference = 'X'.
IF gt_fupararef-paramtype EQ 'T'.
CLEAR gv_tabnm.
gv_tabnm = gt_fupararef-parameter && |[]|.
ASSIGN (gv_tabnm) TO <fs_tab>.
IF NOT <fs_tab> IS ASSIGNED.
gv_flag = 'X'.
EXIT.
ENDIF.
IF <fs_tab>[] IS INITIAL.
UNASSIGN <fs_tab>.
CONTINUE.
ENDIF.
CLEAR gv_idx.
LOOP AT <fs_tab> ASSIGNING <fs_wa>.
IF NOT <fs_wa> IS ASSIGNED.
gv_flag = 'X'.
EXIT.
ENDIF.
IF gv_flag EQ 'X'.
EXIT.
ENDIF.
gv_idx = gv_idx + 1.
LOOP AT gt_dd03l WHERE tabname = gt_fupararef-structure.
gt_t2302-logno = gv_logno.
gt_t2302-paramcall = gv_paramcall.
gt_t2302-tabname = gt_dd03l-tabname.
gt_t2302-fieldname = gt_dd03l-fieldname.
gt_t2302-tabline = gv_idx.
ASSIGN COMPONENT gt_dd03l-fieldname
OF STRUCTURE <fs_wa> TO <fs_fld>.
IF NOT <fs_fld> IS ASSIGNED.
gv_flag = 'X'.
EXIT.
ENDIF.
IF NOT <fs_fld> IS INITIAL.
gt_t2302-p_value = <fs_fld>.
APPEND gt_t2302.
ENDIF.
CLEAR gt_dd03l.
ENDLOOP.
ENDLOOP.
ELSE.
ASSIGN (gt_fupararef-parameter) TO <fs_wa>.
IF NOT <fs_wa> IS ASSIGNED.
gv_flag = 'X'.
EXIT.
ENDIF.
IF <fs_wa> IS INITIAL.
UNASSIGN <fs_wa>.
CONTINUE.
ENDIF.
LOOP AT gt_dd03l.
CHECK gt_dd03l-tabname EQ gt_fupararef-structure.
CLEAR gt_t2302.
gt_t2302-logno = gv_logno.
gt_t2302-paramcall = gv_paramcall.
gt_t2302-tabname = gt_dd03l-tabname.
gt_t2302-fieldname = gt_dd03l-fieldname.
gt_t2302-tabline = 1.
ASSIGN COMPONENT gt_dd03l-fieldname
OF STRUCTURE <fs_wa> TO <fs_fld>.
IF NOT <fs_fld> IS ASSIGNED.
gv_flag = 'X'.
EXIT.
ENDIF.
IF NOT <fs_fld> IS INITIAL.
gt_t2302-p_value = <fs_fld>.
APPEND gt_t2302.
ENDIF.
CLEAR gt_dd03l.
ENDLOOP.
ENDIF.
APPEND gt_t2301.
ELSE.
ASSIGN (gt_fupararef-parameter) TO <fs_fld>.
IF NOT <fs_fld> IS ASSIGNED.
gv_flag = 'X'.
EXIT.
ENDIF.
IF NOT <fs_fld> IS INITIAL.
gt_t2301-p_value = <fs_fld>.
DESCRIBE FIELD <fs_fld> TYPE gv_typ.
APPEND gt_t2301.
ENDIF.
ENDIF.
CLEAR gt_t2301.
ENDLOOP.
" 실행시간 + 결과 메시지 헤더 업데이트
IF gs_t2300-logno IS NOT INITIAL.
gv_time_end = gr_runtime->get_runtime( ).
IF <fs_mtype> IS ASSIGNED.
gs_t2300-f_result = <fs_mtype>.
ENDIF.
IF <fs_mtext> IS ASSIGNED.
gs_t2300-f_message = <fs_mtext>.
ENDIF.
gs_t2300-f_runtime = gv_time_end - gv_time_start.
MODIFY ztxx2300 FROM gs_t2300.
ENDIF.
IF gv_flag IS INITIAL.
IF NOT gt_t2301[] IS INITIAL.
INSERT ztxx2301 FROM TABLE gt_t2301.
ENDIF.
IF NOT gt_t2302[] IS INITIAL.
INSERT ztxx2302 FROM TABLE gt_t2302.
ENDIF.
ENDIF.
ENDIF.
ENDIF.
CATCH cx_root INTO gc_exc.
ENDTRY.
요약
| 단계 | 작업 | 핵심 |
|---|---|---|
| 1 | 로그 테이블 3종 | 헤더 / 파라미터 / 필드 디테일 (1 : N : N) |
| 2 | Number Range | SNRO 로 ZXX_RFC NRO 생성 — 동시 호출 충돌 방지 |
| 3 | 호출자 식별 | SYSTEM_CALLSTACK + sy-tcode 분기 (RFC / SAP / 테스트) |
| 4 | 동적 ASSIGN | FUPARAREF+DD03L 메타데이터 + ASSIGN (...) TO <fs> |
| 5 | END INCLUDE | EXPORTING + 실행시간 + 결과 메시지(E_MTYPE/E_MTEXT) |
| 6 | RFC 부착 | 대상 펑션 첫 줄 + 마지막 줄에 INCLUDE 두 개 |
RFC 호출 로그 자동 추적의 핵심은 "코드 침입을 최소화하면서 모든 펑션 호출을 같은 패턴으로 캡처" 하는 것입니다. ABAP 메타테이블(FUPARAREF·DD03L) + 동적 ASSIGN 의 조합이 그 마법을 만듭니다. 한 번 구축해 두면 새 RFC 펑션이 추가될 때마다 INCLUDE 두 줄만 넣으면 자동으로 모니터링 대상에 포함되고, 운영 중 "어제 14시 호출 값이 뭐였는지" 같은 문의에 5초 안에 답할 수 있게 됩니다.
Disclaimer — 이 포스트는 실무 정리 노트를 바탕으로 AI 보조로 정리되었습니다.
SYSTEM_CALLSTACK FM · FUPARAREF(펑션 파라미터 메타) · DD03L(테이블 필드 메타) · TADIR(객체 디렉토리) · TFTIT(펑션 설명) 은 ABAP 표준 메타데이터로 ECC 6.0 / S/4HANA 환경에서 그대로 사용 가능합니다. 동적 ASSIGN (parameter_name) TO <fs> 와 ASSIGN COMPONENT ... OF STRUCTURE ... 은 ABAP 의 표준 동적 데이터 접근 메커니즘이며, INCLUDE 가 호출된 펑션의 글로벌 데이터 영역을 공유한다는 ABAP 의 스코프 규칙에 의존합니다. CL_ABAP_RUNTIME=>CREATE_HR_TIMER 는 마이크로초 단위 실행시간 측정용으로 SAP NetWeaver 7.40 이상에서 제공됩니다. 본문의 테이블명 · INCLUDE 명 · Number Range 객체명은 회사 식별 가능 정보를 일반화한 형태이며, 실제 구현 시 사내 명명 규칙에 맞춰 조정해 사용하시기 바랍니다. 로그 테이블은 데이터가 빠르게 누적되므로 운영 환경 적용 시 인덱스 설계와 정기 아카이빙 정책을 함께 수립하시기 바랍니다.
'디버깅 & 트러블슈팅' 카테고리의 다른 글
| [SAP ABAP] BAPI · BDC 에러 메시지 확인 방법 — MESSAGE INTO · FORMAT_MESSAGE 빌드 (BAPIRET2 · BDCMSGCOLL) (0) | 2026.05.21 |
|---|---|
| [SAP] CTS 이관 후 PERFORM 오브젝트 없음 에러 — SE80 Rebuild Object List로 해결 (0) | 2026.05.13 |
| [SAP ABAP] 디버깅 중 원하는 라인으로 점프 — Goto Statement 사용법 (0) | 2026.05.12 |
| [SAP ABAP] Watchpoint 디버깅 — 변수 값 변화 추적으로 대량 데이터 한 건만 잡기 (0) | 2026.05.12 |
| [SAP ABAP] Message 디버깅 — 에러 메시지가 어디서 발생하는지 추적하기 (0) | 2026.05.12 |