SAP 개발하다 보면 자주 만나는 상황 — 함수의 입력값이 여러 개인데 사용자가 어떤 건 채우고 어떤 건 비워서 넘기는 경우. 예를 들어 입력 매개변수가 4개(i_keyno·i_ebeln·i_ebelp·i_matnr) 일 때, 채워진 것만 조건에 넣어 SELECT 를 돌려야 합니다.
단순히 WHERE field = @i_value 만 쓰면 비어 있는 매개변수는 빈 문자열로 비교되어 매칭이 안 됩니다. IF 문으로 4가지 경우를 다 분기하면 코드가 16가지로 폭주. 해결 방법은 세 가지 — UNION 으로 SELECT 합치기 / 동적 WHERE 문자열 조립 / RANGE 변수 + IN 절.
이 글은 3가지 방법의 코드 형태·장단점·실무 추천 패턴(RANGE) + 자주 빠뜨리는 함정 까지 한 번에 정리한 메모입니다.
핵심 원리
3가지 방법의 작동 방식 비교.
| 방법 | 로직 | 언제 쓰나 |
|---|---|---|
| UNION | 각 조건별 SELECT 를 합쳐서 결과 합집합(OR 로직) | "이 조건 또는 저 조건" — 합집합 필요 |
| 동적 WHERE | WHERE 절을 문자열로 조립 → 실행 시점에 적용 | 조건 갯수·형태가 매우 다양할 때 |
| RANGE + IN (★ 권장) | 매개변수별 RANGE 테이블 → WHERE f IN range |
대부분의 경우 — 가장 안전하고 깔끔 |
핵심 트레이드오프: 합집합(OR) 이 필요하면 UNION, 정말 다양한 조건(LIKE·BETWEEN 섞임) 이면 동적 WHERE, AND 로 채워진 것만 적용 이라는 가장 일반적인 케이스는 RANGE + IN. 보통 90% 이상은 RANGE 패턴 하나로 처리됩니다.
1단계 — UNION / UNION ALL 방법
각 매개변수마다 SELECT 를 따로 작성한 뒤 UNION 으로 합칩니다.
SELECT keyno, ebeln, ebelp, matnr,
qty_a, qty_b, qty_c, qty_d,
qty_total
FROM ztxx0123
WHERE keyno = @i_keyno
UNION
SELECT keyno, ebeln, ebelp, matnr,
qty_a, qty_b, qty_c, qty_d,
qty_total
FROM ztxx0123
WHERE ebeln = @i_ebeln
UNION
SELECT keyno, ebeln, ebelp, matnr,
qty_a, qty_b, qty_c, qty_d,
qty_total
FROM ztxx0123
WHERE ebelp = @i_ebelp
UNION
SELECT keyno, ebeln, ebelp, matnr,
qty_a, qty_b, qty_c, qty_d,
qty_total
FROM ztxx0123
WHERE matnr = @i_matnr
INTO CORRESPONDING FIELDS OF TABLE @et_data.
핵심 포인트와 제약:
- OR 로직 — 4개 매개변수 중 어느 하나라도 매칭되면 결과에 포함
- 모든 SELECT 의 컬럼 순서·갯수가 동일해야 함 — SAP UNION 규칙
- NEW Open SQL(7.40+) 에서만 사용 가능 — 인라인 변수
@i_xxx형식 UNION ALL은 중복 행도 살리고, 그냥UNION은 중복 제거 (성능 손해)
언제 쓰면 좋은가:
- "발주번호 OR 자재번호 OR 키 번호 중 아무거나 매칭되면 조회" — 사용자에게 "찾기" 기능 제공할 때
- AND 로직 필요하면 부적합
2단계 — 동적 WHERE 절 (문자열 조립)
매개변수 채워진 것만 골라 WHERE 절을 문자열로 만들고 SELECT 에 전달.
DATA: lv_where TYPE string.
IF i_ebeln IS NOT INITIAL.
IF lv_where IS NOT INITIAL.
lv_where = lv_where && ' AND ebeln = @i_ebeln'.
ELSE.
lv_where = lv_where && 'ebeln = @i_ebeln'.
ENDIF.
ENDIF.
IF i_ebelp IS NOT INITIAL.
IF lv_where IS NOT INITIAL.
lv_where = lv_where && ' AND ebelp = @i_ebelp'.
ELSE.
lv_where = lv_where && 'ebelp = @i_ebelp'.
ENDIF.
ENDIF.
IF i_keyno IS NOT INITIAL.
IF lv_where IS NOT INITIAL.
lv_where = lv_where && ' AND keyno = @i_keyno'.
ELSE.
lv_where = lv_where && 'keyno = @i_keyno'.
ENDIF.
ENDIF.
IF i_matnr IS NOT INITIAL.
IF lv_where IS NOT INITIAL.
lv_where = lv_where && ' AND matnr = @i_matnr'.
ELSE.
lv_where = lv_where && 'matnr = @i_matnr'.
ENDIF.
ENDIF.
SELECT *
INTO CORRESPONDING FIELDS OF TABLE et_data
FROM ztxx0123
WHERE (lv_where).
핵심 포인트와 제약:
- AND 로직 — 채워진 매개변수 모두 만족해야 결과
- WHERE 절을 괄호로 감싸서 전달 —
WHERE (lv_where) - 비교 연산자 자유롭게(
LIKE·BETWEEN·<·>등) 조립 가능 — 가장 유연 - 단점: 문자열 오타나면 런타임 에러, 디버깅 어려움, 코드 길어짐, SQL 인젝션 위험(사용자 입력 직접 받는 경우)
3단계 — RANGE 변수 + WHERE IN (★ 권장)
매개변수마다 RANGE 테이블을 만들고 IS NOT INITIAL 일 때만 채워서 IN 절로 SELECT.
" 1) RANGE 테이블 선언
DATA: r_keyno TYPE RANGE OF ekko-bedat,
r_ebeln TYPE RANGE OF ekko-ebeln,
r_ebelp TYPE RANGE OF ekpo-ebelp,
r_matnr TYPE RANGE OF mara-matnr.
" 2) IS NOT INITIAL 인 매개변수만 RANGE 에 추가
IF i_keyno IS NOT INITIAL.
APPEND VALUE #( sign = 'I' option = 'EQ' low = i_keyno ) TO r_keyno.
ENDIF.
IF i_ebeln IS NOT INITIAL.
APPEND VALUE #( sign = 'I' option = 'EQ' low = i_ebeln ) TO r_ebeln.
ENDIF.
IF i_ebelp IS NOT INITIAL.
APPEND VALUE #( sign = 'I' option = 'EQ' low = i_ebelp ) TO r_ebelp.
ENDIF.
IF i_matnr IS NOT INITIAL.
APPEND VALUE #( sign = 'I' option = 'EQ' low = i_matnr ) TO r_matnr.
ENDIF.
" 3) SELECT — 비어있는 RANGE 는 자동으로 WHERE 무시
SELECT *
INTO CORRESPONDING FIELDS OF TABLE et_data
FROM ztxx0123
WHERE keyno IN r_keyno
AND ebeln IN r_ebeln
AND ebelp IN r_ebelp
AND matnr IN r_matnr.
핵심 포인트:
- AND 로직 — 채워진 RANGE 끼리는 AND 로 적용 (비어있는 RANGE 는 모든 행 통과)
- RANGE 테이블 구조 =
sign(I/E) ·option(EQ/BT/CP 등) ·low·high4 필드 (SELECT-OPTIONS 와 동일) - 빈 RANGE 의 IN = 모든 행 통과 — IF 분기 없어도 동작 ★
- 한 매개변수에 여러 값 받기 쉬움 (APPEND 여러 번)
- SQL 인젝션 위험 없음, 정적 SQL 그대로
가장 큰 장점은 코드가 깔끔 하고 확장이 쉽다 는 점 — 매개변수가 10개가 되어도 같은 패턴 반복이라 유지보수 편합니다.
4단계 — 비교와 선택 기준
세 방법 한눈에 정리.
| 항목 | UNION | 동적 WHERE | RANGE + IN |
|---|---|---|---|
| 로직 | OR (합집합) | AND (조립 자유) | AND |
| 코드 길이 | 길다 (SELECT 반복) | 매우 길다 (IF 분기) | 짧다 |
| 오타 위험 | 낮음 (컴파일 체크) | 높음 (문자열, 런타임 에러) | 낮음 |
| SQL 인젝션 | 없음 | 위험 (사용자 입력 시) | 없음 |
| 확장성 | 매개변수 늘면 SELECT 추가 | IF 폭주 | 같은 패턴 반복 |
| 비교 연산자 | SELECT 별로 다양 | 가장 자유 | EQ/BT/CP/NE 등 RANGE 옵션 |
실무 선택 가이드:
- 단순 "AND 로 채워진 것만 적용" → RANGE + IN (90%)
- 복잡한 OR / 다른 테이블 조합 결과 → UNION
- WHERE 자체가 동적으로 달라져야 함 (LIKE·BETWEEN 자유 조립) → 동적 WHERE
흔히 빠뜨리는 함정
IF 분기 안 하고 WHERE field = @i_value 만 쓰기
비어있는 매개변수면 빈 문자열로 비교되어 매칭이 0건. 반드시 IS NOT INITIAL 가드 또는 RANGE 패턴 사용.
UNION 의 컬럼 갯수·순서 다름
각 SELECT 의 컬럼 갯수와 데이터 타입이 똑같지 않으면 컴파일 에러. 컬럼 변경 시 모든 SELECT 동시 수정.
동적 WHERE 의 첫 조건 AND 빠뜨림
문자열 조립 시 첫 조건에는 AND 가 안 붙어야 함. IF lv_where IS NOT INITIAL 가드로 분기.
동적 WHERE 인라인 변수 누락
문자열 안에서 변수 참조는 @i_ebeln 식으로 인라인 변수 표기. 그냥 i_ebeln 만 쓰면 인식 안 됨.
RANGE 의 sign·option 오타
sign 은 'I'/'E', option 은 'EQ'/'BT'/'CP'/'NE' 등 정확한 두 글자. 소문자나 다른 표기 X.
RANGE 의 빈 IN 동작 이해 부족
빈 RANGE 에 IN 절은 모든 행 통과 가 핵심 — 이 동작 알면 IS NOT INITIAL 가드 없이 그냥 IN 만 써도 OK.
UNION 대신 그냥 OR 로 작성
WHERE keyno = @i_keyno OR ebeln = @i_ebeln OR ...
이런 코드는 비어있는 매개변수도 OR 에 포함 되어 의도와 다른 결과. 빈 매개변수 = 빈 문자열 매칭이 추가됨. UNION 또는 RANGE 가 안전.
CORRESPONDING FIELDS 누락
UNION 또는 동적 WHERE 결과를 일반 itab 으로 받을 때 컬럼이 정확히 안 맞으면 빈 값. INTO CORRESPONDING FIELDS OF TABLE 또는 명시적 INTO 컬럼 매핑 필요.
동적 WHERE 의 SQL 인젝션 위험
사용자 입력을 그대로 lv_where 에 붙이면 위험. 입력값은 인라인 변수(@i_xxx) 로만 전달하고, 필드명·연산자만 문자열 조립.
전체 코드 — 복사용 통합본
세 방법을 한 펑션 모듈에 모두 담은 통합본입니다. SE37 에 새 펑션 만들어 그대로 복사하면 동작합니다(테이블·매개변수는 환경에 맞게 교체).
*&---------------------------------------------------------------------*
*& 다중 입력 매개변수 — UNION / 동적 WHERE / RANGE+IN 세 패턴
*&---------------------------------------------------------------------*
FUNCTION z_example_dynamic_select.
*"----------------------------------------------------------------------
*" IMPORTING
*" VALUE(I_MODE) TYPE CHAR1 DEFAULT '3' " 1=UNION 2=DYN_WHERE 3=RANGE
*" VALUE(I_KEYNO) TYPE ekko-bedat OPTIONAL
*" VALUE(I_EBELN) TYPE ekko-ebeln OPTIONAL
*" VALUE(I_EBELP) TYPE ekpo-ebelp OPTIONAL
*" VALUE(I_MATNR) TYPE mara-matnr OPTIONAL
*" EXPORTING
*" VALUE(ET_DATA) TYPE ztt_example_data
*"----------------------------------------------------------------------
CASE i_mode.
* ===============================================================
* 1) UNION — OR 로직, 매개변수 어느 하나라도 매칭
* ===============================================================
WHEN '1'.
SELECT keyno, ebeln, ebelp, matnr, qty_a, qty_b, qty_total
FROM ztxx0123
WHERE keyno = @i_keyno
UNION
SELECT keyno, ebeln, ebelp, matnr, qty_a, qty_b, qty_total
FROM ztxx0123
WHERE ebeln = @i_ebeln
UNION
SELECT keyno, ebeln, ebelp, matnr, qty_a, qty_b, qty_total
FROM ztxx0123
WHERE ebelp = @i_ebelp
UNION
SELECT keyno, ebeln, ebelp, matnr, qty_a, qty_b, qty_total
FROM ztxx0123
WHERE matnr = @i_matnr
INTO CORRESPONDING FIELDS OF TABLE @et_data.
* ===============================================================
* 2) 동적 WHERE — 문자열 조립, AND 로직
* ===============================================================
WHEN '2'.
DATA: lv_where TYPE string.
IF i_keyno IS NOT INITIAL.
IF lv_where IS NOT INITIAL.
lv_where = lv_where && ' AND keyno = @i_keyno'.
ELSE.
lv_where = 'keyno = @i_keyno'.
ENDIF.
ENDIF.
IF i_ebeln IS NOT INITIAL.
IF lv_where IS NOT INITIAL.
lv_where = lv_where && ' AND ebeln = @i_ebeln'.
ELSE.
lv_where = 'ebeln = @i_ebeln'.
ENDIF.
ENDIF.
IF i_ebelp IS NOT INITIAL.
IF lv_where IS NOT INITIAL.
lv_where = lv_where && ' AND ebelp = @i_ebelp'.
ELSE.
lv_where = 'ebelp = @i_ebelp'.
ENDIF.
ENDIF.
IF i_matnr IS NOT INITIAL.
IF lv_where IS NOT INITIAL.
lv_where = lv_where && ' AND matnr = @i_matnr'.
ELSE.
lv_where = 'matnr = @i_matnr'.
ENDIF.
ENDIF.
* ★ 모든 매개변수가 비어있으면 lv_where 가 비어 전체 SELECT 위험 — 가드
IF lv_where IS INITIAL.
RETURN.
ENDIF.
SELECT *
INTO CORRESPONDING FIELDS OF TABLE et_data
FROM ztxx0123
WHERE (lv_where).
* ===============================================================
* 3) RANGE + IN — ★ 권장 패턴
* ===============================================================
WHEN '3'.
DATA: r_keyno TYPE RANGE OF ekko-bedat,
r_ebeln TYPE RANGE OF ekko-ebeln,
r_ebelp TYPE RANGE OF ekpo-ebelp,
r_matnr TYPE RANGE OF mara-matnr.
IF i_keyno IS NOT INITIAL.
APPEND VALUE #( sign = 'I' option = 'EQ' low = i_keyno ) TO r_keyno.
ENDIF.
IF i_ebeln IS NOT INITIAL.
APPEND VALUE #( sign = 'I' option = 'EQ' low = i_ebeln ) TO r_ebeln.
ENDIF.
IF i_ebelp IS NOT INITIAL.
APPEND VALUE #( sign = 'I' option = 'EQ' low = i_ebelp ) TO r_ebelp.
ENDIF.
IF i_matnr IS NOT INITIAL.
APPEND VALUE #( sign = 'I' option = 'EQ' low = i_matnr ) TO r_matnr.
ENDIF.
* ★ 빈 RANGE 는 자동으로 모든 행 통과 — IF 분기 없이 깔끔
SELECT *
INTO CORRESPONDING FIELDS OF TABLE et_data
FROM ztxx0123
WHERE keyno IN r_keyno
AND ebeln IN r_ebeln
AND ebelp IN r_ebelp
AND matnr IN r_matnr.
ENDCASE.
* ※ 참고:
* - 대부분 RANGE + IN 패턴이면 충분 (3번 모드)
* - OR 합집합 필요할 때만 UNION
* - 동적 WHERE 는 LIKE/BETWEEN 등 자유 조립 필요할 때만
* - 빈 RANGE 의 IN = 모든 행 통과 (가장 큰 장점)
* - 모든 매개변수 비어있을 때 전체 SELECT 방지 가드 권장
ENDFUNCTION.
요약
| 방법 | 키워드 | 언제 |
|---|---|---|
| 1 | UNION | OR 합집합 — "어느 하나라도 매칭" (찾기 기능) |
| 2 | 동적 WHERE | 문자열 조립 — LIKE/BETWEEN 등 자유 조건 |
| 3 | RANGE + IN ★ | AND — 채워진 매개변수만 적용, 빈 RANGE 는 자동 무시 |
입력 매개변수가 여러 개고 각각이 비어있을 수도 있을 때, IF 분기로 16가지 경우를 나누는 건 좋지 않습니다.
RANGE + IN 패턴 이 90% 케이스를 깔끔하게 처리해주는 표준입니다 — 매개변수마다 RANGE 테이블을 두고, IS NOT INITIAL 일 때만 한 줄 APPEND 하고, SELECT 의 WHERE 에는 그냥 field IN range 라고 쓰면 끝.
빈 RANGE 의 IN 절은 모든 행 통과 라는 게 핵심 — IF 분기 줄어들고 코드가 짧아지고 매개변수가 늘어도 같은 패턴만 반복하면 됩니다. OR 합집합이 정말 필요할 때만 UNION, 자유로운 비교 연산자가 필요할 때만 동적 WHERE 로 가는 게 안전한 순서입니다.
Disclaimer — 이 포스트는 실무 정리 노트를 바탕으로 AI 보조로 정리되었습니다.
UNION·동적 WHERE (lv_where)·RANGE + IN 은 ABAP Open SQL 표준 문법입니다.
UNION 과 인라인 변수(@)·VALUE #(...) 표현은 NetWeaver 7.40 이상에서 사용 가능합니다.
동적 WHERE 사용 시 사용자 입력값을 문자열에 직접 붙이지 말고 인라인 변수로만 바인딩해 SQL 인젝션을 방지해야 하며, 모든 매개변수가 비어있을 때 전체 테이블 SELECT 가 발생하지 않도록 가드(예: IF lv_where IS INITIAL. RETURN.) 를 추가하시기 바랍니다.