ABAP SQL 튜닝 ⑥ - PACKAGE SIZE와 커서로 메모리 지키기

ABAP SQL 튜닝 6편 배너

ABAP SQL 튜닝 시리즈 여섯 번째 글입니다. ⑤편이 속도였다면, 이번은 메모리입니다.

전건을 한 번에 담으면 생기는 일

1" 500만 건을 통째로 내부 테이블에 담는다
2SELECT * FROM zsales
3  WHERE gjahr = @lv_gjahr
4  INTO TABLE @DATA(lt_all).

쿼리 자체는 문제없어 보입니다. 조건도 있습니다. 그런데 500만 건이 전부 애플리케이션 서버 메모리로 올라옵니다.

한 행이 500바이트라면 2.5GB입니다. 그러면 이런 일이 벌어집니다.

  • TSV_TNEW_PAGE_ALLOC_FAILED — 내부 테이블에 더 이상 공간을 못 잡는 덤프
  • 세션 메모리 한도를 넘어 강제 종료
  • 살아남더라도 다른 사용자의 몫까지 잡아먹어 시스템 전체가 무거워짐
WARNING

이 문제는 개발계에서 잘 안 보입니다. 테스트 데이터가 1만 건이면 아무 일도 안 일어납니다. 운영 데이터가 수백만 건일 때 처음 터집니다.

메모리 상황을 실제로 확인하는 방법은 메모리 스냅샷과 Memory Inspector 글에서 다룹니다.

PACKAGE SIZE로 나눠 읽기

해법은 단순합니다. 한 번에 다 받지 말고 묶음 단위로 받아 처리하고 버립니다.

1SELECT vbeln, posnr, netwr
2  FROM zsales
3  WHERE gjahr = @lv_gjahr
4  INTO TABLE @DATA(lt_pack)
5  PACKAGE SIZE 10000.
6
7  " lt_pack 에는 최대 1만 건씩만 들어온다
8  PERFORM process_package USING lt_pack.
9
10ENDSELECT.
!전건 조회메모리가 건수에 비례

500만 건이면 500만 건분의 메모리를 씁니다. 건수가 늘면 언젠가 반드시 터집니다.

✓PACKAGE SIZE메모리가 일정

전체가 몇 건이든 한 묶음 크기만큼만 메모리에 있습니다. 건수가 늘어도 안전합니다.

묶음 크기는 얼마가 좋나

정답은 없지만 기준은 있습니다.

  • 너무 작으면 — DB 왕복이 늘어 ④편에서 본 접근 횟수 문제가 됩니다.
  • 너무 크면 — 나눠 읽는 의미가 없어집니다.

보통 수천~수만 건 범위에서 시작해, 한 행의 크기와 실제 메모리 사용량을 보고 조정합니다. 한 행이 크면 묶음을 작게 잡아야 합니다.

INFO

PACKAGE SIZE 없이 SELECT ... ENDSELECT로 한 건씩 도는 방식도 있지만, 왕복이 지나치게 잦아 권하지 않습니다. 나눠 읽되 묶음 단위로 받으세요.

OPEN CURSOR - 명시적 커서

PACKAGE SIZE는 내부적으로 커서를 쓰지만, 그 커서를 우리가 직접 다룰 수는 없습니다. 더 세밀한 제어가 필요하면 커서를 직접 엽니다.

1DATA lv_cursor TYPE cursor.
2
3OPEN CURSOR @lv_cursor FOR
4  SELECT vbeln, posnr, netwr
5    FROM zsales
6    WHERE gjahr = @lv_gjahr.
7
8DO.
9  FETCH NEXT CURSOR @lv_cursor
10    INTO TABLE @DATA(lt_pack)
11    PACKAGE SIZE 10000.
12
13  IF sy-subrc <> 0.
14    EXIT.
15  ENDIF.
16
17  PERFORM process_package USING lt_pack.
18ENDDO.
19
20CLOSE CURSOR @lv_cursor.

언제 명시적 커서가 필요한가

  • 여러 커서를 동시에 열어 두 결과를 나란히 진행해야 할 때
  • 읽기 흐름을 다른 로직 사이에 끼워야 할 때 (한 덩어리 루프로 못 묶일 때)
  • SELECT ... ENDSELECT 블록 구조가 로직에 맞지 않을 때

그 외 단순한 대량 순차 처리라면 PACKAGE SIZE로 충분합니다.

COMMIT WORK가 커서를 닫는다

이 편에서 가장 중요한 함정입니다.

!함정커서 루프 안의 COMMIT WORK

COMMIT WORK는 데이터베이스 커밋을 일으키고, 데이터베이스 커밋은 열려 있는 커서를 닫습니다. 다음 FETCH가 정상 동작하지 않습니다.

✓대응WITH HOLD 또는 구조 변경

OPEN CURSOR WITH HOLD를 쓰면 데이터베이스 커밋 후에도 커서가 유지됩니다. 또는 읽기와 쓰기를 분리합니다.

대량 처리에서는 중간중간 커밋을 해야 하는 경우가 많아서, 이 조합이 자연스럽게 만들어집니다.

1" ✗ 위험 - 루프 안에서 커밋하면 커서가 닫힌다
2OPEN CURSOR @lv_cursor FOR SELECT ... .
3DO.
4  FETCH NEXT CURSOR @lv_cursor INTO TABLE @lt_pack PACKAGE SIZE 10000.
5  IF sy-subrc <> 0. EXIT. ENDIF.
6  PERFORM update_data USING lt_pack.
7  COMMIT WORK.                      " ← 여기서 커서가 닫힌다
8ENDDO.
9
10" ✓ WITH HOLD 로 커밋을 견디게 한다
11OPEN CURSOR WITH HOLD @lv_cursor FOR SELECT ... .
WARNING

COMMIT WORK 말고도 암묵적인 데이터베이스 커밋이 발생하는 지점이 있습니다. 다이얼로그 화면 전환, 일부 RFC 호출 등이 그렇습니다. 커서를 연 상태로 로직이 길어진다면 그 사이에 커밋이 끼어들 여지가 없는지 확인하세요.

또 하나, 한 작업 프로세스가 동시에 열 수 있는 커서 수에는 한계가 있습니다. 커서를 열었으면 반드시 CLOSE CURSOR로 닫으세요.

처리하고 버리기

나눠 읽더라도, 결과를 다시 다른 내부 테이블에 쌓으면 아무 소용이 없습니다.

1" ✗ 나눠 읽는 의미가 없다 - 결국 전건이 lt_result 에 쌓인다
2SELECT ... INTO TABLE @lt_pack PACKAGE SIZE 10000.
3  APPEND LINES OF lt_pack TO lt_result.
4ENDSELECT.
5
6" ✓ 묶음 단위로 처리를 끝내고 버린다
7SELECT ... INTO TABLE @lt_pack PACKAGE SIZE 10000.
8  PERFORM process_and_save USING lt_pack.
9ENDSELECT.

큰 내부 테이블을 다 썼다면 FREE로 즉시 반납하는 것도 습관으로 둘 만합니다.

1FREE lt_big.    " CLEAR와 달리 할당된 메모리까지 반납
TIP

CLEAR는 내용을 비우지만 확보한 메모리는 그대로 잡고 있습니다. 큰 테이블을 확실히 반납하려면 FREE를 쓰세요.

집계만 필요하다면 애초에 담을 이유가 없습니다. ③편에서 본 것처럼 DB에서 계산해 결과만 받으세요.

페이징과 패키지 처리는 다르다

⑤편과 ⑥편이 비슷해 보이지만 목적이 다릅니다. 이걸 구분해야 상황에 맞는 선택을 할 수 있습니다.

페이징 (⑤편)사용자에게 보여줄 한 조각
  • 목적: 응답 속도
  • 매번 새로운 SQL을 실행
  • 전체를 다 읽지 않는다 (읽을 생각이 없다)
  • 화면·API 단위
패키지 처리 (⑥편)전체를 나눠서 다 처리
  • 목적: 메모리 통제
  • 하나의 SQL을 커서로 이어 읽음
  • 결국 전체를 다 읽는다
  • 배치·서버 내부 처리 단위

실측으로 확인하기

고쳤으면 확인해야 합니다.

  1. 메모리 — 처리 전후 메모리 스냅샷을 찍어 비교합니다. 묶음 크기를 바꿔가며 재보면 적정값이 보입니다.
  2. 시간 — SAT로 전체 소요 시간을 봅니다. 묶음이 너무 작으면 시간이 늘어납니다.
  3. DB 왕복 — ST05에서 FETCH 횟수를 확인합니다.

메모리와 시간은 상충 관계입니다. 묶음을 줄이면 메모리는 안전해지지만 왕복이 늘어 느려집니다. 둘을 함께 보면서 균형점을 찾으세요.

정리

  • 대량 데이터를 전건 INTO TABLE로 받지 마세요. 언젠가 반드시 터집니다.
  • PACKAGE SIZE 로 나눠 읽으면 메모리 사용량이 일정해집니다.
  • 세밀한 제어가 필요하면 OPEN CURSOR, 다 쓰면 반드시 CLOSE CURSOR.
  • COMMIT WORK는 커서를 닫습니다. 중간 커밋이 필요하면 WITH HOLD를 쓰거나 구조를 바꾸세요.
  • 나눠 읽고 다시 쌓으면 의미가 없습니다. 묶음 단위로 처리를 끝내세요.
  • 큰 테이블은 FREE 로 반납하세요.

다음 ⑦편은 시리즈 마지막으로, S/4HANA의 Code-to-Data 방향을 다룹니다.

Disclaimer — 이 포스트는 AI(Claude)를 활용하여 작성된 초안을 바탕으로 검수 및 보완하여 작성되었습니다. 내용 중 오류나 오타가 있다면 댓글로 알려주시면 감사하겠습니다.