본문 바로가기
디버깅 & 트러블슈팅

[SAP ABAP] 레거시 RFC 호출 자동 로깅 — INCLUDE 2개 + 동적 ASSIGN 으로 IMPORT/EXPORT 캡처 (SYSTEM_CALLSTACK)

by Song.sh 2026. 5. 19.

레거시 시스템(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.

CBO Table의 Data를 가지고 ALV는 따로 생성 해야합니다.

 

실행 시간 측정: 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 객체명은 회사 식별 가능 정보를 일반화한 형태이며, 실제 구현 시 사내 명명 규칙에 맞춰 조정해 사용하시기 바랍니다. 로그 테이블은 데이터가 빠르게 누적되므로 운영 환경 적용 시 인덱스 설계와 정기 아카이빙 정책을 함께 수립하시기 바랍니다.