Microsoft.Data.SqlClient는 서버 및 공급자 실패를 SqlException를 통해 보고합니다. 데이터베이스 연산은 여러 오류를 반환할 수 있으므로, 결합된 Errors에만 의존하지 말고 예외의 Message 집합을 검사하세요.
데이터베이스 엔진에서 오는 경고와 정보 메시지는 InfoMessage를 통해 전달됩니다. 연결을 열거나 메시지를 생성할 수 있는 명령을 실행하기 전에 구독하세요.
데이터베이스 장애를 다음과 같이 처리하세요:
- 작업 컨텍스트를 추가하거나 복구 작업을 선택할 수 있는 경계에서
SqlException를 처리하세요. -
SqlError컬렉션의 모든Errors을(를) 점검하세요. - 자격 증명이나 민감한 값을 기록하지 않고 구조화된 진단 필드를 기록하세요.
- 연결 관련 장애가 발생한 후 연결 상태를 확인하세요.
- 오류가 일시적이고 작업이 안전하게 반복될 때만 재시도하세요.
이 글의 예시들은 connection라는 이름의 열린 SqlConnection이 AdventureWorks 샘플 데이터베이스를 대상으로 한다고 가정합니다. C# 예제는 using Microsoft.Data.SqlClient;를 요구합니다.
정보 메시지 수신
이벤트는 InfoMessage 심각도가 낮은 서버 메시지를 받으며, PRINT의 출력을 포함합니다. 하나의 이벤트에는 여러 SqlError 객체가 포함될 수 있습니다. 메시지를 생성할 수 있는 명령을 실행하기 전에 구독합니다:
connection.InfoMessage += (_, eventArgs) =>
{
foreach (SqlError message in eventArgs.Errors)
{
Console.WriteLine(
$"Info {message.Number}, class {message.Class}: {message.Message}");
}
};
using var command = new SqlCommand(
"PRINT N'Connected and ready.';",
connection);
command.ExecuteNonQuery();
발췌문은 다음과 같다:
Info 0, class 0: Connected and ready.
Catch SqlException
데이터베이스 작업 중 처리하거나 보강할 수 있는 위치에 SqlException를 두세요. 실패한 데이터베이스 작업을 성공적인 결과로 바꾸기 위해 광범위한 catch문을 사용하지 마세요.
데이터베이스 작업은 여러 오류를 반환할 수 있습니다.
Errors의 각 SqlError에는 구조화된 필드가 포함됩니다:
| Property | Meaning |
|---|---|
Number |
데이터베이스 엔진 또는 제공자 오류 번호. |
Class |
심각도 |
State |
동일한 오류 번호에 대해 위치를 구분할 수 있는 서버 정의 상태. |
LineNumber |
보고된 줄 번호. |
Procedure |
제공된 경우 프로시저 또는 트리거 이름 |
Server |
오류와 함께 보고된 서버 이름. |
Message |
사람이 이해할 수 있는 오류 텍스트. |
SqlException의 편의 속성은 Procedure, Server, Class, LineNumber, State 및 Number을 포함하여 첫 번째 오류만 반영합니다. 다음 발췌문은 메시지를 출력한 후 같은 배치에서 오류를 발생시킵니다.
exception.Number를 읽고 그런 다음 Errors를 반복하여 모든 항목을 보고합니다:
using (var failing = new SqlCommand("""
PRINT N'Starting controlled operation.';
RAISERROR (N'The controlled operation failed.', 16, 1);
""", connection))
{
try
{
await failing.ExecuteNonQueryAsync();
}
catch (SqlException exception)
{
Console.WriteLine($"First error number: {exception.Number}");
foreach (SqlError error in exception.Errors)
{
Console.WriteLine(
$"Number={error.Number}; Class={error.Class}; " +
$"State={error.State}; Message={error.Message}");
}
}
}
발췌문은 다음과 같다:
First error number: 50000
Number=50000; Class=16; State=1; Message=The controlled operation failed.
Number=0; Class=0; State=1; Message=Starting controlled operation.
PRINT 출력은 InfoMessage를 발생시키지 않습니다. 배치가 SqlException를 발생시키는 오류를 반환하면, 제공자는 SqlException.Errors의 끝에 해당 배치의 정보 메시지를 덧붙입니다.
이 발췌문은 제어된 오류를 포착하여 해당 필드를 보여줍니다. 애플리케이션 코드에서는 캐치 블록이 실제 복구 액션을 선택하지 않는 한 throw;로 다시 throw합니다. 문서화된 오류 수치와 운영 맥락을 바탕으로 의사결정을 내리세요. 현지화된 메시지 텍스트를 구문 분석하지 마세요.
연결 상태 확인
데이터베이스 엔진은 각 메시지에 심각도 클래스를 할당합니다. 하위 계층은 정보 제공용입니다. 오류로 처리된 클래스는 SqlException를 생성합니다. 심각한 오류는 연결이 닫히거나 손상된 상태로 남을 수 있습니다.
오류가 발생한 후에는 연결을 재사용하기 전에 State 확인하세요:
using var command = new SqlCommand(
"RAISERROR (N'Controlled failure.', 16, 1);",
connection);
try
{
command.ExecuteNonQuery();
}
catch (SqlException)
{
Console.WriteLine($"Connection state: {connection.State}");
}
예외가 잡혔다고 해서 연결이 계속 사용 가능하다고 가정하지 마세요. 심각도 16 오류는 보통 열린 상태로 남지만, 연결을 끊는 오류가 발생하면 닫힐 수 있습니다. 닫혔거나 끊긴 연결을 폐기하고 풀에서 다른 연결을 가져오세요. 재시도 정책을 심각도만으로 인코딩하지 마세요. 이 작업은 또한 안전하게 반복할 수 있어야 합니다.
실패한 배치 명령 식별
프로세스 SqlBatch가 실패할 때, SqlException.BatchCommand는 관련된 SqlBatchCommand를 참조할 수 있습니다. 속성은 제공자가 실패를 한 배치 명령으로 귀속시킬 수 없는 경우 null입니다. 다음 발췌문은 두 개의 배치 명령어를 실행합니다. 두 번째 것은 실패합니다:
using (var batch = new SqlBatch(connection))
{
batch.BatchCommands.Add(new SqlBatchCommand("SELECT 1;"));
batch.BatchCommands.Add(new SqlBatchCommand("SELECT 1 / 0;"));
try
{
await batch.ExecuteNonQueryAsync();
}
catch (SqlException exception)
{
Console.WriteLine($"Error {exception.Number}: {exception.Message}");
if (exception.BatchCommand is SqlBatchCommand failedCommand)
{
Console.WriteLine($"Failed command: {failedCommand.CommandText}");
}
}
}
발췌문은 다음과 같다:
Error 8134: Divide by zero error encountered.
Failed command: SELECT 1 / 0;
명령어 텍스트는 매개변수화가 제대로 되지 않은 애플리케이션에서는 민감한 리터럴을 포함할 수 있습니다. 프로덕션 로그에서 안정적인 애플리케이션 작업 이름을 선호하고, 기본적으로 매개변수 값을 기록하지 마세요.
사용자 오류를 InfoMessage로 라우팅합니다
FireInfoMessageEventOnUserErrors 기본값은 false입니다. 이를 true로 설정하면 심각도 16 이하의 오류는 InfoMessage를 발생시키는 대신 SqlException 핸들러로 전달됩니다:
connection.InfoMessage += (_, eventArgs) =>
{
foreach (SqlError message in eventArgs.Errors)
{
Console.WriteLine(
$"Info {message.Number}, class {message.Class}: {message.Message}");
}
};
connection.FireInfoMessageEventOnUserErrors = true;
using var command = new SqlCommand(
"RAISERROR (N'Routed to InfoMessage.', 16, 1);",
connection);
command.ExecuteNonQuery();
예외가 던져지지 않습니다. 발췌문은 다음과 같다:
Info 50000, class 16: Routed to InfoMessage.
이 옵션은 애플리케이션이 의도적인 이벤트 기반 오류 모델을 가질 때만 사용하세요. 호출 코드가 더 이상 예외를 인식하지 못하기 때문입니다. 심각도 17 이상인 오류도 여전히 SqlException발생합니다. 개별 명령마다 해당 속성을 켰다 껐다 하지 말고 연결 설정의 일부로 한 번 설정하세요.
로그 진단 컨텍스트
데이터를 노출하지 않으면서도 실패 경위를 연관시킬 수 있을 만큼 충분한 구조화된 맥락을 포착하세요. 다음 발췌문은 애플리케이션 작업 이름, ClientConnectionId, 그리고 모든 SqlError의 필드를 기록합니다.
ClientConnectionId 클라이언트 장애와 서버 및 네트워크 진단을 연계하는 데 도움을 줍니다. 그것은 자격증이 아닙니다.
const string operation = "LoadOrders";
using (var load = new SqlCommand("SELECT 1 / 0;", connection))
{
try
{
await load.ExecuteScalarAsync();
}
catch (SqlException exception)
{
Console.WriteLine(
$"Operation={operation}; " +
$"ClientConnectionId={exception.ClientConnectionId}");
foreach (SqlError error in exception.Errors)
{
Console.WriteLine(
$"Number={error.Number}; Class={error.Class}; " +
$"State={error.State}; Procedure={error.Procedure}; " +
$"Line={error.LineNumber}");
}
}
}
ClientConnectionId 연결마다 값이 다릅니다. 발췌문은 다음과 유사한 출력을 표시합니다:
Operation=LoadOrders; ClientConnectionId=<connection ID>
Number=8134; Class=16; State=1; Procedure=; Line=1
트랜잭션이 활성화되었는지, 그리고 BatchCommand을 사용할 수 있으면 어떤 배치 명령어가 실패했는지도 기록하세요. 조직 정책이 허용하는 경우에만 정제된 서버 및 데이터베이스 식별자를 포함하세요. 연결 문자열, 접근 토큰, 비밀번호, 민감한 매개변수 값을 기록하지 마세요. 진단 카운터, 이벤트 소스 추적, 그리고 SqlClient 문제 해결 가이드를 활용해 더 깊은 조사를 하세요.
관련 콘텐츠
- 명령 실행 워크플로우로 돌아가세요.
- 그룹 내 실패한 명령을 식별하려면 SqlBatch로 여러 명령을 실행하세요.
- 제한된 재시도를 추가하려면 설정 가능한 재시도 로직을 사용하세요.
- 예외 필드만으로는 부족할 때는 SqlClient 문제 해결 가이드를 참고하세요.