출시·고도화 중
C# 안내서 · 5/6
C#은 오류를 예외(exception)로 알립니다. 예외가 던져지면 현재 실행 흐름이 멈추고, 호출 스택을 거슬러 올라가며 그 예외를 처리할 catch 블록을 찾습니다. 이 장에서는 예외를 잡고 던지는 방법, 자원을 안전하게 정리하는 using, 사용자 정의 예외, 그리고 xUnit과 MSTest로 단위 테스트를 작성하고 dotnet test로 실행하는 방법을 다룹니다.
try 블록에서 예외가 발생하면 타입이 맞는 첫 번째 catch 블록이 실행됩니다. 구체적인 예외를 먼저, 일반적인 예외를 나중에 적어야 합니다. finally 블록은 예외가 났든 안 났든 항상 실행되므로 정리 작업에 씁니다.
try
{
var text = File.ReadAllText("config.json");
var port = int.Parse(text.Trim());
Console.WriteLine($"포트: {port}");
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"설정 파일이 없습니다: {ex.FileName}");
}
catch (FormatException)
{
Console.WriteLine("포트 번호 형식이 잘못되었습니다.");
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
Console.WriteLine($"파일을 읽지 못했습니다: {ex.Message}");
}
finally
{
Console.WriteLine("설정 읽기를 마쳤습니다.");
}when 절은 예외 필터로, 조건이 참일 때만 그 catch가 예외를 받습니다. 예외를 잡았다가 다시 던질 때는 throw;라고만 적어야 원래 스택 추적이 유지됩니다. throw ex;라고 쓰면 스택 추적이 그 지점에서 다시 시작되어 원인을 찾기 어려워집니다.
예외는 정말 예외적인 상황에 씁니다. 사용자 입력처럼 실패가 흔한 경우에는 int.TryParse(input, out var value)처럼 성공 여부를 bool로 돌려주는 Try 패턴이 더 알맞고 빠릅니다.
메서드는 잘못된 인수를 받으면 일찍 실패하는 것이 좋습니다. .NET에는 이를 위한 도우미 메서드가 있습니다. 업무 규칙 위반처럼 의미 있는 오류는 Exception을 상속한 사용자 정의 예외로 표현하면 호출하는 쪽에서 골라 잡기 쉽습니다.
public class InsufficientFundsException : Exception
{
public InsufficientFundsException(decimal balance, decimal requested)
: base($"잔액 {balance:N0}원으로 {requested:N0}원을 출금할 수 없습니다.")
{
Balance = balance;
Requested = requested;
}
public decimal Balance { get; }
public decimal Requested { get; }
}
public class Wallet(decimal balance)
{
public decimal Balance { get; private set; } = balance;
public void Withdraw(decimal amount, string reason)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);
ArgumentException.ThrowIfNullOrWhiteSpace(reason);
if (amount > Balance)
throw new InsufficientFundsException(Balance, amount);
Balance -= amount;
}
}예외 클래스 이름은 Exception으로 끝내는 것이 관례입니다. Wallet(decimal balance)처럼 클래스 이름 옆에 매개변수를 적는 문법은 기본 생성자(primary constructor)로, 매개변수를 클래스 안에서 바로 쓸 수 있습니다.
파일, 네트워크 연결, 데이터베이스 연결처럼 운영 체제 자원을 쥐고 있는 객체는 IDisposable을 구현하며, 다 쓴 뒤 Dispose를 호출해 자원을 돌려줘야 합니다. using을 쓰면 예외가 나더라도 Dispose가 자동으로 호출됩니다.
// using 문: 블록이 끝날 때 Dispose 됩니다(파일이 닫힙니다)
using (var writer = new StreamWriter("log.txt", append: true))
{
writer.WriteLine($"{DateTime.Now:O} 시작");
}
// using 선언: 변수가 속한 범위가 끝날 때 Dispose 됩니다
using var reader = new StreamReader("log.txt");
Console.WriteLine(reader.ReadLine());
// 직접 만든 타입도 IDisposable 을 구현하면 using 으로 쓸 수 있습니다
using (new ElapsedTimer())
{
Thread.Sleep(100);
}
sealed class ElapsedTimer : IDisposable
{
private readonly System.Diagnostics.Stopwatch _watch = System.Diagnostics.Stopwatch.StartNew();
public void Dispose() => Console.WriteLine($"걸린 시간: {_watch.ElapsedMilliseconds}ms");
}using은 컴파일러가 try/finally로 바꿔 주는 문법이므로, 직접 finally에서 Dispose를 부르는 것보다 짧고 실수가 적습니다. 비동기 정리가 필요한 객체(IAsyncDisposable)에는 await using을 씁니다.
.NET에서 가장 널리 쓰이는 테스트 프레임워크는 xUnit, NUnit, MSTest입니다. 여기서는 앞의 Wallet 클래스가 src/Wallet 라이브러리 프로젝트에 있다고 보고, xUnit 테스트 프로젝트를 만들어 시험해 봅니다.
dotnet new xunit -o tests/Wallet.Tests
dotnet sln add tests/Wallet.Tests
dotnet add tests/Wallet.Tests reference src/Wallet
dotnet test # 모든 테스트 실행
dotnet test --filter "FullyQualifiedName~Withdraw" # 이름으로 골라 실행using Xunit;
public class WalletTests
{
[Fact]
public void Withdraw_DecreasesBalance()
{
var wallet = new Wallet(10_000m);
wallet.Withdraw(3_000m, "점심");
Assert.Equal(7_000m, wallet.Balance);
}
[Fact]
public void Withdraw_MoreThanBalance_Throws()
{
var wallet = new Wallet(1_000m);
var ex = Assert.Throws<InsufficientFundsException>(() => wallet.Withdraw(5_000m, "책"));
Assert.Equal(5_000m, ex.Requested);
}
[Theory]
[InlineData(0)]
[InlineData(-100)]
public void Withdraw_InvalidAmount_Throws(int amount)
{
var wallet = new Wallet(1_000m);
Assert.Throws<ArgumentOutOfRangeException>(() => wallet.Withdraw(amount, "잘못된 금액"));
}
}[Fact]는 입력이 없는 테스트, [Theory]는 [InlineData]로 여러 입력을 받아 반복하는 테스트입니다. 테스트 하나는 준비(Arrange) · 실행(Act) · 확인(Assert) 세 단계로 나누어 쓰면 읽기 쉽습니다.
MSTest는 마이크로소프트가 만든 프레임워크로, 특성 이름만 다를 뿐 구조는 비슷합니다. 템플릿은 dotnet new mstest입니다.
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class WalletTests
{
[TestMethod]
public void Withdraw_DecreasesBalance()
{
var wallet = new Wallet(10_000m);
wallet.Withdraw(3_000m, "점심");
Assert.AreEqual(7_000m, wallet.Balance);
}
}| 하는 일 | xUnit | MSTest |
|---|---|---|
| 테스트 메서드 | [Fact] | [TestMethod] |
| 매개변수 테스트 | [Theory] + [InlineData] | [TestMethod] + [DataRow] |
| 같은지 확인 | Assert.Equal | Assert.AreEqual |
| 테스트마다 준비 | 생성자 | [TestInitialize] |
try/catch/finally로 처리하고, 구체적인 예외부터 잡습니다. 다시 던질 때는 throw;를 씁니다.TryParse 같은 Try 패턴을, 업무 규칙 위반에는 사용자 정의 예외를 씁니다.IDisposable 객체는 using으로 감싸 예외가 나도 자원이 정리되게 합니다.dotnet new xunit(또는 mstest)으로 테스트 프로젝트를 만들고 dotnet test로 실행합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.