소프트웨어 개발

데이터 조작 프로시저 최적화

출제기준 세세항목: 쿼리(Query) 성능 측정 · 소스코드 인스펙션

핵심 요약

프로시저가 느려지는 곳은 대부분 SQL입니다

데이터 조작 프로시저가 느리다면 원인은 대부분 그 안의 SQL에 있습니다. 조건문이나 변수 계산은 금방 끝나지만, SQL 한 줄은 데이터가 많아지면 수백만 행을 읽게 될 수도 있기 때문입니다. 특히 개발할 때는 테스트 데이터가 적어 아무 문제가 없다가, 실제 운영 데이터가 쌓이면서 갑자기 느려지는 일이 흔합니다.

그래서 프로시저 최적화도 '추측하지 말고 측정하라'에서 시작합니다. 어느 SQL이 시간을 가장 많이 쓰는지 숫자로 확인하고, 그 SQL이 왜 느린지 실행 방식을 들여다본 뒤, 고치고 나서 다시 재어 효과를 확인합니다. 이 항목의 두 세세항목 가운데 쿼리 성능 측정은 '어디가 왜 느린가'를 숫자로 찾는 일이고, 소스코드 인스펙션은 사람의 눈으로 코드를 함께 읽으며 느려질 원인과 결함을 미리 걸러내는 일입니다.

쿼리 성능은 무엇으로 재나요

가장 먼저 떠오르는 지표는 쿼리를 보내고 결과를 받기까지 걸린 응답 시간(경과 시간)입니다. 하지만 응답 시간은 그때 서버가 얼마나 바빴는지에 따라 달라지므로 이것만으로는 원인을 알기 어렵습니다. 그래서 쿼리가 실제로 CPU를 얼마나 썼는지(CPU 시간), 데이터를 얼마나 읽었는지를 함께 봅니다.

데이터를 읽은 양은 성능을 가장 정직하게 보여 주는 지표입니다. 데이터베이스는 데이터를 블록(페이지) 단위로 읽는데, 메모리(버퍼 캐시)에서 읽은 것을 논리적 읽기, 메모리에 없어 디스크에서 가져온 것을 물리적 읽기라고 합니다. 디스크 읽기는 메모리보다 훨씬 느리므로 물리적 읽기가 많으면 응답 시간이 길어집니다. 결과로 돌려준 행은 열 개뿐인데 읽은 블록이 수만 개라면, 필요 없는 데이터를 잔뜩 읽고 버리고 있다는 신호입니다.

한 번에는 빠르지만 너무 자주 실행되는 SQL도 살펴야 합니다. 0.01초짜리 SQL이라도 반복문 안에서 10만 번 불리면 전체 시간은 1,000초가 됩니다. 그래서 SQL별 실행 횟수와 총 소요 시간을 같이 보고, 개선했을 때 효과가 가장 큰 것부터 손을 댑니다.

측정 도구로는 DBMS가 제공하는 실행 계획 확인 기능, SQL 실행 기록을 남기는 추적(트레이스) 기능과 이를 읽기 쉽게 정리해 주는 도구(Oracle의 TKPROF 등), 그리고 애플리케이션 전체의 응답 시간과 느린 SQL을 실시간으로 보여 주는 APM 도구 등이 있습니다.

옵티마이저와 실행 계획 읽기

SQL은 '무엇을 원하는지'만 적을 뿐 '어떻게 찾을지'는 적지 않습니다. 어떻게 찾을지는 데이터베이스 안의 옵티마이저가 정합니다. 미리 정해 둔 우선순위 규칙에 따라 방법을 고르는 규칙 기반 옵티마이저(RBO)와, 테이블 크기나 값의 분포 같은 통계 정보를 보고 비용이 가장 적게 드는 방법을 고르는 비용 기반 옵티마이저(CBO)가 있으며, 요즘 DBMS는 대부분 비용 기반을 씁니다. 통계 정보가 오래되면 옵티마이저가 잘못된 판단을 할 수 있으므로 주기적으로 갱신해 줍니다.

옵티마이저가 고른 방법을 단계별로 보여 주는 것이 실행 계획입니다. EXPLAIN PLAN 같은 명령으로 확인할 수 있습니다. 실행 계획에서는 먼저 테이블을 처음부터 끝까지 다 읽는 전체 테이블 스캔(Full Table Scan)을 했는지, 인덱스를 따라 필요한 행만 찾아가는 인덱스 스캔을 했는지를 봅니다. 많은 행 가운데 몇 건만 필요한데 전체 스캔을 하고 있다면 개선할 여지가 큽니다.

여러 테이블을 합칠 때 쓰는 조인 방식도 실행 계획에 나옵니다. 한쪽 테이블의 행마다 다른 테이블에서 짝을 찾는 중첩 루프 조인(Nested Loop)은 짝을 찾는 쪽에 인덱스가 있고 처리할 행이 적을 때 유리합니다. 양쪽을 조인 열 기준으로 정렬한 뒤 맞춰 나가는 정렬 병합 조인(Sort Merge), 한쪽으로 해시 표를 만들어 짝을 찾는 해시 조인(Hash Join)은 대량의 데이터를 합칠 때 주로 쓰입니다. 해시 조인은 같다(=) 조건으로 합칠 때만 쓸 수 있고, 정렬 병합 조인은 크다·작다 같은 범위 조건에도 쓸 수 있다는 차이가 있습니다. 옵티마이저의 선택이 마땅치 않으면 SQL에 힌트를 적어 원하는 방식을 권할 수도 있습니다.

측정 결과로 무엇을 고칠까요

가장 흔한 원인은 인덱스를 쓸 수 있는데도 쓰지 못하는 경우입니다. 인덱스는 열의 원래 값을 정렬해 둔 목차라서, WHERE 절에서 그 열을 함수로 감싸거나 계산식에 넣거나 형식을 바꿔 비교하면 목차를 그대로 쓸 수 없게 됩니다. 이런 조건은 열은 그대로 두고 비교할 값 쪽을 바꿔 쓰는 형태로 고칩니다. 또 '%'로 시작하는 LIKE 검색처럼 앞부분을 알 수 없는 조건도 일반적인 인덱스를 제대로 활용하기 어렵습니다.

필요한 것만 읽는 것도 기본입니다. SELECT *로 모든 열을 가져오지 말고 필요한 열만 적으면 읽고 보내는 양이 줄어듭니다. DISTINCT나 ORDER BY처럼 정렬이 필요한 작업은 꼭 필요할 때만 씁니다. 같은 SQL을 값만 바꿔 반복 실행한다면 값을 바인드 변수로 넘겨, 데이터베이스가 SQL을 매번 새로 분석하지 않고 한 번 세운 실행 계획을 재사용하게 합니다.

프로시저에서 특히 효과가 큰 것은 처리 단위를 바꾸는 일입니다. 커서로 행을 하나씩 꺼내 반복문 안에서 UPDATE를 한 건씩 실행하면 SQL이 행 수만큼 실행됩니다. 같은 일을 조건을 건 UPDATE 한 번으로 처리하면 데이터베이스가 집합 단위로 한꺼번에 처리해 훨씬 빠릅니다. 반대로 인덱스를 무작정 늘리는 것은 주의해야 합니다. 조회는 빨라져도 INSERT, UPDATE, DELETE 때마다 인덱스도 함께 고쳐야 하므로 쓰기 작업이 느려지고 저장 공간도 늘어납니다.

소스코드 인스펙션: 함께 읽으며 결함 찾기

측정은 이미 만들어진 코드가 느리다는 사실을 알려 주지만, 코드를 실행해 보기 전에 사람이 읽어서 미리 잡을 수 있는 문제도 많습니다. 소스코드 인스펙션은 여러 사람이 정해진 역할을 나눠 맡고, 정해진 절차와 체크리스트에 따라 코드를 꼼꼼히 읽으며 결함을 찾아내는 정형화된 검토 방법입니다. 프로그램을 실행하지 않고 검사하므로 정적 테스트에 속합니다.

비슷한 검토 방법과 비교하면 특징이 잘 보입니다. 가장 가벼운 동료 검토(Peer Review)는 작성자가 동료들에게 코드를 직접 짚어 가며 설명하고 의견을 듣는 방식입니다. 워크스루(Walkthrough)는 한 단계 더 준비된 방식으로, 참석자들이 자료를 미리 받아 읽어 온 뒤 짧은 모임에서 찾아 온 결함을 확인합니다(국제 표준 문서에서는 워크스루를 작성자가 이끄는 검토로 설명하기도 합니다). 인스펙션은 이 가운데 가장 형식을 갖춘 방식으로, 작성자가 아닌 진행자가 회의를 이끌고, 참가자가 미리 준비해 오며, 찾은 결함을 기록해 수정 여부까지 끝까지 확인한다는 점이 다릅니다.

인스펙션에는 역할이 정해져 있습니다. 회의를 계획하고 진행하는 사회자(중재자, Moderator), 코드를 쓴 작성자(Author), 코드를 읽어 나가며 설명하는 낭독자(Reader), 결함을 찾아내는 검토자(Inspector), 발견한 내용을 기록하는 기록자(Recorder)입니다. 한 사람이 여러 역할을 겸하기도 하지만, 작성자가 회의를 이끌지 않는다는 원칙은 지키는 것이 좋습니다. 자기 코드는 자기 눈에 익숙해서 잘못을 지나치기 쉽기 때문입니다.

인스펙션은 어떤 순서로 진행할까요

인스펙션은 계획 → 사전 교육 → 준비 → 인스펙션 회의 → 수정 → 후속 조치 순서로 진행합니다. 계획 단계에서는 검토할 대상과 범위, 참여자와 일정을 정합니다. 사전 교육 단계에서는 작성자가 참여자들에게 코드의 목적과 배경을 간단히 설명해 모두가 같은 출발선에 서게 합니다.

준비 단계에서는 참여자들이 각자 코드를 미리 읽고 체크리스트에 따라 의심 가는 곳을 표시해 옵니다. 인스펙션 회의에서는 낭독자가 코드를 따라 읽고, 검토자들이 준비한 내용을 바탕으로 결함을 지적하며, 기록자가 이를 목록으로 남깁니다. 이 회의의 목적은 결함을 찾는 것이지 그 자리에서 고치는 방법을 토론하는 것이 아니므로, 해결책 논의는 짧게 끊습니다.

수정 단계에서는 작성자가 기록된 결함을 고치고, 후속 조치 단계에서는 사회자가 모든 결함이 제대로 고쳐졌는지 확인합니다. 수정 범위가 크면 다시 인스펙션을 열기도 합니다. 프로시저를 검토할 때는 체크리스트에 '인덱스 열을 함수로 감싼 조건이 있는가', '반복문 안에서 SQL을 한 건씩 실행하는가', '열어 둔 커서를 닫는가', '예외 처리와 ROLLBACK이 빠지지 않았는가', '값을 코드에 직접 박아 두지 않았는가' 같은 항목을 넣어 두면, 측정 전에 성능 문제와 결함을 함께 걸러낼 수 있습니다.

실무에서는 이렇게 쓰여요

함수로 감싼 날짜 조건 풀어 주기

주문일 열에 인덱스가 있는데도 '주문일을 연-월 문자열로 바꾼 값이 2026-09인 행'을 찾는 조회가 테이블 전체를 읽고 있었습니다. 실행 계획을 보니 인덱스 대신 전체 테이블 스캔이 선택되어 있었습니다. 조건을 '주문일이 2026년 9월 1일 이상이고 10월 1일 미만'처럼 열은 그대로 두는 범위 조건으로 바꾸자 인덱스 스캔으로 바뀌고, 읽는 블록 수가 크게 줄었습니다.

한 건씩 UPDATE를 한 번으로

휴면 회원을 처리하는 프로시저가 커서로 대상 회원을 하나씩 꺼내 반복문 안에서 UPDATE를 실행하고 있어, 대상이 5만 명이면 UPDATE도 5만 번 실행되었습니다. 마지막 접속일 조건을 건 UPDATE 한 문장으로 바꾸자 SQL 실행은 한 번으로 줄고, 처리 시간도 몇 분에서 몇 초 수준으로 짧아졌습니다.

인스펙션 회의에서 잡은 커서 누수

정산 프로시저 인스펙션 준비 단계에서 한 검토자가 '예외가 나면 커서를 닫지 않고 빠져나간다'는 점을 표시해 왔습니다. 회의에서 기록자가 이를 결함으로 남겼고, 작성자는 수정 단계에서 예외 처리 부분에 커서를 닫는 코드를 추가했습니다. 후속 조치 단계에서 사회자가 수정 내용을 확인해 검토를 마무리했습니다. 실행해 보기 전에 사람의 눈으로 찾아낸 결함입니다.

총정리 목차로