Files
ABAP-Indexing/docs/definition-block-design.md
T

7.2 KiB

정의부(declaration) 수집 설계 — 2026-09-21

왜 필요한가

이 인덱스의 최종 용도는 비슷한 로직을 새로 짤 때 기존 코드를 찾아 복사해 오는 것이다. 그런데 인덱스에 담긴 조각(logic_chunk)의 코드 원문은 로직만이다. ABAP 은 내부테이블·스트럭처· 상수·필드심볼을 TOP 인클루드나 FORM 머리에 따로 선언하므로, 로직만 복사해 붙여넣으면 GT_LOAD 는 선언되지 않았습니다 류의 문법 오류가 난다.

그래서 정의부를 인덱싱 시점에 따로 수집한다. 조각 코드에는 계속 로직만 둔다(검색 품질을 지키기 위해서다 — 선언 수백 줄이 조각에 섞이면 조각의 뜻이 흐려진다).

수집한 정의부를 쓸지 말지는 판단하지 않는다. 붙여넣는 쪽의 LLM 이 정한다: 이미 같은 선언이 있으면 버리고, 이름이 겹치면 바꾸고, 타입만 빌려 쓰기도 한다. 인덱스는 "이 조각을 가져가려면 이것들이 함께 필요하다"는 재료와 분류만 정확히 넘긴다.

무엇을 수집하는가

parser/declarations.py 가 선언 문장을 붙여넣을 수 있는 한 문장 단위로 잘라 declaration 테이블에 넣는다 (index/db.py).

항목 내용
kind data / types / constants / tables / field-symbol / parameter / select-option / ranges / type-pool / controls / class / macro
scope global(TOP·공용 인클루드) / unit(FORM·METHOD 머리의 로컬 선언)
code 선언 원문 — 체인 항목이면 헤드를 다시 붙이고 ,. 로 닫아 유효한 문장으로 만든다
depends 이 선언이 참조하는 다른 이름 (LIKE GT_DATA → GT_DATA, TYPE TABLE OF TY_X → TY_X)

세 가지가 핵심이다.

  1. BEGIN OF … END OF 는 한 덩어리다. 업무 프로그램의 내부테이블은 거의 전부 이 형태다 (DATA : BEGIN OF GT_UPLOAD OCCURS 0, … END OF GT_UPLOAD.). parser/dataflow.py 의 심볼 추출은 이걸 건너뛰지만(추적에는 이름만 필요하다), 정의부에는 반드시 통째로 있어야 한다.
  2. 체인 항목도 혼자 설 수 있어야 한다. DATA: a TYPE i,\n b TYPE i.b 만 잘라내면 b TYPE i. 라 붙여넣을 수 없다 → DATA:\n b TYPE i. 로 헤드를 복원한다. 이를 위해 parser/statements.py 가 체인 전개 시 항목별 줄 범위를 기록한다.
  3. 로컬 클래스·매크로도 정의부다. CLASS lcl_x DEFINITION … ENDCLASS, DEFINE … END-OF-DEFINITION 은 unit 통째를 한 건으로 담는다.

조각 → 정의부 조립 (index/decls.py)

  1. 조각 코드의 식별자를 모은다 (파서 토크나이저 · 문장 단위)
  2. declaration 에서 찾는다 — unit 로컬 먼저, 없으면 전역
  3. 찾은 선언의 depends재귀로 끌어온다 (LT_DATA LIKE GT_DATA → GT_DATA)
  4. 의존이 먼저 오도록(post-order) 정렬해 declaration_code 한 덩어리로 만든다

결과는 네 갈래로 분류해 넘긴다.

필드 붙여넣는 쪽이 할 일
declarations / declaration_code 함께 가야 하는 선언 원문 있으면 버리거나, 이름을 바꿔 쓰거나, 그대로 넣는다
declaration_external_refs DDIC 사전 객체 (BKPF, LVC_T_FCAT …) 가져갈 게 없다. 대상 시스템에 있는지만 본다
declaration_params 이 조각이 쓰는 FORM 파라미터 호출 측에서 넘어오는 값이다
declaration_unresolved 변수처럼 생겼는데 선언을 못 찾은 이름 수집 안 된 인클루드·함수 인터페이스일 수 있다

판정에서 가른 두 가지

  • LIKE BSEG-WRBTR 은 작업영역이 아니다. 사전에서 타입만 빌려 쓰는 것이라 TOP 의 TABLES: BSEG. 를 딸려 보내면 안 된다. 반면 bseg-wrbtr = 100. 처럼 작업영역을 실제로 쓰면 그 선언이 필요하다. 그래서 참조를 컴포넌트까지 보존해 둘을 구분한다.
  • 호출문의 형식 파라미터는 변수가 아니다. CALL FUNCTION 'X' EXPORTING i_bukrs = gv_bukrsi_bukrsEXCEPTIONS no_rate_found 는 호출 대상의 인터페이스다. 이름만 보면 변수와 구분되지 않아(둘 다 I_/NO_) 문장 구조로 걸러낸다.

저장하지 않고 읽을 때 계산한다

조각↔선언 대응은 logic_chunk 에 캐시하지 않는다. declaration 과 조각 코드만 있으면 결정론적으로 다시 나오므로, 캐시하면 조각 재추출·선언 변경과 엇갈려 따로 늙기만 한다. 조각 하나당 수십 줄을 토큰화하는 비용이라 조회 경로에서 충분히 싸다.

실측 (샘플 55본 / 조각 3,212건, 2026-09-21)

  • 선언 18,049건 수집 (data 11.7k · constants 1.7k · field-symbol 1.0k · tables 1.0k · type-pool 488 · macro 295 · class 91 …)
  • 조각의 79% 가 정의부를 갖는다 (평균 3.5건, 최대 21건). 나머지 21%는 호출·제어·메시지처럼 프로그램 선언을 쓰지 않는 조각이다.
  • 선언을 못 찾은 이름이 남은 조각 3% — DDIC 테이블(J_1BBRANCH), 함수 인터페이스 파라미터, 화면 필드가 대부분이다.

곁가지로 고친 파서 버그 3건

정의부를 조립하면서 "선언이 없다"고 새어 나온 이름들을 좇아가 드러난 것들이다.

  1. CLASS lcl_x DEFINITION DEFERRED. 를 블록으로 열어 그 뒤 인클루드 전체를 CLASS_DEF 하나로 삼켰다 (ENDCLASS 가 없다). ZCO_ALV 의 선언 수십 건이 통째로 사라져 있었다.
  2. FORM 시그니처의 파라미터를 첫 개만 잡았다 (USING p_date LIKE x p_days LIKE y 에서 TYPE/LIKE 를 만나면 멈췄다).
  3. PARAMETER(단수형)를 선언 키워드로 몰랐다 — 미인식 문장 123건 → 3건.

API

Method Path 정의부 관련
GET /chunks/{chunk_id} declarations, declaration_code, declaration_external_refs, declaration_params, declaration_unresolved (끄려면 ?decls=false)
GET /programs/{name}/units/{unit}/code 같은 필드 (unit 통째로 가져갈 때)
GET /programs/{name}/declarations?scope=global|unit 프로그램의 선언 카탈로그

위키에서의 표시

원문은 프로그램 문서의 ## 정의부 섹션에 한 번만 싣는다 (인클루드별, 전역 선언 전체 + 조각이 실제로 쓰는 unit 로컬 선언). 조각 쪽에는 이름만 두고 그 항목으로 가는 문서 내 링크를 건다.

- 정의부(복사 시 함께 필요): [`GT_LOAD`](#decl-zfic0020top-6-gt_load), [`C_X`](#decl-zficom-213-c_x)
…
#### GT_LOAD — data · L6-L56 {#decl-zfic0020top-6-gt_load}

{#id} 는 제목에 앵커를 다는 표기이고, 뷰어(wiki_out/viewer.html)가 이걸 id 로 바꿔 눌러서 이동하게 만든다(이동 후 1.4초 강조). 뷰어 전용 기능이 아니다 — 마크다운으로 읽으면 링크 텍스트가 곧 선언 이름이라 LLM 에게는 지금까지와 똑같이 읽히고, 앵커는 무시된다.

링크와 앵커가 어긋나면 조용히 죽으므로 짝을 테스트로 묶어 뒀다 (tests/test_wiki_out.py::test_chunk_links_to_declaration). 실측 11,051개 링크 / 깨진 링크 0개.