[PART10.예외 처리 기본(6/9)] using 선언 (C# 8) — 들여쓰기 없이 자원을 안전하게 풀어주는 법
블록형 vs 선언형 / 스코프 끝 자동 Dispose / IL은 동일 / 인위적 블록으로 수명 좁히기
목차
C# 8에서 도입된 using 선언은 한 줄로 자원을 안전하게 풀어주면서 코드 들여쓰기를 한 단계 줄여주는 문법입니다. 같은 일을 하는 기존 using 블록과 무엇이 다르고, 언제 어느 쪽을 골라야 하는지를 IL 분석으로 직접 확인하면서 정리합니다.
이전 글([using과 IDisposable](#))에서 IDisposable이 무엇이고 왜 필요한지를 다뤘습니다. 이 글은 그 기반 위에서 "같은 자원을 더 깔끔하게 푸는 방법"에 집중합니다.
1. 문제 제기 — using 블록이 깊어진다
StreamReader로 파일을 한 줄씩 읽고, JSON으로 파싱하고, 그 결과를 다시 다른 파일로 쓰는 메서드를 작성한다고 생각해 봅시다. IDisposable을 구현한 객체가 셋이라면 블록형 using으로 감쌀 때 들여쓰기가 세 단계로 깊어집니다.
public void ConvertJsonToCsv(string inputPath, string outputPath)
{
using (var fs = File.OpenRead(inputPath))
{
using (var reader = new StreamReader(fs))
{
using (var writer = new StreamWriter(outputPath))
{
// 진짜 작업은 여기서부터 시작
string? line;
while ((line = reader.ReadLine()) is not null)
{
writer.WriteLine(Convert(line));
}
}
}
}
}
문제는 두 가지입니다.
- rightward drift(우측 표류) — 핵심 로직이 화면 오른쪽 끝까지 밀려납니다. Unity 신입이 처음 마주치면 "이 메서드가 무슨 일을 하는지" 파악하기 어렵습니다.
- 수명 표현이 부정확 —
fs,reader,writer셋 다 메서드가 끝날 때 풀어주면 충분한데, 블록 구조 때문에 마치 "각각이 더 좁은 스코프를 가져야 하는 것처럼" 보입니다.
C# 8은 이 두 문제를 한 번에 해결하기 위해 using 선언(using var x = ...;)을 도입했습니다. 동작은 동일하되 형태만 평탄하게 풀어쓸 수 있게 한 문법입니다.
using— 자원 자동 해제 키워드IDisposable을 구현한 객체를 감싸 스코프가 끝날 때 자동으로Dispose()를 호출하게 만든다. C# 8 이전에는using (...) { ... }블록형만 있었고, C# 8부터using var x = ...;선언형이 추가됐다.
예시:using var stream = File.OpenRead(path);변수stream이 속한 스코프가 끝날 때stream.Dispose()가 자동 호출된다.
2. 개념 정의 — "선언이 있는 곳부터, 스코프 끝까지"
2.1 비유 — 손목 밴드와 입장권
블록형 using은 "이 영역 안에서만 유효한 입장권"입니다. 영역을 나가는 순간 입장권을 회수합니다. 선언형 using은 "손목에 채워진 밴드"입니다. 들어온 시점부터 그 공간을 떠날 때까지 차고 있다가, 나가는 문에서 한꺼번에 풀어줍니다.
차이는 "회수 시점"이 아니라 "회수 시점을 지정하는 방식"에 있습니다. 블록형은 닫는 중괄호 }가 회수 위치이고, 선언형은 변수가 속한 가장 가까운 스코프의 끝이 회수 위치입니다.
2.2 시각화 — 두 형태의 스코프 비교

블록형은 { } 블록 안만이 변수의 수명입니다. 선언형은 변수가 선언된 시점부터 그 변수를 감싸는 가장 안쪽 블록(메서드·if 블록·for 본문·인위적 { } 등)이 끝날 때까지가 수명입니다.
2.3 기본 코드 — 같은 일을 두 가지 형태로
// Before: 블록형 using
public static void BlockForm(string path)
{
using (var stream = File.OpenRead(path))
{
int b = stream.ReadByte();
}
}
// After: using 선언 (C# 8)
public static void DeclarationForm(string path)
{
using var stream = File.OpenRead(path);
int b = stream.ReadByte();
}
읽은 결과는 같지만 들여쓰기가 한 단계 줄었고, "stream의 수명은 메서드 전체"라는 의도가 코드에 그대로 드러납니다.
3. 내부 동작 — 두 형태가 같은 IL로 풀린다
선언형은 문법적으로만 다를 뿐, 컴파일러가 내부적으로 만드는 코드는 블록형과 동일합니다. 컴파일러는 둘 다 try { ... } finally { x?.Dispose(); } 형태로 풀어냅니다. 직접 IL을 비교해 봅니다.
3.1 시각화 — 두 형태가 같은 IL로 만나는 경로

3.2 IL 분석 — 블록형
블록형의 IL입니다.
.method public hidebysig static
void BlockForm (string path) cil managed
{
.maxstack 1
.locals init (
[0] class [System.Runtime]System.IO.FileStream, // FileStream 지역 변수
[1] int32 // ReadByte 결과
)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: call class [System.Runtime]System.IO.FileStream [System.Runtime]System.IO.File::OpenRead(string) // File.OpenRead
IL_0007: stloc.0 // 결과를 stream(loc.0)에 저장
.try
{
IL_0008: nop
IL_0009: ldloc.0
IL_000a: callvirt instance int32 [System.Runtime]System.IO.Stream::ReadByte() // stream.ReadByte()
IL_000f: stloc.1
IL_0010: nop
IL_0011: leave.s IL_001e // try 정상 종료 — finally 거쳐 함수 끝으로
}
finally
{
IL_0013: ldloc.0
IL_0014: brfalse.s IL_001d // null이면 Dispose 호출 건너뜀
IL_0016: ldloc.0
IL_0017: callvirt instance void [System.Runtime]System.IDisposable::Dispose() // stream.Dispose()
IL_001c: nop
IL_001d: endfinally
}
IL_001e: ret
}
3.3 IL 분석 — 선언형
이번엔 같은 일을 하는 선언형입니다.
.method public hidebysig static
void DeclarationForm (string path) cil managed
{
.maxstack 1
.locals init (
[0] class [System.Runtime]System.IO.FileStream,
[1] int32
)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: call class [System.Runtime]System.IO.FileStream [System.Runtime]System.IO.File::OpenRead(string)
IL_0007: stloc.0
.try
{
IL_0008: ldloc.0
IL_0009: callvirt instance int32 [System.Runtime]System.IO.Stream::ReadByte()
IL_000e: stloc.1
IL_000f: leave.s IL_001c
}
finally
{
IL_0011: ldloc.0
IL_0012: brfalse.s IL_001b // 같은 null 체크
IL_0014: ldloc.0
IL_0015: callvirt instance void [System.Runtime]System.IDisposable::Dispose() // 같은 Dispose 호출
IL_001a: nop
IL_001b: endfinally
}
IL_001c: ret
}
핵심 포인트는 다음과 같습니다.
- 두 형태 모두
.try { } finally { }구조로 풀립니다. 컴파일러가 자동으로try/finally를 생성하므로 예외가 던져지더라도Dispose()는 반드시 호출됩니다. brfalse.s+callvirt Dispose패턴은 양쪽이 똑같습니다.stream이null이 아닌 경우에만Dispose()를 호출하는 방어 로직입니다. 이는 사용자가 흔히 쓰는stream?.Dispose()와 동등한 IL입니다.- 차이는 단 두 가지 — 블록형은 try 진입 직후
nop이 하나 더 있고(블록 시작 위치 표시용 디버그 nop), 명령어 위치(IL_xxxx)가 1바이트씩 밀려 있습니다. 즉 런타임 동작·할당·성능은 완벽히 동일합니다.
callvirt— 가상 디스패치 호출 IL 명령어.null체크가 내장되어 있어 인스턴스가null이면NullReferenceException을 던진다.IDisposable.Dispose()는 인터페이스 메서드라callvirt로 디스패치된다.
Unity 핫패스에서 사용해도 둘은 같은 비용을 갖습니다. "선언형이 더 빠르다", "블록형이 더 안전하다" 같은 주장은 IL을 보면 모두 근거가 없습니다. 선택 기준은 오직 가독성과 수명 표현입니다.
4. 실전 적용 — 언제 어느 쪽을 고르는가
4.1 판단 기준 한 줄 요약
| 상황 | 권장 형태 | 이유 |
|---|---|---|
| 메서드 전체에서 자원이 살아 있어도 무방 | 선언형 | 들여쓰기 절약 + 수명 표현이 자연스러움 |
여러 IDisposable을 연속으로 사용 |
선언형 | 중첩 블록 제거(평탄화) |
| 메서드 안에서 자원을 일찍 풀어주고 다른 작업 수행 | 블록형 또는 인위적 블록 | 수명 정밀 제어 |
| 자원의 수명 영역을 시각적으로 강조하고 싶을 때 | 블록형 | { }로 영역이 한눈에 보임 |
4.2 Before — 들여쓰기 지옥(블록형 중첩)
public string ReadFirstLine(string path)
{
using (var fs = File.OpenRead(path))
{
using (var reader = new StreamReader(fs))
{
return reader.ReadLine() ?? string.Empty;
}
}
}
들여쓰기가 두 단계 깊어지고, "이 메서드의 본질은 첫 줄을 읽어 반환하는 것뿐"이라는 사실이 시각적으로 잘 드러나지 않습니다.
4.3 After — 평탄화된 선언형
public static void MultiDeclaration(string path)
{
using var fs = File.OpenRead(path);
using var reader = new StreamReader(fs);
string? line = reader.ReadLine();
}
같은 동작이지만 메서드 본문이 위에서 아래로 한 줄씩 읽힙니다. Dispose는 선언의 역순(LIFO)으로 호출됩니다. 즉 reader.Dispose() → fs.Dispose() 순서이며, 이는 의존성 순서(reader가 fs를 감싸고 있음)와 일치하므로 자원 해제 순서가 안전합니다.
4.4 IL 분석 — 평탄화도 결국 중첩 try/finally
선언형으로 평탄해 보여도 컴파일러는 여전히 중첩된 try/finally로 풀어냅니다. fs를 감싸는 외부 try와, reader를 감싸는 내부 try가 만들어집니다.
.method public hidebysig static
void MultiDeclaration (string path) cil managed
{
.maxstack 1
.locals init (
[0] class [System.Runtime]System.IO.FileStream, // fs
[1] class [System.Runtime]System.IO.StreamReader, // reader
[2] string // line
)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: call class [System.Runtime]System.IO.FileStream [System.Runtime]System.IO.File::OpenRead(string)
IL_0007: stloc.0 // fs 저장
.try // ← 외부 try (fs 보호)
{
IL_0008: ldloc.0
IL_0009: newobj instance void [System.Runtime]System.IO.StreamReader::.ctor(class [System.Runtime]System.IO.Stream) // new StreamReader(fs)
IL_000e: stloc.1 // reader 저장
.try // ← 내부 try (reader 보호)
{
IL_000f: ldloc.1
IL_0010: callvirt instance string [System.Runtime]System.IO.TextReader::ReadLine() // reader.ReadLine()
IL_0015: stloc.2
IL_0016: leave.s IL_002e
}
finally // ← 내부 finally → reader.Dispose()
{
IL_0018: ldloc.1
IL_0019: brfalse.s IL_0022
IL_001b: ldloc.1
IL_001c: callvirt instance void [System.Runtime]System.IDisposable::Dispose()
IL_0021: nop
IL_0022: endfinally
}
}
finally // ← 외부 finally → fs.Dispose()
{
IL_0023: ldloc.0
IL_0024: brfalse.s IL_002d
IL_0026: ldloc.0
IL_0027: callvirt instance void [System.Runtime]System.IDisposable::Dispose()
IL_002c: nop
IL_002d: endfinally
}
IL_002e: ret
}
reader.Dispose()가 내부 finally에서 먼저 호출되고 그 다음 외부 finally에서 fs.Dispose()가 호출됩니다. 선언 역순으로 풀린다는 약속이 IL에서 그대로 보장됩니다. 이는 단순히 시각적 평탄화일 뿐, 실제 정리 순서는 사용자가 의도한 대로 정확히 재현됩니다.
4.5 Unity 실전 — NativeArray<T>와 Job System
Unity의 NativeArray<T>나 NativeList<T>는 IDisposable을 구현한 비관리 메모리 컬렉션입니다. Allocator에 따라 자동 회수가 되지 않으면 즉시 메모리 누수로 이어집니다.
// Unity 핫패스에서 한 프레임 내에 임시로 native 배열을 만들고 Job에 넘기는 패턴
void RunJobThisFrame()
{
using var positions = new NativeArray<Vector3>(1024, Allocator.TempJob);
using var velocities = new NativeArray<Vector3>(1024, Allocator.TempJob);
var job = new MoveJob { Positions = positions, Velocities = velocities };
job.Schedule(positions.Length, 64).Complete();
// 이 메서드가 끝나면 velocities → positions 순서로 Dispose 호출됨
// → Allocator.TempJob의 4프레임 안전 검증과 잘 어울림
}
Allocator.TempJob은 4프레임 안에 Dispose()를 호출하지 않으면 안전 시스템이 경고를 던집니다. using 선언으로 묶어두면 메서드 끝에서 자동으로 풀어주므로, 이 경고 조건을 자연스럽게 만족하게 됩니다. 블록형 중첩으로 같은 일을 하면 코드가 어떻게 늘어나는지 비교해 보면 선언형의 가치가 분명해집니다.
Unity GC 관점:NativeArray<T>는 C#의 가비지 컬렉션 대상이 아닙니다(struct). 다만 내부에서 비관리 힙(native allocator)을 잡고 있기 때문에Dispose를 누락하면 C# GC와 무관한 메모리 누수가 발생합니다.using 선언은 이 누수를 가장 안전하게 막는 패턴입니다.
5. 함정과 주의사항
5.1 ❌ 함정 1: 큰 자원이 메서드 끝까지 살아있는 문제
가장 흔한 함정은 선언형이 자원을 너무 오래 잡고 있는 경우입니다. 자원을 빨리 풀어줘야 다음 작업이 가능한 상황에서 메서드 전체가 끝날 때까지 들고 있으면 메모리·핸들 점유가 길어집니다.
// ❌ 잘못된 패턴 — 1GB 메모리를 메서드 끝까지 점유
public void ProcessLargeFile(string path)
{
using var fs = File.OpenRead(path);
byte[] buffer = ReadAll(fs); // 1GB 읽기
// 여기서 fs는 더 이상 필요 없는데...
// 무거운 후처리 (수십 초)
var result = HeavyTransform(buffer);
SendToServer(result);
} // ← 이 시점에야 fs.Dispose()가 호출됨
// 파일 핸들이 후처리·네트워크 IO 동안 내내 열려 있음
문제는 두 가지입니다.
- 파일 핸들이 메서드 끝까지 열려 있어 다른 프로세스(또는 Unity AssetBundle 갱신 도구 등)가 해당 파일에 접근할 수 없습니다.
- 모바일 환경에서 파일 디스크립터(file descriptor) 수가 제한된 경우, 핸들 점유가 누적되어 다른 파일을 못 여는 상황이 생깁니다.
5.2 ✅ 해법 — 인위적 블록으로 스코프 좁히기
자원을 빨리 풀고 싶을 때는 블록형으로 되돌리거나, 인위적 { } 블록으로 선언형의 수명을 좁힙니다.
// ✅ 해법 1: 인위적 블록
public void ProcessLargeFile(string path)
{
byte[] buffer;
{
using var fs = File.OpenRead(path);
buffer = ReadAll(fs);
} // ← 이 시점에 fs.Dispose() — 파일 핸들 즉시 반환
// 무거운 후처리는 핸들이 닫힌 상태에서 진행
var result = HeavyTransform(buffer);
SendToServer(result);
}
이 패턴은 IL을 보면 명확합니다.
.method public hidebysig static
void NarrowScope (string path) cil managed
{
.locals init (
[0] class [System.Runtime]System.IO.FileStream,
[1] int32
)
IL_0000: nop
IL_0001: nop // 인위적 블록 시작
IL_0002: ldarg.0
IL_0003: call class [System.Runtime]System.IO.FileStream [System.Runtime]System.IO.File::OpenRead(string)
IL_0008: stloc.0
.try
{
IL_0009: ldloc.0
IL_000a: callvirt instance int32 [System.Runtime]System.IO.Stream::ReadByte()
IL_000f: stloc.1
IL_0010: leave.s IL_001d
}
finally
{
IL_0012: ldloc.0
IL_0013: brfalse.s IL_001c
IL_0015: ldloc.0
IL_0016: callvirt instance void [System.Runtime]System.IDisposable::Dispose() // ← 여기서 끝남(이후 코드 실행 전)
IL_001b: nop
IL_001c: endfinally
}
IL_001d: nop // 인위적 블록 끝
IL_001e: ldstr "after dispose" // 후속 작업은 Dispose 이후 실행됨
IL_0023: call void [System.Console]System.Console::WriteLine(string)
IL_0028: nop
IL_0029: ret
}
Dispose()가 후속 코드 실행 전에 호출되는 것이 IL로 명백히 보입니다. 메서드 끝까지 기다리지 않습니다. Unity에서 AssetBundle 파일을 임시로 열어 헤더만 읽고 즉시 닫아야 하는 경우에 이 패턴을 사용합니다.
// ✅ 해법 2: 블록형으로 명시적으로 표현
public void ProcessLargeFile(string path)
{
byte[] buffer;
using (var fs = File.OpenRead(path))
{
buffer = ReadAll(fs);
}
var result = HeavyTransform(buffer);
SendToServer(result);
}
해법 1과 해법 2는 IL이 동일합니다(인위적 블록 vs using 블록은 결국 같은 try/finally 구조로 풀립니다). 어느 쪽을 골라도 됩니다. 팀 컨벤션에 맞추세요.
5.3 ❌ 함정 2: if/for 안에서 선언하면 수명이 그 안에서 끝남
선언형은 변수가 속한 가장 가까운 블록의 끝에서 풀립니다. if 블록 안에서 선언하면 if 블록을 벗어나는 순간 Dispose가 호출됩니다.
// ❌ 의도와 다른 동작 — if 블록을 벗어나면 닫힘
public void Foo(bool condition, string path)
{
if (condition)
{
using var stream = File.OpenRead(path);
// ...
} // ← 여기서 stream.Dispose()
DoSomethingElse(); // stream은 여기까지 살아 있지 않다
}
대부분의 경우 이는 의도한 대로 잘 동작하는 패턴입니다. 다만 "메서드 전체에서 살아 있길 원했는데 왜 일찍 닫히지?" 같은 혼동이 생기면, 변수가 속한 가장 가까운 블록을 다시 확인하면 됩니다.
5.4 ✅ 해법 — 변수를 메서드 레벨에서 선언
public void Foo(bool condition, string path)
{
using var stream = condition ? File.OpenRead(path) : Stream.Null;
if (condition)
{
// ...
}
DoSomethingElse(); // stream은 여기서도 살아 있음
} // ← 메서드 끝에서 Dispose
Stream.Null은 IDisposable이지만 실제로는 아무것도 풀어주지 않는 안전한 더미입니다. 분기에 따라 자원을 만들지 결정해야 한다면 이 패턴이 유용합니다.
5.5 ❌ 함정 3: await using 선언과 혼동
비동기 자원에는 IAsyncDisposable이 있고, 이를 위한 await using 선언이 별도로 존재합니다. using 선언만 쓰면 DisposeAsync()가 동기 컨텍스트에서 호출될 위험이 있습니다.
// ❌ 비동기 자원에 동기 using 사용
async Task ProcessAsync()
{
using var conn = await OpenAsync(); // conn이 IAsyncDisposable이면 잘못 — conn.Dispose()만 호출됨
// ...
}
비동기 정리에 해당하는 자원에는 반드시 await using을 사용해야 합니다.
// ✅ 비동기 자원 → await using
async Task ProcessAsync()
{
await using var conn = await OpenAsync();
// ...
} // ← 메서드 끝에서 await conn.DisposeAsync()
await using 선언과 IAsyncDisposable은 다음 글에서 자세히 다룹니다.
6. C# 버전별 변화
6.1 C# 1.0 ~ C# 7 — 블록형 using만 존재
// C# 7 시절 — 블록형만 가능
using (var stream = File.OpenRead(path))
{
// ...
}
여러 자원을 사용할 때 들여쓰기가 깊어지는 것을 피할 방법이 없었습니다. 같은 타입 두 개는 한 using 안에서 콤마로 묶을 수 있었지만(using (var a = ..., b = ...)), 다른 타입은 여전히 중첩이 필요했습니다.
6.2 C# 8 — using 선언과 await using 도입
// C# 8
using var stream = File.OpenRead(path);
await using var conn = await OpenAsync();
런타임 요건:
- C# 8.0 이상 컴파일러가 필요합니다.
- 기본 타깃은 .NET Core 3.0 / .NET Standard 2.1 이상입니다.
await using은IAsyncDisposable인터페이스가 필요하고, 이 인터페이스는 .NET Standard 2.1에서 처음 등장했습니다. - 단,
using선언만 사용한다면(await using없이)<LangVersion>8.0</LangVersion>설정만으로 .NET Framework·Unity 구버전에서도 컴파일 가능합니다. 컴파일러가 만들어주는 IL은 단순히try/finally이기 때문입니다.
6.3 Unity에서의 사용 가능 시점
Unity의 C# 버전 지원은 엔진 버전에 묶여 있습니다.
| Unity 버전 | 지원 C# 버전 | using 선언 사용 |
|---|---|---|
| Unity 2020 LTS | C# 8 (제한적) | 기본 활성 — 사용 가능 |
| Unity 2021.2 LTS 이후 | C# 9 지원 | 사용 가능(권장 환경) |
| Unity 2022 LTS 이후 | C# 9 + 일부 10 기능 | 사용 가능 |
Unity 2020 LTS부터 using 선언은 정식 지원되며, 사실상 현행 모든 모바일 프로젝트에서 사용 가능합니다. 단, Unity가 자체적으로 패치하는 부분(async/await·record 등)이 버전마다 미묘하게 다르므로 프로젝트의 Editor 버전 기준으로 한 번 확인하는 것이 안전합니다.
6.4 마이그레이션 가이드 — 블록형 → 선언형
기존 코드베이스에 블록형 using이 많다면 다음 기준으로 옮깁니다.
| 패턴 | 권장 행동 |
|---|---|
메서드 전체가 using 블록으로 감싸진 경우 |
선언형으로 변환(들여쓰기 한 단계 절약) |
한 using 블록에 다른 using이 중첩된 경우 |
둘 다 선언형으로 변환(평탄화) |
using 블록 뒤에 같은 메서드 내에서 추가 작업이 있는 경우 |
블록형 그대로 유지(수명을 좁혀야 의미 있음) |
마지막 줄이 핵심입니다. 모든 블록형을 선언형으로 바꾸는 것은 잘못입니다. 블록형이 의도적으로 수명을 짧게 가져가는 경우라면 그대로 둬야 함정 1을 피할 수 있습니다.
7. 정리 — using 선언 체크리스트
- [ ] 블록형과 선언형은 IL이 사실상 동일하다 — 성능 차이 없음, 둘 다
try/finally + callvirt Dispose로 풀린다. - [ ] 선언형의 수명은 변수가 속한 가장 가까운 블록의 끝까지이다. 메서드면 메서드 끝,
if블록이면if끝. - [ ] 여러
IDisposable을 연속으로 쓰면 선언형 → 평탄화.Dispose는 선언 역순(LIFO)으로 호출되어 의존성 순서를 보장한다. - [ ] 자원이 메서드 끝보다 일찍 닫혀야 하면 블록형 또는 인위적
{ }블록을 사용한다. "선언형이 더 나아 보여서" 무리하게 변환하지 않는다. - [ ]
async메서드에서IAsyncDisposable자원을 다룰 때는await using을 사용한다.using만 쓰면Dispose()동기 호출이 되어 잘못된 정리 경로로 빠진다. - [ ] Unity는 2020 LTS부터
using선언 사용 가능.NativeArray<T>등 비관리 메모리 컬렉션의 정리 누락을 가장 안전하게 막는 패턴이다. - [ ] 마이그레이션은 일괄 변환이 아니라 "수명이 메서드 전체와 일치하는 경우"부터 차근히 진행한다.
다음 글에서는 이 글에서 잠깐 등장한 await using과 IAsyncDisposable을 다룹니다. 비동기로 정리해야 하는 자원(DB 커넥션·HTTP 연결·Pipe 등)이 왜 동기 using으로는 부족한지를 IL과 함께 살펴봅니다.
'C# 기초' 카테고리의 다른 글
| [PART10.예외 처리 기본(8/9)] CallerArgumentExpression — 인수 표현식 문자열 자동 주입 (C# 10) (0) | 2026.05.05 |
|---|---|
| [PART10.예외 처리 기본(7/9)] await using + IAsyncDisposable — 비동기 정리 (2) | 2026.05.05 |
| [PART10.예외 처리 기본(5/9)] using 문과 IDisposable 기초 — 비관리 리소스를 안전하게 닫는 약속 (0) | 2026.05.05 |
| [PART10.예외 처리 기본(4/9)] 자주 만나는 예외 — 이름만이라도 기억 (0) | 2026.05.05 |
| [PART10.예외 처리 기본(3/9)] throw — 예외를 어떻게, 왜 그렇게 던져야 하는가 (2) | 2026.05.05 |
