.NET Core 환경의 MS SQL Server 최적화를 위한 AI 파트너 Kiro: 며칠 걸리던 작업을 이제 몇 시간 만에
요약
ORM을 사용하는 환경에서 발생하는 비효율적인 SQL 쿼리를 AI 파트너 Kiro를 통해 빠르게 디버깅하는 방법을 소개합니다. Kiro는 raw query와 코드베이스를 분석하여 문제의 원인이 되는 코드를 찾아줌으로써 작업 시간을 획기적으로 단축합니다.
핵심 포인트
- ORM 생성 쿼리와 실제 코드 간의 괴리로 인한 디버깅 어려움 해결
- Kiro를 활용해 며칠 걸리던 쿼리 분석 작업을 몇 시간으로 단축
- 데이터베이스 직접 접근 없이 코드와 쿼리 컨텍스트를 통한 협업 방식
- AWS CloudWatch 로그 분석 등 애플리케이션 컨텍스트 기반 분석 가능
예전에는 데이터베이스 스파이크(spike)를 유발하는 쿼리를 찾는 데 며칠이 걸리기도 했습니다. 찾는 사람도 지치고, 수정하는 사람도 지치죠. 하지만 지금은 어떤가요? 단 몇 시간 만에 해결됩니다. 게다가 보너스로 새로운 것을 배우는 즐거움도 있습니다.
이야기는 이렇습니다. 만약 여러분이 ORM (Object-Relational Mapping — 코드와 데이터베이스 사이의 일종의 "자동 번역기")을 사용하는 애플리케이션에서 작업해 본 적이 있다면, 다음과 같은 상황이 익숙할 것입니다: 데이터베이스가 갑자기 느려지고, 원인이 되는 raw query (원시 쿼리)를 찾아냈지만, 정작 여러분의 코드베이스에는 그 raw SQL과는 형태가 완전히 다른 ORM 구문으로 작성되어 있는 상황 말이죠.
ORM을 다뤄본 적이 없는 분들을 위해 비유하자면 이렇습니다: 여러분이 인도네시아어로 메시지를 작성하면, "자동 번역기"가 이를 일본어로 변환하여 수신자에게 보냅니다. 어느 날 전송된 메시지에 문제가 생겼는데, 여러분은 일본어 버전만 볼 수 있는 상황입니다. 여러분이 작성한 인도네시아어 문장 중 어느 부분이 번역 문제를 일으켰는지 찾아내는 작업, 그 노력이 정말 잠이나 자고 싶게 만들 정도로 힘듭니다.
이제 Kiro의 도움을 받으면, raw query와 코드베이스에 대한 접근 권한만 제공하면 됩니다. 그러면 Kiro가 문제의 쿼리를 생성하는 코드의 어느 부분이 문제인지 자동으로 찾아냅니다. 예전에는 며칠이 걸렸던 일이 이제는 단 몇 시간 만에 끝날 수 있습니다. 이는 조사 단계일 뿐이며, 수정(fixing) 단계는 아직 포함되지 않은 시간입니다.
Kiro를 사용하게 된 이유
최근 직장에서 Kiro를 활발하게 사용하고 있습니다. 작년 초에 회사가 Kiro for Startup 프로그램을 통해 크레딧을 받았기 때문에, 이를 최대한 활용하고 있습니다.
MS SQL Server에서 쿼리를 디버깅하고 탐색하는 것 외에도, 때로는 AWS CloudWatch 로그를 분석하는 데에도 Kiro를 사용합니다. 이때 실행 중인 애플리케이션의 컨텍스트(context)를 함께 제공하여, 분석이 일반적이지 않고 더 정확하게 이루어지도록 합니다.
이번 글에서는 지난 몇 주 동안 .NET Core 애플리케이션의 쿼리 성능(query performance)을 개선하기 위해 파트너(partner)로서 Kiro를 어떻게 활용했는지 공유하고자 합니다. 왜 "파트너"일까요? Kiro가 데이터베이스에 직접 접근할 수 없기 때문입니다. 즉, 반드시 저라는 중개자를 거쳐야만 합니다. 우리는 함께 논의하고, 협업하며, 문제를 해결합니다. 단순히 버튼만 눌러주면 알아서 돌아가는 AI가 아닙니다.
데이터베이스가 비명을 지르기 시작했을 때
얼마 전, 저희 데이터베이스 메트릭(metrics)에서 경고가 발생했습니다. 데이터 I/O가 빈번하게 스파이크(spike)를 기록했습니다. 사용자들이 체감하기 전에 이를 점검하고 개선해야 했습니다.
앞서 말씀드린 것처럼, 이것은 제가 가장 하기 싫어하는 작업 중 하나입니다. 복잡한 이유는 다음과 같습니다. 우리는 ORM이 생성한 로우 쿼리(raw query)만 받을 수 있는 반면, 정작 코드베이스(codebase)에는 이와는 훨씬 다른 형태의 구문(syntax)으로 작성되어 있기 때문입니다. 어떤 코드 부분이 느린 쿼리를 생성하는지 즉각적으로 파악하기가 어렵습니다.
더 쉬운 비유를 들자면: ORM은 당신이 비서에게 "최신순으로 정렬해서 활성화된 주문 데이터를 가져와 줘"라고 말하는 것과 같습니다. 비서는 그 지시를 데이터베이스를 위한 기술적인 명령어로 번역합니다. 그런데 만약 비서의 번역 방식이 비효율적이어서 데이터베이스를 느리게 만든다면, 당신은 그 기술적인 번역 결과물을 가지고 다시 원래의 지시 사항으로 역공학(reverse-engineer)해야 합니다. 바로 이 점이 머리를 아프게 만듭니다.
워크플로우: Kiro는 데이터베이스를 건드릴 수 없다
데이터베이스에 AI가 직접 접근할 수 없으므로, 워크플로우는 다음과 같습니다. Kiro가 제가 실행해야 할 쿼리 지침을 주면, 제가 데이터베이스에서 실행하고, 그 결과를 다시 Kiro에게 보내 분석하게 합니다.
저는 다음과 같이 컨텍스트(context)를 제공하며 시작했습니다:
"몇 분 전 데이터 I/O 스파이크가 발생한 MS SQL Server 데이터베이스를 가지고 있습니다. 우리는 무엇이 원인인지 프로세스/쿼리를 찾아낼 것입니다. 당신은 저에게 어떤 쿼리를 실행해야 하는지 알려주세요. 그러면 제가 실행한 뒤 분석할 수 있도록 결과를 보내드리겠습니다."
그 후, Kiro는 제가 실행해야 할 몇 가지 진단 쿼리(diagnostic query)를 제공했습니다:
- Top I/O queries — 물리적 읽기/쓰기(physical reads/writes)가 가장 높은 쿼리
- Currently running I/O heavy — 스파이크(spike) 발생 시 실행 중인 I/O 집약적 쿼리
- I/O stats per database file — 어떤 파일에서 가장 많은 I/O가 발생하는지
- I/O wait stats — 지배적인 I/O 관련 대기 유형(wait type)
- Current indexes — 데이터베이스에 존재하는 모든 인덱스
- Index usage stats — 인덱스가 업데이트되는 빈도 대비 사용되는 빈도
- Missing indexes — SQL Server 자체의 인덱스 추천 사항
- Index fragmentation — 파편화된 인덱스는 과도한 I/O를 유발할 수 있음
- Resource stats — 과거 리소스 사용량 (Azure 전용)
- I/O stats per table — 어떤 테이블이 가장 많은 I/O를 생성하는지
모든 쿼리를 실행하고 결과를 보낸 후, Kiro는 즉시 분석을 시작하여 실행 요약(executive summary)부터 우선순위별 실행 계획(action plan)에 이르기까지 상당히 포괄적인 결과물을 제공했습니다.
분석 결과: 단순히 "쿼리가 느리다"는 수준이 아님
Kiro는 심각도 수준(severity level)과 함께 완전한 발견 사항을 제공했습니다:
| 발견 사항 | 심각도 |
|---|---|
| 읽기 지연 시간(Read latency)이 매우 높음 (평균 130-169ms) | 🔴 Critical |
| ... |
흥미로운 점은, Kiro가 데이터베이스 수준에서 멈추지 않았다는 것입니다. 그는 즉시 문제가 되는 코드의 패턴을 식별했습니다:
// Kiro가 발견한 문제 패턴:
var data = _db.Orders
.Include(a => a.OrderItems) // ← 모든 항목을 먼저 Eager load 함
...
.NET 비개발자를 위해 설명하자면: .Include()는 "관련된 모든 관계 데이터도 한꺼번에 가져와"라고 말하는 것과 같습니다. 문제는 이것이 페이지네이션(pagination) 이전에 배치되어 있다는 점입니다. 즉, 데이터베이스가 모든 데이터와 그 관계를 메모리에 먼저 로드한 다음 페이지 단위로 자른다는 의미입니다. 데이터가 수백만 행이라면 어떻게 될까요? 당연히 메모리 부족(Out of Memory)이 발생합니다.
Kiro는 2단계 접근 방식의 수정 권장 사항을 제시했습니다:
// Step 1: ID만 먼저 가져오기 (관계 없이 가볍게 수행)
var pagedIds = await _db.Orders
.Where(x => x.TenantId == tenantId && !x.Deleted)
...
개념은 보편적입니다: 모든 데이터를 먼저 로드한 다음 자르는 것이 아니라, 먼저 자른 다음 필요한 데이터만 로드하는 것입니다. 이는 ORM (Object-Relational Mapping)을 지원하는 모든 프레임워크와 언어에 적용 가능합니다.
코드 레벨의 수정 외에도, Kiro는 데이터베이스 레벨에서의 실행 계획(action plan)을 제공합니다:
- 누락된 인덱스 생성 (Create missing index): 필터링에 자주 사용되지만 아직 인덱스가 없는 컬럼에 대해 수행
- 사용하지 않는 인덱스 삭제 (Drop unused index): 읽기 작업에는 전혀 사용되지 않으면서 쓰기(write) 작업이 발생할 때마다 계속 업데이트되는 인덱스 (이득 없는 비용 발생)
- Azure의 자동 인덱스(auto-index) 비활성화: 때때로 불필요한 인덱스를 생성하여 오버헤드를 유발하는 기능
단순히 AI에 쿼리를 붙여넣는 것과 결과가 다른 이유
위에서 언급한 실행 계획을 보면, Kiro는 단순히 느린 원시 쿼리(raw query)를 분석하여 "인덱스를 추가하세요"와 같은 일반적인 제안을 하는 것에 그치지 않습니다. 차별점은 다음과 같습니다: Kiro는 해당 쿼리의 근원이 되는 .NET Core 백엔드 코드를 함께 학습합니다.
예를 들어, 느린 원시 쿼리가 발견되면 Kiro는 스스로 추적할 수 있습니다 — "아, 이 쿼리는 OrderController.GetDataTableParams의 몇 번째 라인, GetOrdersWithDateFilter 리포지토리 메서드에서 생성되었습니다." 제가 어떤 파일인지 알려줄 필요가 없습니다. Kiro가 코드베이스(codebase) 내에서 직접 찾아내기 때문입니다.
이를 통해 제안은 템플릿 답변이 아닌, 우리의 상황에 특화된 구체적인 내용이 됩니다:
- 튜닝이 필요한 쿼리는 SQL 측면뿐만 아니라 ORM이 이를 생성하는 방식 측면에서도 고려됩니다. 예를 들어, 페이지네이션(pagination)이 작동하기 전에 데이터베이스가 수백만 개의 행을 로드하게 만드는
.Include()위치의 경우, 이는 데이터베이스 레벨이 아닌 코드 레벨에서의 리팩터링(refactor)을 제안합니다. - 우리의 설정에서 오히려 역효과를 내는 Azure 기능이 있습니다: 활성화된 자동 인덱스가 읽히지 않는 인덱스를 생성하여, 아무런 이득 없이 약 47,000건의 업데이트 비용을 추가하고 있었습니다.
- 삭제를 제안한 인덱스는 추측이 아닙니다. Kiro는 사용 통계(사용 횟수 0회, 쓰기 횟수 수만 회)를 교차 검증하여 코드베이스 내에 해당 인덱스를 필요로 하는 쿼리가 실제로 없음을 확인합니다.
이는 단순히 AI에게 쿼리 한 조각을 던져주고 의견을 묻는 것과는 다릅니다. Kiro는 전체 코드베이스(codebase)에 접근할 수 있기 때문에, "데이터베이스의 느린 쿼리"에서부터 "그 쿼리를 생성하는 애플리케이션의 코드 패턴"에 이르기까지 점들을 연결(connect the dots)하여 양쪽 측면을 모두 아우르는 통합된 솔루션을 제공할 수 있습니다.
AI는 우리를 바보로 만드는 것이 아니라, 오히려 학습 파트너가 된다
제가 자주 듣는 말 중 하나는 "AI가 우리를 더 바보로 만들고 학습을 게으르게 만든다"는 것입니다. 하지만 이번 사례에서의 제 경험은 오히려 그 반대였습니다.
이야기는 이렇습니다. Kiro가 코드베이스에서 문제가 되는 패턴을 발견했을 때, 단순히 "이것은 틀렸으니 이렇게 바꾸세요"라고 말하는 데 그치지 않았습니다. 왜 그것이 틀렸는지, 내부적으로 어떤 일이 일어나는지, 그리고 그 패턴이 실제로 사용될 수 있는 상황은 언제인지를 설명해 주었습니다. 그리고 수년간 .NET 코드를 작성해 온 저조차도 그동안 놓치고 있었던 몇 가지 근본적인 사항들을 비로소 깨닫게 되었습니다.
Kiro가 단순한 스마트 자동 완성(autocomplete)을 넘어 "학습 파트너"가 된 이유는 다음과 같습니다. Kiro가 흥미로운 무언가를 발견할 때마다, 저는 더 자세한 설명을 요구하고 이를 별도의 마크다운(markdown) 파일로 저장하도록 했습니다. 제목은 학습 노트처럼 만들었습니다. "오늘 배운 것: 왜 LINQ 쿼리의 .ToLower()가 데이터베이스를 느리게 만드는가."
그 후 이 파일들을 PDF로 생성하여 팀 Slack에 공유했습니다. 덕분에 저 혼자 배우는 것이 아니라 팀 전체가 통찰(insight)을 얻을 수 있었습니다.
다음은 이 과정을 통해 제가 배운 몇 가지 사례입니다:
ORM 쿼리의 .ToLower()가 의외로 데이터베이스를 느리게 만든다
Kiro는 이 패턴이 여러 리포지토리(repository)에 퍼져 있음을 발견했습니다:
// ❌ 인덱스를 사용할 수 없게 만듦
db.Products.Any(x => x.Code.ToLower() == code.ToLower());
왜 문제가 될까요? ORM은 .ToLower()를 SQL의 LOWER() 함수로 변환합니다. WHERE 절에서 데이터베이스 컬럼에 함수가 적용되면 인덱스(index)를 사용할 수 없게 됩니다. 즉, SQL Server가 모든 행을 하나씩 스캔(scan)해야 합니다.
160만 개의 행(rows)이 있는 테이블의 경우: 원래 1밀리초(ms) 미만이어야 할 작업이 2~5초가 걸렸습니다. 그리고 저희 케이스에서는 이 함수가 시간당 약 2,400번 호출됩니다. 즉, 불필요한 2,400번의 전체 테이블 스캔(full table scan)이 발생하고 있었던 것입니다.
알고 보니 저희 데이터베이스는 대소문자를 구분하지 않는 콜레이션 (Collation Case Insensitive, CI)을 사용하고 있었습니다. 즉, 어떤 함수를 사용할 필요 없이 문자열 비교 시 자동으로 대소문자를 구분하지 않는다는 의미입니다:
// ✅ 인덱스(index)가 사용됨, DB 콜레이션이 이미 CI이므로 결과는 동일함
db.Products.Any(x => x.Code == code);
Kiro가 설명하여 저를
그리고 제가 배운 다른 사항들도 더 있습니다. 나중에 별도의 아티클로 분리해야 할 정도로 꽤 많습니다.
핵심은 이렇습니다: 우리가 정확하게 프롬프팅 (Prompting)을 할 수 있고 "왜?"라고 물어보는 호기심을 가지고 있다면, AI는 오히려 학습을 위한 가속기 (Accelerator)가 됩니다. AI는 사고를 대체하는 것이 아니라, 지금까지 우리가 존재조차 몰랐던 것들로 향하는 문을 열어주는 도구입니다. 그리고 이러한 통찰 (Insight)을 꾸준히 저장한다면, 한 번의 학습 세션이 팀 전체를 위한 지식 공유 (Knowledge sharing)가 될 수 있습니다.
버리기 아까운 성공적인 토론 — 워크플로우 (Workflow)로 만들기
이 부분이 제가 생각하기에 가장 영향력 있는 지점입니다.
긴 토론 — 쿼리 (Query)를 주고받고, 분석 결과를 받고, 해결책을 논의하는 과정 — 을 거친 후 저는 깨닫기 시작했습니다. "패턴을 찾았어. 내일 다른 데이터베이스에서 스파이크 (Spike)가 발생한다면, 매번 처음부터 다시 반복해야 할까?"
그래서 저는 Kiro에게 요청했습니다: 오늘 우리의 토론 내용을 재사용 가능한 (Reusable) 형태로 공식화해줘.
구체적으로는 다음과 같습니다:
- 앞서 수행한 진단용 쿼리 (Diagnostic query)들을
sql-audit/폴더에 저장할 것 — 목적에 따라 파일별로 분리할 것 - 처음부터 다시 토론할 필요 없이 즉시 사용할 수 있는 프롬프트 템플릿 (Prompt template) 하나를 만들 것
그러자 Kiro는 즉시 실행에 옮겼습니다. 결과는 다음과 같습니다:
sql-audit/ 폴더 — 모든 진단용 쿼리가 조사 순서에 따라 파일별로 깔끔하게 정리되었습니다. 앞으로 I/O 문제가 발생하더라도 "어디서부터 시작해야 하지?"라고 고민할 필요 없이, 폴더를 열어 하나씩 실행하기만 하면 됩니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기