Microsoft.Data.SqlClient를 사용하여 데이터 원본에 연결

SqlConnectionSQL Server, Azure SQL Database, Azure SQL Managed Instance 또는 Microsoft Fabric 내 SQL 데이터베이스에 하나의 논리적 연결을 나타냅니다. 객체를 열면, 사용 가능한 경우 연결 풀에서 물리적 연결을 가져옵니다. 닫거나 삭제하면 해당 물리적 연결이 풀에 반환됩니다.

작업 단위에는 수명이 짧은 SqlConnection 객체를 사용하세요. 애플리케이션에 대해 하나의 글로벌 연결을 열어두지 마세요.

Prerequisites

  • .NET 10 SDK 또는 이후 지원되는 .NET SDK입니다. SDK는 Windows, Linux, macOS에서 실행됩니다. 또한 각 샘플의 .NET 프레임워크 버전을 빌드하는데, 이 버전은 Windows에서만 실행되고 .NET Framework 4.8.1이 필요합니다.
  • SQL Server, Azure SQL Database, Azure SQL Managed Instance, 또는 Microsoft Fabric의 SQL 데이터베이스에서 연결할 수 있는 데이터베이스입니다.

프로젝트를 만듭니다.

Microsoft.Data.SqlClient 7.0 및 이후 버전은 드라이버가 제공하는 Microsoft Entra ID 인증 모드(예: Microsoft.Data.SqlClient.Extensions.Azure)에 대해 버전이 일치하는 Active Directory Default 패키지가 필요합니다. 만약 앱이 SQL 인증이나 Windows 통합 인증만 사용한다면 생략할 수 있습니다.

빈 폴더에서 다음 명령어를 실행하세요:

dotnet new console --framework net10.0 --name SqlClientConnect
cd SqlClientConnect
dotnet add package Microsoft.Data.SqlClient --version 7.1.1
dotnet add package Microsoft.Data.SqlClient.Extensions.Azure --version 7.1.1

이 글의 샘플들은 SQL_CONNECTION_STRING 환경 변수에서 연결 문자열을 읽습니다. 샘플을 돌리기 전에 설정하세요. 연결 문자열 형식에 대해서는 Connection strings를 참조하세요.

트랜잭션 샘플을 제외한 모든 샘플은 완전한 프로그램입니다. 하나를 실행하려면 Program.cs의 내용을 그것으로 대체한 후 dotnet run을 실행하세요. 거래 샘플은 사용자 고유 메서드의 일부분입니다. .NET 탭은 최상위 문, 암시적 using 지시문, 그리고 await using을(를) 사용합니다. .NET Framework 탭은 C# 7.3을 사용하는데, 여기에는 await using가 없기 때문에 대신 using 문을 사용합니다.

연결을 열어

비동기 코드에서 Open를 호출하거나 동기 코드에서 OpenAsync를 호출하세요. 코드가 범위를 벗어날 때 예외가 발생하더라도 연결이 해제되도록 연결을 await using 또는 using로 선언하세요.

Windows 통합 인증에서는 연결이 프로세스의 보안 컨텍스트를 사용합니다. 웹 애플리케이션이나 Windows 서비스의 경우, 그건 로그인한 사용자가 아니라 애플리케이션 풀이나 서비스 계정입니다. 자세한 내용은 인증 패턴 선택을 참조하세요.

이 완전한 프로그램은 연결을 열고, 각 상태 변화를 보고하며, 두 개의 서버 속성을 읽습니다:

using Microsoft.Data.SqlClient;

string connectionString =
    Environment.GetEnvironmentVariable("SQL_CONNECTION_STRING")
    ?? throw new InvalidOperationException(
        "Set the SQL_CONNECTION_STRING environment variable.");

await using var connection = new SqlConnection(connectionString);
connection.StateChange += (_, e) =>
    Console.WriteLine($"State: {e.OriginalState} to {e.CurrentState}");

await connection.OpenAsync();

await using var command = new SqlCommand(
    """
    SELECT CAST(SERVERPROPERTY('Edition') AS nvarchar(128)),
           CAST(SERVERPROPERTY('EngineEdition') AS int);
    """,
    connection);

await using (SqlDataReader reader = await command.ExecuteReaderAsync())
{
    await reader.ReadAsync();
    Console.WriteLine($"Edition: {reader.GetString(0)}");
    Console.WriteLine($"EngineEdition: {reader.GetInt32(1)}");
}

SQL Server 2025 엔터프라이즈 에디션에 대한 결과는 다음과 같습니다:

State: Closed to Open
Edition: Enterprise Edition: Core-based Licensing (64-bit)
EngineEdition: 3
State: Open to Closed

마지막 줄은 프로그램 종료 시 연결이 종료될 때 나타납니다.

연결 폐쇄 및 폐기

연결을 끝내면 항상 연결을 종료해서 물리적 연결이 풀로 돌아오도록 하세요. await using 및 using 문은 코드가 블록을 벗어날 때, 처리되지 않은 예외가 throw되는 경우를 포함하여 연결을 종료합니다. Close 또는 Dispose로 직접 전화할 수도 있습니다. Close 및 Dispose은 물리적 연결에 같은 영향을 미칩니다.

다음 규칙을 따릅니다.

  • 리더와 명령은 이를 소유한 연결보다 먼저 해제하세요.
  • 연결을 풀로 반환하기 위해 가비지 컬렉션에 의존하지 마세요. 범위를 벗어나면서 닫히지 않은 연결은 제때 반환되지 않으며, 풀에 연결이 부족해질 수 있습니다.
  • 클래스 종료자에서 연결, 리더 또는 다른 관리되는 객체에 대해 Dispose 또는 Close를 호출하지 마세요. 파이널라이저는 자신의 클래스가 직접 소유한 관리되지 않은 자원만 해제해야 합니다. 자세한 내용은 가비지 컬렉션을 참조하세요.
  • Close 후 동일한 객체에서 Open를 다시 호출할 수 있습니다. Dispose 후에 새 객체를 생성합니다.

풀링이 활성화된 상태에서는 연결을 종료하면 네트워크 연결을 종료하지 않고 풀로 반환됩니다. 연결이 재사용될 때 풀은 세션 상태를 초기화합니다. 자세한 내용은 SQL Server 연결 풀링을 참조하세요.

비동기 API 사용하기

웹 서버, 서비스, 사용자 인터페이스 및 워커에서 네트워크 바운드 데이터베이스 작업에 비동기 호출을 사용하세요:

  • OpenAsync(cancellationToken)
  • ExecuteNonQueryAsync(cancellationToken)
  • ExecuteReaderAsync(cancellationToken)
  • ExecuteScalarAsync(cancellationToken)
  • ReadAsync(cancellationToken)

현재 비동기 작업이 완료되기 전에는 연결, 명령어, 리더에 대해 다른 작업을 시작하지 마세요.

취소 및 타임아웃 적용

모든 비동기 데이터베이스 호출에 호출자의 CancellationToken를 전달하세요. 취소는 운전자에게 대기 중인 작업을 중단하라고 요청하지만, 즉시 완료된다는 보장은 없습니다. 계속 제한된 연결과 명령 타임아웃을 사용하세요.

이 제어장치들은 별도의 범위가 있습니다:

제어 Scope
Connect Timeout 연결 설정 또는 풀 연결 대기
SqlCommand.CommandTimeout 하나의 명령 실행
CancellationToken 발신자 요청 비동기 작업 취소

타임아웃이나 취소가 서버가 작업을 롤백했다는 증거는 아닙니다. 여러 변경 사항이 하나의 단위로 커밋되거나 롤백해야 할 트랜잭션을 사용하고, 연산의 멻등성과 트랜잭션 결과에 따라 재시도 결정을 내리세요.

연결 상태 추적

State 속성은 ConnectionState 열거형의 스냅샷을 반환합니다.

주 Meaning
Closed 논리적 연결고리가 열려 있지 않아요.
Connecting 공개 작전이 진행 중입니다.
Open 논리적 연결고리는 열려 있습니다.

StateChange 이벤트가 상태가 변할 때 발생합니다. 이 StateChangeEventArgs 인수는 두 가지 속성을 가진다:

  • OriginalState 는 변화 이전의 상태입니다.
  • CurrentState 변화 후의 상태입니다.

드라이버는 Closed 예제에서 볼 수 있듯이 에서 Open로의 전환과 Open에서 Closed로의 전환을 보고합니다. Executing, Fetching, 또는 Broken를 애플리케이션 수명 주기 단계로서 관찰하는 것에 의존하지 마세요.

모든 명령 전에 체력 검사로 사용하지 State 마세요. 네트워크는 어떤 점검 후에도 실패할 수 있습니다. 연산을 실행하고 결과로 나오는 예외를 처리하세요. StateChange 진단용으로 사용하고, 동시 작업을 조정하는 데는 사용하지 마세요.

정보 메시지 수신

이 InfoMessage 이벤트는 서버가 오류를 발생시키지 않고 보내는 메시지를 전달합니다. 이 메시지에는 RAISERROR 출력, 심각도 10 이하의 PRINT 메시지, 그리고 서버 경고가 포함됩니다. 심각도 11 이상인 오류는 대신 A SqlException 를 올립니다. 심각도 수준에 대해서는 데이터베이스 엔진 오류 심각도를 참조하세요.

이벤트 핸들러는 SqlInfoMessageEventArgs 객체를 받습니다. 그 Errors 속성은 SqlError 객체들의 집합을 담고 있습니다. 각 항목에는 메시지 번호, 텍스트, 심각도 등급, 절차, 줄 번호가 포함되어 있습니다.

using Microsoft.Data.SqlClient;

string connectionString =
    Environment.GetEnvironmentVariable("SQL_CONNECTION_STRING")
    ?? throw new InvalidOperationException(
        "Set the SQL_CONNECTION_STRING environment variable.");

await using var connection = new SqlConnection(connectionString);
connection.InfoMessage += (_, e) =>
{
    foreach (SqlError error in e.Errors)
    {
        Console.WriteLine(
            $"Message {error.Number}, severity {error.Class}: {error.Message}");
    }
};

await connection.OpenAsync();

await using var command = new SqlCommand(
    """
    PRINT 'Starting the batch.';
    RAISERROR('Processed %d rows.', 10, 1, 42);
    """,
    connection);

await command.ExecuteNonQueryAsync();

결과는 다음과 같습니다.

Message 0, severity 0: Starting the batch.
Message 50000, severity 0: Processed 42 rows.

서버는 심각도 10을 클래스 0으로 RAISERROR 보고하므로, SqlError.Class 두 메시지 모두에 대해 0를 표시합니다.

사용자 오류를 정보 메시지로 처리하세요

기본적으로 드라이버는 심각도 11 이상의 오류를 보고할 때 SqlException를 throw합니다. 그 예외가 서버가 배치를 멈췄다는 의미는 아닙니다. RAISERROR를 포함해 심각도가 16인 많은 오류는 서버가 나머지 문을 실행하도록 하므로, 코드에서 예외를 catch하더라도 이후 문이 데이터를 변경할 수 있습니다. 이 오류들을 예외 대신 메시지로 받으려면 true를 FireInfoMessageEventOnUserErrors로 설정하세요. 그러면 드라이버는 throw하지 않고 심각도 16 이하의 오류를 InfoMessage 이벤트로 보냅니다. 이 속성은 드라이버가 오류를 보고하는 방식을 바꿉니다. 서버가 실행하는 문은 변하지 않습니다.

using Microsoft.Data.SqlClient;

string connectionString =
    Environment.GetEnvironmentVariable("SQL_CONNECTION_STRING")
    ?? throw new InvalidOperationException(
        "Set the SQL_CONNECTION_STRING environment variable.");

await using var connection = new SqlConnection(connectionString)
{
    FireInfoMessageEventOnUserErrors = true,
};
connection.InfoMessage += (_, e) =>
{
    foreach (SqlError error in e.Errors)
    {
        Console.WriteLine(
            $"Message {error.Number}, severity {error.Class}: {error.Message}");
    }
};

await connection.OpenAsync();

await using var command = new SqlCommand(
    """
    RAISERROR('Row 7 failed validation.', 16, 1);
    SELECT 'The batch continued.';
    """,
    connection);

await using (SqlDataReader reader = await command.ExecuteReaderAsync())
{
    do
    {
        while (await reader.ReadAsync())
        {
            Console.WriteLine(reader.GetString(0));
        }
    }
    while (await reader.NextResultAsync());
}

결과는 다음과 같습니다.

Message 50000, severity 16: Row 7 failed validation.
The batch continued.

심각도가 17 이상인 오류는 이 설정과 상관없이 여전히 SqlException를 던집니다. 이 속성을 활성화하면 핸들러가 실패를 감지할 책임이 있습니다. 모든 오류에 예외를 기대하는 코드는 예외를 보지 못합니다.

애플리케이션 ID 보고

Microsoft.Data.SqlClient 7.1.0부터 프레임워크, 도구 또는 기타 미들웨어가 어떤 애플리케이션이 연결을 열었는지 보고할 수 있습니다. 연결을 열기 전에 RegisteredApplication을/를 RegisteredApplication 값으로 설정하세요. 드라이버는 로그인 시 서버에 값을 한 번 보냅니다.

다음 규칙을 따릅니다.

  • 속성을 통해 값을 설정하세요. 이에 해당하는 연결 문자열 키워드는 없습니다.
  • 연결을 열기 전에 값을 설정하세요. 연결이 열리는 중이거나 이미 열려 있을 때 설정하면 예외InvalidOperationException가 발생합니다.
  • 값을 텔레메트리로 간주하세요. 클라이언트가 값을 제공하고 어떤 식별자든 전달할 수 있으므로 서버는 이를 인증하지 않습니다.
  • 풀링 연결은 원래 ID를 보고할 것으로 예상됩니다. 가치는 풀 키의 일부가 아닙니다. 풀에서 제공된 연결은 물리적 연결이 생성되도록 한 논리적 연결의 ID를 보고합니다.

Caution

RegisteredApplication를 권한 부여나 기타 보안 결정에 절대 사용하지 마세요. 어떤 클라이언트든 어떤 식별자든 전달할 수 있습니다.

식별자를 선택하세요

기본 값은 Unknown이며, 이는 0입니다. 열거는 ushort 유형을 기반으로 하므로, 식별자는 0부터 0xFFFF까지입니다.

범위 사용
0x0001 ~ 0x7FFF Microsoft가 정의한 애플리케이션. 이 범위는 이름이 지정된 열거형 멤버에 사용됩니다.
0x8000 ~ 0xBFFF 소규모 용도로 지정되어 있습니다.
0xC000 ~ 0xFFFF 일반 사용자 및 개발자용.

명명된 구성원은 SemanticKernel, ManagementStudio, AspNetCoreDistributedSqlServerCache, SqlManagementObjects, DataTierApplicationFramework, OrleansAdoNet, SqlPackage, SqlToolsService, EntityFramework, AzureFunctionsSqlExtension, DurableTaskSqlServer, DataApiBuilder, 및 EntityFrameworkCore입니다. 이름 있는 멤버가 없는 식별자를 보고하려면 숫자를 RegisteredApplication로 캐스팅하세요.

열거가 RegisteredApplication를 사용하기 때문에 형식과 속성은 CLS를 준수하지 않습니다. C# 코드는 그것들을 직접 사용할 수 있습니다.

식별자를 설정하세요

이 프로그램은 개발자 범위에서 식별자를 보고하고, 연결이 열린 후 코드가 값을 변경할 때 어떤 일이 일어나는지 보여줍니다:

using Microsoft.Data.SqlClient;

string connectionString =
    Environment.GetEnvironmentVariable("SQL_CONNECTION_STRING")
    ?? throw new InvalidOperationException(
        "Set the SQL_CONNECTION_STRING environment variable.");

await using var connection = new SqlConnection(connectionString)
{
    RegisteredApplication = (RegisteredApplication)0xC001,
};

await connection.OpenAsync();
Console.WriteLine(
    $"Reported identifier: 0x{(ushort)connection.RegisteredApplication:X4}");

try
{
    connection.RegisteredApplication = RegisteredApplication.Unknown;
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}

결과는 다음과 같습니다.

Reported identifier: 0xC001
InvalidOperationException: Not allowed to change the 'RegisteredApplication' property. The connection's current state is open.

풀링 효과 이해하기

드라이버는 새로운 물리적 연결을 생성할 때만 식별자를 보고합니다. 풀링이 활성화된 상태에서:

  • 풀링된 물리적 연결을 재사용하는 연결은 자체 ID가 아니라 물리적 연결을 만든 연결의 ID를 반환합니다.
  • 풀이 Unknown보고서 Min Pool Size를 충족하기 위해 백그라운드에서 여는 물리적 연결입니다.

만약 하나의 연결 문자열이 여러 ID에 사용되는 경우, 보고된 값을 근사값으로 처리합니다. 정확한 귀속이 필요할 때 해당 연결 문자열에 대한 풀링을 비활성화하고, 먼저 추가 연결 비용을 측정하세요.

SqlConnection.Clone 값을 RegisteredApplication 새 연결에 복사합니다.

동시에 연결을 공유하지 마세요

SqlConnection SqlCommand, , SqlDataReader, , SqlTransaction 그리고 여러 스레드의 동시 사용을 지원하지 않습니다. 각 동시 작업을 별도의 연결을 부여하고, 연결 풀링이 물리적 연결을 재사용할 수 있도록 하세요.

다중 활성 결과 집합(MARS)은 지원되는 시나리오에서 하나의 연결에 여러 활성 배치를 허용합니다. SqlClient 객체를 스레드 안전하게 만들지 않고, 세션과 트랜잭션 규칙을 추가합니다. 특정 작업이 필요하지 않으면 비활성화 상태로 두세요.

오픈 제네릭 SqlConnection를 의존성 주입에서 싱글톤으로 등록하지 마세요. 연결 문자열, 변경 불가능한 옵션 객체, 또는 새로운 연결을 생성하는 공장을 등록하세요.

트랜잭션을 신중하게 사용하세요

로컬 트랜잭션은 그 연결에 속합니다. 트랜잭션의 모든 명령어는 해당 연결을 사용하고 속성을 Transaction 설정해야 합니다.

await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(cancellationToken);

await using SqlTransaction transaction =
    (SqlTransaction)await connection.BeginTransactionAsync(cancellationToken);

using var command = new SqlCommand(sql, connection, transaction);
command.Parameters.Add(
    new SqlParameter("@value", SqlDbType.Int) { Value = value });
await command.ExecuteNonQueryAsync(cancellationToken);

await transaction.CommitAsync(cancellationToken);

커밋 전에 연산이 실패하면, 트랜잭션을 폐기하면 롤백됩니다. 거래는 짧게 유지하세요. 데이터베이스 트랜잭션이 잠금을 유지하고 있을 때 네트워크 호출, 사용자 상호작용, 관련 없는 계산을 하지 마세요.

System.Transactions.Transaction.Current이(가) 활성 상태이면 OpenAsync 및 Open가 기본적으로 자동 등록됩니다. 연산이 반드시 주변 거래 외부에 있어야 할 때만 설정됩니다 Enlist=false .

하나의 논리적 연결을 측정합니다

하나의 StatisticsEnabled 객체에 대한 드라이버 통계를 수집하려면 SqlConnection을(를) true로 설정하세요:

using System.Collections;
using Microsoft.Data.SqlClient;

string connectionString =
    Environment.GetEnvironmentVariable("SQL_CONNECTION_STRING")
    ?? throw new InvalidOperationException(
        "Set the SQL_CONNECTION_STRING environment variable.");

await using var connection = new SqlConnection(connectionString)
{
    StatisticsEnabled = true,
};

await connection.OpenAsync();
connection.ResetStatistics();

await using var command = new SqlCommand("SELECT name FROM sys.databases;", connection);
await using (SqlDataReader reader = await command.ExecuteReaderAsync())
{
    while (await reader.ReadAsync())
    {
    }
}

IDictionary statistics = connection.RetrieveStatistics();
Console.WriteLine($"ServerRoundtrips: {statistics["ServerRoundtrips"]}");
Console.WriteLine($"SelectRows: {statistics["SelectRows"]}");
Console.WriteLine($"BytesReceived > 0: {(long)statistics["BytesReceived"]! > 0}");

출력은 왕복 한 번을 보여줍니다. 행 수는 서버에 있는 데이터베이스 수에 따라 달라집니다:

ServerRoundtrips: 1
SelectRows: 14
BytesReceived > 0: True

RetrieveStatistics 스냅샷을 반환합니다. ResetStatistics 새로운 측정 경계가 시작됩니다. StatisticsEnabled을 false로 설정하여 수집을 중지하세요. 지금까지 수집된 값들은 여전히 이용 가능합니다. 통계는 연결 객체별로 할당되어 오버헤드가 발생하므로, 모든 프로덕션 요청이 아닌 타겟 진단에 이를 활성화하세요.

연결 실패 처리

실패를 기록하거나 변환하거나 재시도할 수 있는 경계에서 SqlException를 잡습니다. 레코드:

  • Number
  • State
  • Class
  • ClientConnectionId
  • 운영 이름과 설정된 서버 및 데이터베이스 식별자

연결 문자열, password, client secret, access token을 기록하지 마세요.

끊어진 연결을 처리하세요. 풀은 무효 물리 연결을 감지하면 제거합니다. 자격 증명, 토큰, 인증서, DNS 대상 또는 서버가 변경되었다면, 재시도하기 전에 설정을 수정하세요.

일시적 오류에만 제한된 재시도 로직을 사용하세요. 초기 열린 재시도, 유휴 연결 복구, 명령 재시도는 서로 다른 메커니즘입니다. 자세한 내용은 설정 가능한 재시도 로직 과 연결 옵션 내 연결 복구 옵션을 참조하세요.