Community코딩 & 개발github.com

yanghangmvp/sap-abap-pitfalls

ABAP 개발 함정과 해결법 라이브러리 (프로젝트 간 공용). 실제 고객 DEV ERP 프로젝트의 100+ 건의 인도 보고서와 일일 오류 기록을 바탕으로 약 120개의 '현상 → 근본 원인 → 해결 방법'을 제공하며, 새로운 프로젝트 시작 시 체크리스트도 포함되어 있습니다.

sap-abap-pitfalls란 무엇인가요?

이 스킬은 SAP ABAP 개발 시 흔히 발생하는 함정(pitfall)과 그 해결 방법을 체계적으로 정리한 지식 베이스입니다. 실제 고객 프로젝트 경험을 바탕으로 축적된 120여 개의 사례가 '현상 → 근본 원인 → 해결 방법' 형식으로 제공되어, 신규 프로젝트 시작 시 사전에 숙독하거나 문제 발생 시 증상에 따라 빠르게 참조할 수 있습니다. 주요 내용은 ADT(Application Development Tool) 활성화 시 발생하는 가짜 성공 문제, ABAP 구문의 엄격한 제약 조건, 반환 값이 성공(S)임에도 데이터가 실제로 변경되지 않는 '무음 실패(silent failure)' 증상, 트랜잭션 및 LUW(Logical Unit of Work) 처리, 팝업, F4 도움말, ALV, 인터페이스(JSON, 게이트웨이), DDIC 객체 생성, ST22 분석 기법, 패치 스크립트 관련 문제 등을 광범위하게 다룹니다. 이 스킬은 Claude Code, Cursor, Codex와 같은 AI 에이전트가 SAP ABAP 프로젝트를 지원할 때, 특히 새로운 프로젝트를 시작하거나 '컴파일은 성공했지만 활성화 실패', '활성화는 성공했지만 런타임 오류', '반환 값은 S인데 데이터가 안 바뀜', '에러 메시지가 실제 원인과 다름', '버튼 클릭이 안 먹힘', '데이터가 이상하게 뒤섞임' 같은 난감한 상황에서 효과적으로 활용하도록 설계되었습니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/yanghangmvp/MCP_ABAP/tree/HEAD/skills/sap-abap-pitfalls

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

ABAP 개발 함정과 해결법 라이브러리

용도: 다음 프로젝트 시작 전에 한 번 정독하고, 문제 해결 시 증상별로 찾아보세요. 이 스킬은 '프로젝트 간 재사용 가능한' 경험만을 수집합니다. 환경 주소, 요청 번호, 객체 목록은 프로젝트 측 강령(예: sap-abap-project-ops)을 참조하세요. 전체 120개 항목(코드 조각 및 출처 포함)은 references/casebook.md에서 확인할 수 있습니다.

사용 방법: 새 프로젝트 시작 → 바로 §11 시작 체크리스트를 확인하세요 (시간 순서대로 실행). 이미 프로젝트가 진행 중이고 문제가 발생했다면 §10 증상 빠른 참조에서 해당 섹션을 찾아보세요. 특정 항목의 전체 코드와 출처를 깊이 있게 알고 싶다면 references/casebook.md를 참조하세요.

세 가지 메타 규칙 (어떤 단일 함정보다 중요):

  1. 에러 메시지는 실제 원인과 관련 없는 경우가 많습니다 — ABAP 컴파일러의 에러 보고 지점은 '파싱된 위치'일 뿐, '잘못 작성된 위치'가 아닙니다. 먼저 구문 순서/키워드 순서/타입 불일치를 의심하고, 구문 자체의 오류라고 단정하지 마세요.
  2. SAP에서 가장 위험한 것은 에러가 아니라 무음 실패입니다 — 'S 반환, 200 활성화, 컴파일 성공'이라는 세 가지 거짓 신호 아래에서 데이터가 조용히 잘못됩니다. BAPI, SELECT INTO TABLE, EXTENSIONIN, 텍스트 요소, Enhancement 필드 등이 관련된 경우에는 반드시 DB에 저장된 값과 대조하고 반환 값을 믿지 마세요.
  3. 실패 시 ST22를 확인하세요. 추측하지 마세요 — Short Dump(단축 덤프)의 Error analysis는 procedure "XXX" "(FORM)" 및 CX_SY_* 예외 클래스를 직접 제공하므로, FORM을 하나씩 테스트하는 것보다 훨씬 빠릅니다.

0. 열 가지 철칙 (위반 시 재작업)

  1. 이미 존재하는 객체를 수정할 때 첫 번째 단계는 현재 시스템(Production)의 소스를 다시 읽어오는 것입니다. 사용자는 종종 SE80에서 프로덕션 객체를 직접 수정하고 아카이브에 다시 쓰지 않습니다. 로컬 아카이브는 비교용으로만 사용하고 기준선(Baseline)으로 사용하지 마세요. get_source → 백업과 diff하여 0 차이 확인 → 패치 적용 → 다시 읽어서 diff 확인 → 역방향으로 아카이브 덮어쓰기.
  2. 텍스트 요소도 기준선에 속하며, get_source에는 TEXTPOOL이 포함되지 않습니다. 덮어쓰기 방식으로 텍스트를 푸시하기 전에 반드시 read_textelements로 활성화된 버전의 전체 텍스트를 가져와서 아카이브하고, 로컬 JSON과 diff한 후, 수동으로 병합한 다음에 푸시해야 합니다. 그렇지 않으면 사용자가 SE38 전면에서 수정한 문구가 흔적 없이 덮어써지고, 소스 코드 diff로는 전혀 확인할 수 없습니다.
  3. 절대 표준 테이블을 직접 UPDATE / MODIFY 하지 마세요. 이미 전기된 FI 전표를 수정하려면 FI_DOCUMENT_CHANGE (그룹 FACG, T_ACCHG에는 FDNAME+NEWVAL만 채우고, 헤더에는 I_BUZEI를 전송하지 않으며, 라인 항목에만 전송)를 사용하세요.
  4. 요청 번호(Request Number)를 직접 생성하지 마세요. 전송 가능한 객체를 푸시할 때 CORR_NR이 누락되면 → 작업을 중단하고 보고한 후, 사용자에게 번호를 요청하세요. 새 TADIR 객체를 생성하기 전에 먼저 "유형+제안 이름+TADIR 중복 검사 결과+번호 연속 부여 이유"를 보고하고 승인을 받은 후에 생성하세요. 이미 요청에 연결된 기존 객체의 후속 변경 사항은 원래 번호를 재사용하고, 다시 묻지 마세요.
  5. Update* 클래스를 사용하여 인터페이스에 대해 테스트 호출을 하지 마세요. 실제로 쓰기 작업이 수행됩니다. — 한 번은 UpdateFunctionModule을 사용하여 함수 본문만 "테스트" 용도로 전송했는데, 비활성(inactive) 소스 코드를 620자로 잘린 버전으로 덮어쓴 적이 있습니다. 프로브(Probe)는 읽기 전용 인터페이스로만 이동하세요.
  6. 인도(Delivery) 네 단계: 생성 → 소스 코드 푸시 → 텍스트 요소 푸시 (PROG/PX는 별도로 활성화해야 함) → GUI 상태 생성 (CUA). 텍스트 요소가 활성화되지 않으면 런타임 시 TEXT-xxx가 항상 비어 있습니다.
  7. 프로덕션 소스 코드 주석: 골격(뼈대)은 남기고 설명은 제거하세요. 프로그램 헤더(단위/작성자/날짜/요약/변경 기록) + *&--- 박스 라인 + FORM 배너 + -->/<-- 파라미터 문서 + 판단형 짧은 주석은 유지하세요. 설계 근거, 계산 방법 설명, 통합 테스트 결과, TODO 이유, scripts/work/ADT/ZTEST_* 참조는 제거하세요. 분리 후에는 diff를 통해 코드 본문에 변경 사항이 없음을 증명해야 합니다.
  8. 테스트/프로브 객체는 테스트 후 즉시 삭제하세요. 먼저 get_source로 backup/에 보관 → 삭제 → TADIR이 깨끗한지 다시 확인하세요. 현재 라운드에서 자신이 만든 객체만 삭제합니다. 다른 사람이 만들었거나, 이미 패키지/요청에 연결되었거나, 사용 중인 DEMO는 절대 건드리지 마세요.
  9. 잠금(Lock)을 먼저 확인하고, 백업을 먼저 수행한 후에 SAP에 쓰기 작업을 하세요. 자격 증명 파일은 외부로 전송하지 말고, 다시 에코하지 말며, 인도물에 작성하지 마세요.
  10. 사용자가 "데이터에 문제없다"고 말할 때, 오히려 프로그램에 문제가 있다고 확신하세요. "오류 금액+전표 번호"를 사용하여 입력 특성을 역추적하고, 프로브를 작성하여 해당 오류를 문자 그대로 재현하세요. 재현에 성공하면 근본 원인이 확정됩니다.

1. 활성화 및 쓰기 채널 (ADT / MCP)

  • HTTP 200 ≠ 활성화 성공 — 성공 여부는 body에 기록됩니다. activationExecuted="false"를 명시적으로 감지하고 오류를 발생시켜야 합니다. 활성화를 확인하는 유일한 신뢰할 수 있는 방법은 include를 다시 읽거나 ?version=active와 inactive 버전을 비교하는 것입니다. TRDIR-RSTAT는 S/4에 값을 쓰지 않습니다.
  • 가짜 성공은 발견하기 가장 어렵습니다 — 이전 스크립트는 <chklst:shortText> / <msg>text</msg> 정규식으로 메시지를 캡처했습니다. 실제 구조는 chkl: 접두사 + <shortText><txt> + 속성이 있는 msg입니다. → 단 하나의 메시지도 출력할 수 없으며, 세 번 연속 "활성화 성공"을 보고했지만 실제로는 131자의 빈 껍데기였습니다. 메시지를 캡처할 수 없으면 실패로 간주해야 합니다.
  • 텍스트 요소는 독립적으로 활성화 가능한 객체 PROG/PX입니다 — 부모 프로그램을 활성화해도 함께 활성화되지 않습니다. push_textelements에 GET→LOCK→PUT→UNLOCK만 있고 ACTIVATE가 없으면, PUT은 비활성(inactive) 버전만 씁니다. PROG/P를 전송하면 SAP는 활성화할 내용이 없다고 간주하여 activationExecuted="false"를 반환하고 어떤 메시지도 없습니다 ⇒ 무음 실패. 활성화 인터페이스는 corrNr를 허용하지 않습니다 (전송 시 400 오류).
  • push_textelements의 PUT은 전체 블록을 덮어쓰므로, 푸시 순서를 반대로 할 수 없습니다 — 새 기호 추가: 텍스트 요소 다음에 소스 코드. 기호 삭제: 소스 코드 다음에 텍스트 요소. TE_FILE은 기존 selections을 모두 포함하여 완전하게 제공해야 합니다.
  • 텍스트 요소에 비활성 버전만 있는 경우, 프로그램을 다시 생성해야 적용됩니다 — 텍스트 요소만 푸시하고 동일한 내용의 소스 코드를 다시 PUT해도 새로고침되지 않습니다. 소스 코드 한 줄을 변경(주석이라도)한 후 다시 푸시하거나, 프로그램을 재생성하세요.
  • 세 가지 '가짜 잠금(False Lock)': ① "이미 사용자 xxx의 요청 yyy에서 잠겼습니다"는 실제로 transport_request/corrNr누락입니다. ② UpdateDataElement에서 locked by another user 오류는 실제로 400 "이미 존재함"입니다. — handler의 isAlreadyExistsError()는 영어만 인식하므로 중국어 메시지는 판별할 수 없어 403으로 fallback되어 "잠김"으로 번역됩니다 (⇒ ZH 환경에서는 MCP가 이미 존재하는 데이터 요소를 변경할 수 없으며, UpdateDomain에는 이 전제 조건이 없으므로 영향을 받지 않습니다). ③ 403 … not specified for this client는 클라이언트(Client)가 잘못된 것입니다.
  • 진짜 잠금은 SM12에서만 삭제할 수 있습니다 — ESDICT/ESRDIRE/SEOCLSENQ 세 가지 유형의 잠금은 ADT 세션에 연결되어 있으며, DEQUEUE_ALL, _scope='3', DEQUEUE_EESDICT는 모두 효과가 없습니다. ENQUEUE_READ로 확인한 후 사용자에게 SM12에서 수동으로 삭제하도록 요청하고 즉시 다시 실행하세요. 세션 시간이 초과될 때까지 기다리지 마세요.
  • 두 가지 403 메시지의 의미는 완전히 다릅니다 — User XXX is currently editing ZFIDxxx = 실제 사용자가 SE80을 열고 있으므로 기다리세요. Object ... locked in task DEVK90011x not specified for this client = 클라이언트가 잘못되었으므로, 요청이 속한 클라이언트로 다시 푸시하세요. 이 둘을 혼동하면 불필요한 우회를 하게 됩니다.
  • ADT LOCK의 Accept 헤더는 datatype을 포함해야 합니다 — application/xml만 보내면 SAP는 이미 잠금에 성공했지만 응답 406을 받고 LOCK_HANDLE을 얻을 수 없습니다 ⇒ 잠금 누수 발생, 이후 모든 요청이 "사용자 X가 현재 편집 중"이라고 보고하여 다른 사람이 점유하고 있다고 계속 속게 됩니다. 올바른 값: application/vnd.sap.as+xml;dataname=com.sap.ad.lock.result.
  • 스크립트 fail()에서 process.exit()는 finally를 건너뜁니다 ⇒ 잠금 잔류 — 클래스 푸시 스크립트가 비정상 종료되면 SEOCLSENQ 잠금이 40분 동안 남아 있을 수 있으며, 이후 모든 LOCK이 "사용자가 SE80을 열고 있는 것처럼" 403 오류를 보고합니다. 스크립트를 작성할 때는 항상 먼저 UNLOCK한 후에 종료하거나, process.exitCode = 1로 변경하세요.
  • 사용자를 대신하여 편집 잠금을 강제로 삭제하지 마세요 — ADT에서 잠금을 삭제하면 상대방이 저장하지 않은 수정 사항이 손실됩니다.
  • include는 독립적인 ADT 리소스입니다 — POST를 /programs/programs로 보내면 항상 실행 가능한 프로그램(TRDIR-SUBC=1)이 생성되며, 이후에 변경할 수 없습니다. include는 /sap/bc/adt/programs/includes (루트 요소 <include:abapInclude>)로 보내세요. 메인 프로그램과 include는 동일한 activation 요청에 포함되어야 합니다. 그렇지 않으면 서로 정의되지 않았다고 보고합니다.
  • INSERT REPORT ... STATE 'A'로 생성된 include는 소스 코드를 활성 버전으로 작성합니다 — 이후에 비활성 버전을 수정해도 메인 프로그램 활성화는 여전히 이전 활성 버전을 기준으로 컴파일되므로, 오류가 이전 코드를 가리킵니다. 수정 후에는 INSERT REPORT 'X' FROM lt_src STATE 'A' PROGRAM TYPE 'I'. (STATE는 PROGRAM TYPE보다 먼저 와야 함)를 실행하여 활성 버전을 새로고침해야 합니다.
  • 로컬 프록시는 ADT 요청을 중단시킬 수 있습니다 — 증상은 "SAP 연결 불가"와 동일합니다 (socket hang up / CSRF 실패). MCP 하위 프로세스를 생성하기 전에 *_proxy를 삭제하고 NO_PROXY=*를 설정하세요. 쓰기 요청에는 항상 proxy: false를 사용하세요.
  • ADT에는 dynpro / GUI Status / Menu 리소스가 없습니다 — 그러나 CUA는 코드화할 수 있습니다 (RS_CUA_INTERNAL_FETCH + _WRITE, state='A', 쓰면 즉시 적용됩니다). D020S는 dynpro 화면 헤더이며 GUI 상태 테이블이 아닙니다. RS_CUA_GET_STATUS는 판단에 사용할 수 없으며, RS_CUA_INTERNAL_FETCH만 인식합니다 (10개의 필수 TABLES를 모두 제공하고, ORDER BY pposition을 사용하여 25행 잘림을 방지해야 합니다).

2. 구문 엄격 제약 (한 번에 컴파일 통과)

  • FORM 파라미터의 네 가지 금기 사항: ① TYPE c LENGTH n을 쓸 수 없습니다 (Numeric literals cannot be used as field names 오류 발생) ⇒ TYPE char<n> 또는 DDIC 유형을 사용하세요. ② 고정 길이 파라미터가 다른 길이의 실제 파라미터를 받으면 컴파일 타임에 incompatible 오류가 발생하지만, 텍스트 기호는 예외이지만 런타임에는 여전히 500 Type conflict when calling a FORM 오류가 발생합니다 ⇒ 길이를 넘어서는 경우 제네릭 TYPE c를 사용하세요. ③ TYPE STANDARD TABLE OF를 인라인할 수 없습니다 (파라미터가 8개로 계산되어 Different number of parameters 오류 발생) ⇒ 먼저 TYPES로 이름을 지정하세요. ④ FORM 내에서 FIELD-SYMBOL을 선언하면 로컬이므로, FORM 간에 사용하려면 전역으로 변경해야 합니다.
  • FORM ... TABLES는 USING/CHANGING보다 먼저 작성해야 합니다 — USING ... CHANGING ... TABLES로 작성하면 Field "TABLES" is unknown. 오류가 발생합니다 (키워드를 파라미터 이름으로 인식). PERFORM 호출 측도 마찬가지입니다.
  • CALL FUNCTION 구문 순서는 강제됩니다 — EXPORTING → IMPORTING → CHANGING → TABLES → EXCEPTIONS. TABLES를 IMPORTING 앞에 쓰면 Unable to interpret "EV_OBJNR" 오류가 발생하며, 순서 문제임을 전혀 알 수 없습니다. 호출된 쪽의 EXPORTING 파라미터는 호출하는 쪽의 IMPORTING 섹션에 작성해야 합니다. 반대로 쓰면 CALL_FUNCTION_PARM_UNKNOWN 오류가 발생하며, "FM 인터페이스가 생성되지 않음"으로 오판하기 쉽습니다.
  • 다중 필드 SELECT는 쉼표로 구분해야 하며, @ 호스트 변수는 이스케이프 처리해야 합니다 — elements in the "SELECT LIST" list must be separated using commas 오류가 발생합니다. UPDATE ... SET 다중 할당도 마찬가지입니다. WHERE 절의 상수( CONSTANTS 포함)에도 @가 필요합니다.
  • INTO와 UP TO n ROWS의 위치는 활성화 도구의 오류 메시지를 기준으로 해야 합니다 — JOIN/서브쿼리/SQL 표현식이 포함된 문은 엄격 모드에 속하며, INTO를 마지막에 강제합니다 (SELECT 563). UP TO n ROWS는 WHERE보다 먼저 와야 합니다. 두 가지 작성 방식은 서로 다른 명령문에서 다르게 동작하므로, 기억에 의존하여 복사하면 실패할 수 있습니다.
  • ASSIGN ... CASTING TYPE c LENGTH <변수>는 지원되지 않습니다 — 길이는 리터럴이어야 합니다. LENGTH를 제거하면 인덱스 범위를 초과할 때 Invalid subfield access 덤프가 발생합니다. 올바른 해결책은 cl_abap_container_utilities=>fill_container_c( )를 사용하는 것입니다 (표준 함수 READ_CONTAINER_C와 역함수 관계).
  • DESCRIBE FIELD의 TYPE은 COMPONENTS보다 먼저 와야 합니다 — 구성 요소 수만 가져오려는 경우에도 작성해야 합니다: DESCRIBE FIELD cs_data TYPE lv_type COMPONENTS lv_count.
  • CONSTANTS는 VALUE를 포함해야 합니다 — CONSTANTS: c_x TYPE flag.는 "VALUE ..." is not specified. 오류를 발생시킵니다.
  • MESSAGE TEXT-xxx TYPE 'S' WITH n은 유효하지 않습니다 — 텍스트 기호는 WITH가 있는 메시지 텍스트로 사용할 수 없습니다. 도우미 FORM을 사용하세요: lv_text = pv_text → REPLACE '&' IN lv_text WITH lv_num → MESSAGE lv_text TYPE 'S'.
  • 고정 길이 유형에는 char<n>이라는 이름이 없습니다 — TYPE char1000/char55/char220은 모두 유효하지 않으며, Type "X" is unknown 오류를 발생시킵니다. 항상 TYPE c LENGTH n을 사용하세요. 유형 이름을 잘못 추측하면 한 번의 활성화 낭비가 발생하므로, 확실하지 않으면 DD04L 또는 get_source를 확인하세요.
  • class=>method는 정적 메서드에만 사용할 수 있습니다 — 인스턴스 메서드는 CREATE OBJECT lo. lo->method( )를 사용해야 하며, 그렇지 않으면 can only be specified with static methods 오류가 발생합니다.
  • 문자열 템플릿은 PERFORM ... USING의 실제 파라미터로 사용할 수 없습니다 — Field "|" is unknown 오류가 발생합니다. 먼저 변수에 할당한 다음 전송하세요.
  • CALL TRANSACTION ... WITH f1 v1은 유효한 구문이 아닙니다 — WITH는 AUTHORITY-CHECK에만 사용됩니다. 파라미터 전송은 USING bdcdata를 통해서만 가능합니다. SM30의 첫 번째 화면에는 테이블 이름이라는 하나의 필드만 있습니다 (VIMDYNFLDS에 업데이트 모드 필드 없음). 수정 모드로 들어가려면 VIEW_MAINTENANCE_CALL + action = 'U'를 사용하세요.
  • REPLACE ALL OCCURRENCES OF ... WITH ''는 이 유형 시스템에서 종료 없는 루프(endless loop)를 유발할 수 있습니다 — 공백/문자 삭제는 문자별 DO strlen TIMES + lv_txt+lv_i(1) 필터링으로 변경하세요.
  • CONCATENATE는 숫자 유형을 허용하지 않습니다 — lv_cnt TYPE i는 '문자형이어야 합니다(must be character-like)' 오류를 발생시킵니다. 내장 함수 (strlen/lines)는 &&에 직접 사용할 수 없습니다 (The syntax for a method specification is objref->method 오류가 발생하며, 한 번에 여러 개의 오류가 발생하여 화살표 구문 오류로 오인하게 됩니다). 숫자는 항상 문자열 템플릿 |...{ lv_cnt }...|를 사용하세요.
  • $TMP 프로브 내에서는 FORM을 정의할 수 없습니다. 메서드 본문 외부의 주석은 저장되지 않습니다 — 프로브 활성화 시 Statement is not accessible 오류가 발생합니다 (CheckProgram은 오류를 보고하지 않음). 클래스 내 ENDMETHOD와 다음 METHOD 사이의 주석은 unknown comments which can't be stored 오류를 발생시킵니다.
  • SELECT-OPTIONS 이름은 8자 이하여야 합니다 — 9자이면 첫 번째 활성화가 거부됩니다. 짧은 이름으로 변경하려면 텍스트 요소 selections 항목 이름도 함께 변경해야 합니다 (이전 변형은 이전 이름을 가지고 있으므로, 오류 메시지 없이 자동으로 무시됩니다. 힌트만 제공되고 오류는 보고되지 않음).

3. 무음 실패 목록 (가장 중요한 섹션)

'S 반환 / 200 활성화 / 컴파일 성공'이지만 결과가 올바르지 않은 경우, 먼저 여기를 확인하세요.

3.1 데이터 잘못 배치 및 손실

  • SELECT 명시적 열 목록 INTO TABLE <미리 선언된 전체 테이블 유형>은 위치별로 값을 할당합니다 — 유형이 호환되는 경우 오류나 경고 없이 통과됩니다: DD07T의 DOMVAL이 DOMNAME에 들어가고, COEP의 WKGBTR이 MEGBTR에 들어갑니다 (금액은 항상 비어 있음). ZTEPIC3A는 component "ZJK" not compatible with "ZJYRQ" 오류를 보고합니다 (_act_dbg로 좌표를 가져와야 함). 하나의 함정이 5개의 보고서에서 재발했습니다. 세 가지 해결 방법: SELECT * / INTO TABLE @DATA(...) 인라인 (구조는 목록에 의해 결정됨) / 열 순서가 일치하는 좁은 구조를 직접 생성하거나 INTO CORRESPONDING FIELDS OF TABLE을 사용하세요.
  • LEFT JOIN의 오른쪽 테이블 필드를 WHERE에 작성하면 ⇒ 의미적으로 INNER JOIN으로 퇴화됩니다 — 오른쪽 테이블에 일치하는 항목이 없으면 NULL이 되고 조건이 성립하지 않습니다. 필터 조건은 ON 절로 이동하세요.
  • DELETE ADJACENT DUPLICATES ... COMPARING <비-키 필드>는 데이터를 삼킬 수 있습니다 — 조건 테이블 A017의 기본 키에는 DATBI가 포함되고, KNUMH는 키가 아닙니다. COMPARING knumh로 중복을 제거하면 동일한 KNUMH를 가진 후속 유효 기간 세그먼트가 모두 삭제됩니다. 먼저 전체 키로 SORT BY한 다음 전체 키로 COMPARING하세요.
  • AT NEW / AT END OF는 물리적 인접성에 따라 그룹을 판단하므로, ALV에서 다시 쓴 후에는 항상 무효화됩니다 — REUSE_ALV_*는 t_outtab을 제자리에서 재정렬합니다 (사용자가 메시지 열별로 정렬하면 트리거됨). 동일한 그룹이 더 이상 인접하지 않게 되어 AT END OF가 여러 번 트리거됩니다. 그룹화는 "외부 루프에서 중복 키 탐색 × 내부 루프에서 키별 수집"으로 변경하고, 그룹 경계는 비즈니스 키만 인식합니다.
  • APPEND는 sy-tabix를 변경합니다 — APPEND INITIAL LINE ... ASSIGNING <fs> 후에 sy-tabix를 가져오면 세그먼트 번호가 반환되고 원래 행 번호가 아닙니다 ⇒ 간격이 확대되어 128개 행이 558개 행으로 출력됩니다 (동일한 레코드가 43번 반복됨). 루프 시작 시 먼저 lv_tabix = sy-tabix.를 저장하세요.
  • READ TABLE ... WITH KEY가 키를 찾지 못하면 sy-tabix를 0으로 설정합니다 — 이후 APPEND sy-tabix TO lt_del은 삭제 목록에 기록되고, DELETE ... INDEX 0은 직접 TABLE_INVALID_INDEX 덤프를 발생시킵니다. 루프 시작 시 행 번호를 저장하고 삭제 전에 IF idx >= 1 AND idx <= lines( )를 확인하세요.
  • IF 분기 내에 인라인 DATA(...) 선언이 있고 조건이 충족되지 않으면 내부 테이블이 생성되지 않습니다 ⇒ 이후 LOOP에서 짧은 덤프 발생. 선언을 메서드 헤더로 올리거나, 데이터 검색 전에 비어 있는지 확인하세요.

3.2 BAPI / 표준 함수가 'S를 반환하거나 오류 없이 쓰기 실패'

  • BAPI의 x 구조는 원래 구조와 쌍으로 전송되어야 합니다 — depreciationareas만 전송하고 depreciationareasx를 누락하면 ⇒ AFASL을 채웠을 때 No change parameter given for depreciation area 01 오류가 발생합니다. x 구조가 누락되어도 구문 오류는 발생하지 않으며, 데이터 조건이 충족될 때만 트리거됩니다.
  • SD Enhancement 필드 식별 구조의 데이터 요소는 CHAR1이어야 합니다 — SD_SALESDOCUMENT_CHANGE는 GET_CHANGEABLE_FIELDS를 사용하며, WHERE rollname = 'CHAR1' OR 'CHAR01' 조건에 맞는 필드만 가져옵니다. BAPIUPDATE를 사용하는 모든 필드는 제외됩니다 ⇒ S를 반환하고, 값이 변경되지 않으며, 오류도 없습니다. 반면 생성 경로는 이 목록을 거치지 않습니다 ⇒ 전형적인 '생성은 되는데 수정이 안 됨' 증상. BAPIUPDATE로 되돌릴 수 없습니다.
  • BAPIPAREX-VALUEPART1는 240자입니다 — 표준 함수는 오프셋을 기준으로 VALUEPART1~4를 연속적으로 읽습니다 (총 960자). 첫 번째 세그먼트만 채우면 나머지는 자동으로 잘립니다 (BAPE_VBAP에서 실제로 271자, 마지막 3개 필드는 모두 비어 있고 오류 없음). 240자 단위로 네 개의 세그먼트를 채우세요.
  • 차변/대변 메모/비계획 라인 주문 유형: 수량은 TARGET_QTY만 인식합니다 — BAPISDITM에는 KWMENG를 참조하는 필드가 없습니다. 이러한 유형의 항목 범주는 TVEPZ에 구성이 없습니다 ⇒ 계획 라인이 생성되지 않으며, 전송된 BAPISCHDL은 자동으로 무시되고, S를 반환하지만 KWMENG=0, NETWR=0, VBEP 0행입니다. 판단 기준은 TVEPZ에 항목이 있는지 여부이며, TVAP-LFREL을 사용할 수 없습니다 (Z001과 Z014의 LFREL은 모두 비어 있음). VBTYP도 사용할 수 없습니다. 읽기 측에서도 동일한 함정: VBAP-KWMENG는 항상 0이고, 수량은 VBAP-ZMENG에 있습니다 ⇒ 역으로 채우는 계산 방식은 "KWMENG가 비어 있지 않으면 우선, 비어 있으면 ZMENG로 대체"입니다.
  • BAPE_*의 첫 번째 필드는 키 위치이며, 수정 모드에서 누락되면 전표 번호가 지워집니다 — 표준 함수는 EXTENSIONIN 값을 E_VBAKKOM으로 move-corresponding하는데, VBELN이 비어 있으면 전표 번호가 비어 있는 값으로 덮어쓰여집니다 ⇒ V1 045 다음에 V4 219가 옵니다 (근본 원인은 V4 219 앞에 있는 E 메시지를 확인).
  • SD 참조 문서는 세 가지 구성 요소입니다 — REF_DOC + REF_DOC_IT + REF_DOC_CA(참조 증빙 유형). 처음 두 개만 전송하면 V2 068 contains no doc. category but contains C 오류가 발생합니다 (&2가 비어 있음). 행 번호를 0으로 설정하거나 문서가 존재하지 않는 경우에도 동일한 메시지이지만 &3이 비어 있습니다. 유형은 참조되는 문서 자체의 VBAK-VBTYP에서 가져오며, 헤더 VGTYP에서 가져오지 않습니다.
  • SD_SALESDOCUMENT_CHANGE 진입점은 두 테이블을 각각 정렬합니다 — 삭제 행은 ITEM_IN과 ITEM_INX 모두에 ITM_NUMBER를 채워야 합니다. ITEM_INX만 채우면 위치가 잘못되어 완전히 관련 없는 T184 no item category available 오류가 발생합니다.
  • BAPI_USER_EXISTENCE_CHECK는 이 유형 시스템에서 모든 계정에 대해 0을 반환합니다 (ZZZZZZ999도 통과) ⇒ 검증이 무용지물이 되어, 유효하지 않은 계정으로 전송된 문서는 두 인터페이스 모두에서 볼 수 없습니다. 직접 USR02를 조회하고 길이를 확인하세요 (0 < len <= 12).
  • DELETE_TEXT는 존재하지 않는 텍스트에 대해 RAISING NOT_FOUND를 발생시킵니다 — 표준 함수가 이 E 메시지를 BAPIRET2에 추가하고, frm_check_return에서 실패로 판단합니다 ⇒ "지우기"가 오히려 인터페이스를 E 상태로 만듭니다. 지우기 전에 먼저 STXH를 확인하세요 (TDOBJECT+TDNAME+TDID+TDSPRAS). 존재하지 않으면 건너뛰고 (작업이 멱등원(idempotent)이 되도록 함). 또한, SD_SALES_DOCU_MAINTAIN에서 지우기 기능 코드는 **003**입니다 (DELETE_TEXT). &&/005를 전송하면 텍스트가 빈 줄로 다시 쓰여집니다.
  • 새로운 가격 결정 조건을 추가할 때 단계 번호를 지정할 수 없습니다 — COND_ST_NO='001'을 하드코딩했는데 ZP01이 STUNR=010에 있으면 ⇒ Condition type not in step number 001 오류가 발생합니다. 비워 두면 가격 결정 절차가 자동으로 위치를 찾습니다. "조건이 이미 존재하는지 여부"를 사용하여 UPDATEFLAG의 I/U를 결정하세요.
  • 인터페이스에서 두 번째 분할 시 OBJ_KEY는 하나의 증빙 번호만 반환합니다 ⇒ 전표 수가 적게 보고되고, 행을 기준으로 다시 쓰면 잘못된 증빙을 가리킵니다. OBJ_KEY 레이아웃은 증빙번호(10)+회사코드(4)+회계연도(4)이며, 연도는 **+14(4)**에 있습니다 (종종 +10(4)로 잘못 작성되어 회사 코드를 가져옴).
  • BSEG-BUZEI는 NUMC(3)이므로, 단일 증빙의 물리적 최대 행 수는 999이며, BAPI는 실제로 990행만 수용합니다 — 분할 상한은 989 세부 + 1 차액입니다. 경계는 999/1000/1997을 포함해야 합니다 (1997은 >999로 판단하면 1000행이 잘려서 넘침).
  • BAPIACHE09에는 EXCH_RATE 필드가 없습니다 — 이중 통화(00+10행)의 경우, 환율이 있는 00행만 제공하면 시스템은 OB08에 따라 헤더 환율을 재설정합니다 (7을 입력하면 8.28이 됨). CURR_TYPE='10' 행에도 exch_rate가 있어야 하며, 세부 행과 차액 행 모두에 필요합니다.
  • 이중 통화 증빙의 각 ITEMNO_ACC에 대해 두 개의 CURRENCYAMOUNT를 제공해야 하며, 각 통화 유형의 소계는 0이어야 합니다 — 10행이 누락되면 Inconsistent currency information 오류가 발생합니다. 분할로 인해 생성된 차액 행도 마찬가지로 추가해야 합니다.
  • UMSKZ는 세 개의 BAPI 구조 모두에 필드가 없습니다 — 어음 유형(W/S)의 경우 표준 BAPI 채널을 통해 전송되면 자동으로 무시되고 계정이 잘못 기록됩니다. EXTENSION2 Enhancement를 통해 데이터베이스에 저장하거나, 전체를 POSTING_INTERFACE_*(FB01 배치 입력)를 통해 처리해야 합니다.
  • READ_TEXT의 NAME 파라미터는 TDOBNAME(70)이지만, 실제 파라미터 TDNAME(16)은 런타임 유형 충돌을 유발합니다 — 컴파일은 성공하고, 빈 입력에서는 정상이지만, 데이터가 일치하면 Type conflict during a function module call 500 오류가 발생합니다. 파라미터는 TYPE tdobname으로 작성하거나, STXL 클러스터 테이블을 직접 읽으세요.

3.3 텍스트 / 설명 / 열 제목이 비어 있음

  • 필드에 익명 내장 유형을 사용하면 ⇒ DD03T 텍스트가 비어 있게 되어 ⇒ ALV 열 제목이 완전히 비어 있습니다 — 필드는 데이터 요소에 연결되어야 합니다 (연결 후 DD03T가 0행이 되는 것은 올바른 상태이며, 텍스트는 위로 이동함). 구조 필드 텍스트는 DDL에서 필드별로 @EndUserText.label을 통해서만 설정할 수 있습니다. 중첩된 STRU/TTYP 유형 필드에는 DD03T 텍스트가 절대 없습니다 (SAP 표준 구조도 마찬가지). CreateStructure의 fields[].description은 DD03T에 들어가지 않습니다.
  • 텍스트 요소가 활성화되지 않았거나 / 비활성 버전만 있는 경우 ⇒ 런타임 시 TEXT-xxx가 항상 비어 있습니다 — ALV 열 제목이 DDIC 짧은 텍스트로 되돌아가고, 상태 열이 비어 있는 것으로 나타납니다.
  • 도메인에 유형만 정의되고 값 집합이 유지 관리되지 않으면 ⇒ 선택 화면 F4에 후보가 표시되지 않습니다 — DD07T를 조회할 때 AS4LOCAL='A'를 포함해야 합니다 (그렇지 않으면 비활성 행을 가져옴). ORDER BY VALPOS를 사용해야 합니다 (그렇지 않으면 1,10,11,2,3... 사전순으로 정렬됨). 언어는 도메인 마스터 언어 DD01L-DOMMASTER로 대체됩니다 (필드 이름은 MASTERLANG이 아님).
  • MESSAGE ... WITH는 전체 텍스트에 대해 약 70자 상한이 있습니다 ⇒ 긴 BAPI 오류는 자동으로 Field Reas...로 잘려서 디버깅 방향을 잘못 잡게 합니다. 짧은 접두사와 원본 텍스트를 사용하는 문자열 템플릿(≤220자, BAPI_MSG 정렬)을 사용하세요.
  • BAPIACGL09-ITEM_TEXT(50) / BAPIACHE09-HEADER_TXT(25)는 자동으로 오른쪽이 잘립니다 — "접두사+접미사"를拼接할 때 먼저 접두사를 길이-접미사 길이로 자릅니다.
  • DDIC 구성 요소 이름 30자가 자동으로 잘립니다 — JSON 키 이름 매핑 시 키가 갑자기 손실될 수 있습니다.

3.4 권한 및 필터링

  • AUTHORITY-CHECK에 빈 값을 전송하면 일반적으로 권한이 없으므로 ⇒ 관련 레코드가 영구적으로 표시되지 않습니다 — 역할에 EKGRP = space가 부여되지 않습니다. 조직 수준 필드가 비어 있으면 권한 확인을 수행하지 않고 직접 통과시킵니다: IF lv_ekgrp IS NOT INITIAL. AUTHORITY-CHECK ... ENDIF.. 전체 화면을 거부하는 대신 행 수준 필터링을 사용하세요. 그렇지 않으면 권한이 없는 사용자가 트랜잭션 코드에 진입할 수 없습니다.
  • MODIF ID는 표시/숨기기만 제어하며, 숨겨진 필드에 이미 채워진 값은 여전히 필터링에 참여합니다 — 숨김 시나리오의 경우 데이터 검색 측에서 RANGE를 사용하여 명시적으로 차단하세요.

4. 트랜잭션 및 LUW

  • EXPORT ... TO DATABASE(클러스터 로그)는 트랜잭션의 제어를 받습니다 — "기본 테이블 쓰기 → 증빙 생성 → 실패 롤백"은 로그와 기본 테이블이 동일한 LUW 내에 있게 하여 ROLLBACK이 indx=01 입력 매개변수 스냅샷을 함께 제거합니다 (indx=02는 남아 있음). 또한, 기본 전기 FM 내부에서 BAPI_TRANSACTION_ROLLBACK을 추가로 호출하므로, "자체 ROLLBACK 제거" 또는 "순서 변경"은 효과가 없습니다. ⇒ 해결 방법: 유일한 쓰기 작업 전, BAPI 호출 전에 독립적인 COMMIT WORK를 삽입하세요. ROLLBACK은 원래대로 유지됩니다. 추가 이점: BAPI를 호출하기 전에 자체 LUW를 먼저 커밋하는 것은 표준 절차입니다.
  • 원자성이 필요하면 각각 커밋하지 마세요. 독립적인 커밋이 필요하면 다시 읽기를 유지하세요 — SD_SALESDOCUMENT_CHANGE의 CALL_FROM_BAPI='X'는 업데이트를 업데이트 큐에 연결하며, 커밋되지 않으면 조회할 수 없습니다 ⇒ COMMIT WORK AND WAIT를 제거한 후 유지되는 SELECT 재읽기 검증은 항상 "플래그가 업데이트되지 않음"으로 잘못 판단합니다. 전기 실패 분기에는 ROLLBACK WORK를 추가해야 합니다. 그렇지 않으면 보류 중인 SD 업데이트가 다음 커밋까지 대기 상태로 남아 잘못 저장될 수 있습니다.
  • 플랫폼 스냅샷은 FM 매개변수에만 바인딩되며, 함수 내부의 로컬 변수는 바인딩되지 않습니다 — 메시지가 lv_req/lv_resp에 있으면 플랫폼에서 볼 수 없습니다. 모니터 serialize는 name_mappings를 포함하지 않으므로, 계약 메시지가 아닌 DDIC 대문자 이름을 출력합니다. 비즈니스 측에서 자체 메시지 로그 테이블을 구축하고, save( ) 한 줄을 메시지가 생성되는 유일한 위치 (frm_send 끝)에 작성하여, 플랫폼에서 롤백으로 인해 로그가 손실되는 문제를 보완하기 위해 독립적인 커밋을 수행합니다.

5. 팝업 / F4 / ALV / 선택 화면

  • POPUP_GET_VALUES의 RETURNCODE 의미는 직관적이지 않습니다 — 'A' = 취소. 공백 = 확인 및 FIELDS가 다시 채워짐 (LSPO4U01: IF ok_code_save = 'CANC'. returncode = 'A'. ELSE. fill_values. returncode = space.). IF lv_rc <> 'A'로 작성하면 "확인"을 "취소"로 간주합니다 ⇒ 현상은 팝업 확인 후 전혀 반응이 없는 것입니다 (메시지 없음, 동작 없음, 덤프 없음).
  • POPUP_GET_VALUES는 필드가 DDIC 검색 도움말에 연결될 수 있을 때만 F4를 표시합니다 — 판단 기준은 F4IF_DETERMINE_SEARCHHELP입니다 (0=있음 / 2=필드는 있지만 도움말 없음 / 1=필드 없음). SKA1-SAKNR로 변경하여 도움말을 빌리지 마세요 — ALPHA 제로 패딩이 도입되어 '8000'이 '000008000'이 되고, 검증이 잘못 보고되며 저장 형식이 변경됩니다. ⇒ POPUP_GET_VALUES_USER_HELP + F4_FORMNAME + F4_PROGRAMNAME = sy-repid를 대신 사용하면 필드 속성이 변경되지 않습니다. 출구 FORM은 FM에 의해 **5개의 USING**으로 호출됩니다 (tabname/fieldname/display_flag/returncode/value). 선택한 값을 다섯 번째 파라미터에 다시 쓰고 returncode를 'F'로 설정하면 팝업이 새로고침됩니다 (sy-stepl을 얻을 수 없으므로 dynprofield를 통한 재입력을 사용하지 마세요).
  • POPUP_GET_VALUES는 조건에 따라 필드 숨기기를 지원하지 않습니다 — SVAL은 FIELD_ATTR(필수 입력/읽기 전용)만 제어할 수 있습니다. 유형별로 해당하는 필드 집합만 로드하고, 사용자가 유형을 변경하면 새 유형에 따라 다시 팝업합니다 (유형이 변경되지 않으면 종료되며 루프에 빠지지 않음).
  • F4IF_INT_TABLE_VALUE_REQUEST에 FIELD_TAB를 전송할 때 각 행은 INTTYPE을 유추할 수 있어야 합니다 — 유추 체인은 TABNAME → INTTYPE → DATATYPE → DOMNAME → ROLLNAME입니다.

...

[번역을 위해 잘림]

관련 스킬