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)를 활용하여 작성된 초안을 바탕으로 검수 및 보완하여 작성되었습니다. 내용 중 오류나 오타가 있다면 댓글로 알려주시면 감사하겠습니다.