C#, IAsyncEnumerable yield return
데이터베이스는 기본적으로 ‘select’와 같이 조회했을 때 그 결과가 운반 단위(array)에 도달하면 호출한 쪽으로 그 결과를 바로 리턴해준다. 예를 들어 어떤 테이블에 row 개수가 1000만 개가 있든 10만 개가 있든지 상관없이 select * from tablename을 실행하면 바로 화면에 나와야 정상이다.
이 부분을 간과하고 조회하는 프로그램 작성한다고 하면 조회 결과를 어떠한 DataSet에 넣고 그 결과가 완료될 때 비로서 Loop을 사용하여 화면에 표현하기 때문에 row 개수에 영향을 받는다.
물론, 페이징 처리를 한다고 하지만 쿼리문 자체에 ‘order by’, ‘group by’ 등을 사용하면 Sort가 발생하고 이 모든 정렬이 끝나야 비로소 그 결과를 출력하기 시작한다. 즉, ‘부분범위처리’와 ‘전체범위처리’의 차이다.
부분범위처리’라 함은 데이터베이스가 해당 내용을 다 읽지 않고도 바로 하나의 row를 바로 출력할 수 있는 상태를 말한다. 옵티마이저가 봤을 때 끝까지 읽어서 분석할 필요가 없다고 판단하기 때문이다. 그래서 정렬하되 전체범위처리(끝까지 다 읽고나서야 결과를 뽑아낼수 있는 상태)가 되지 않도록 쿼리문을 잘 작성해야 한다.
여기에는 인덱스 전략도 포함되며 특히, ‘where’절에 column을 가공하면(예, where left(xxxx) = '1234') 작성한 ‘left(xxxx)’라는 column은 존재하지 않기 때문에 옵티마이저는 전체를 다 읽고 해당 column을 ‘left(xxxx)’로 모두 2차 가공한 다음에 조회하고 그 결과를 출력한다. 즉, 전체범위처리를 하는 것이다. 요지는 ‘어떻게 부분범위처리가 되도록 유도하느냐’이다.
부분범위처리가 되었다고 가정하고 조회 결과를 클라이언트의 프로그램에서 조회한 결과를 DataSet에 모두 넣고 그 다음에 화면에 Loop를 사용하여 표현하는 방법으로 처리하는 것이 아닌 비동기적으로 스트리밍하여 그 결과가 들어오는 즉시 화면에 출력하도록 IAsyncEnumerable, yield return을 사용하는 예제를 작성해 보았다. SQL Server의 특정 테이블에 50만 개 정도의 row가 저장된 테스트 데이터를 사용한다.
nuget 패키지 추가
1
2
3
4
5
6
7
8
9
10
11
12
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.35"/>
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.0-preview3.24332.3"/>
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
</ItemGroup>
</Project>
데이터 모델 정의
1
2
3
4
5
6
// 데이터베이스 table 중에서 특정 컬럼만 조회
public class MyData
{
public string ZipCode { get; init; } = string.Empty;
public string SiDo { get; init; } = string.Empty;
}