C# dynamic은 함정이다: 누수가 퍼지기 전에 차단하라 (Dapper 사용자라면 꼭 읽어야 함)
Source: Dev.to

C#에서 dynamic은 경계 부분에서 편리하게 사용할 수 있습니다. 하지만 정적 타입을 기대하는 곳에 조용히 누수가 발생할 수 있으며, 메서드 시그니처는 여전히 완전히 안전해 보일 수 있습니다.
실제 프로젝트에서 이러한 누수를 경험했고, 이를 조기에 감지하기 위해 작은 Roslyn 분석기를 만들었습니다. 저는 DynamicLeakAnalyzer(NuGet: DimonSmart.DynamicLeakAnalyzer)의 작성자이며, 이 글에서는 해당 분석기가 목표로 하는 문제와 경계에서 누수를 차단하는 방법을 설명합니다.
dynamic이 실제 의미하는 바
C#에서 dynamic은 정적 타입이지만, 표현식에 대해 컴파일 시점 타입 검사를 우회합니다. 멤버 접근, 오버로드 해석, 연산자 및 형 변환은 런타임에 바인딩됩니다.
런타임에서는 값이 여전히 object 형태로 흐릅니다. 컴파일러는 캐싱을 포함한 런타임 바인딩(DLR 호출 사이트)을 생성하므로, 첫 번째 바인딩 이후 반복 호출은 더 빨라질 수 있습니다.
“동적 전염”이 발생하는 방식
검토 중에 누수를 찾기 어렵게 만드는 두 가지 패턴:
- 보이지 않는 런타임 작업: 멤버 접근과 변환이 런타임에 발생합니다.
- 속이는 시그니처: 메서드가
int를 반환하더라도 내부에서 동적 변환을 수행할 수 있습니다. var가 조용히dynamic이 될 수 있음: 오른쪽 값이dynamic이면 추론된 타입이dynamic이 됩니다.
예시
class Program
{
static int GetX(int i) => i;
static void Main()
{
dynamic prm = 123;
int a = GetX(prm); // DSM001: implicit dynamic conversion at runtime
var b = GetX(prm); // DSM002: b becomes dynamic because the invocation is dynamic
}
}
실제 코드에서는 prm이 로컬 변수가 아닌 경우가 많습니다. 메서드에 전달되는 객체일 수 있으며, 동적 타입이 필드나 프로퍼티에 숨겨질 수 있습니다. 예: prm.Payload.Id.
Dapper 함정
Dapper는 눈치채지 못하게 dynamic을 쉽게 도입하도록 합니다. QueryFirst를 반환값 없이 호출하면 동적 행 객체(보통 DapperRow)가 반환되므로 속성 접근이 동적으로 이루어집니다.
using Dapper;
using System.Data;
public static class Repo
{
public static int GetActiveUserId(IDbConnection cn)
{
// QueryFirst() without returns a dynamic row (DapperRow).
var row = cn.QueryFirst("select Id from Users where IsActive = 1");
// row.Id is dynamic, the conversion to int happens at runtime.
return row.Id; // DSM001
}
}
이러한 코드는 시그니처가 int라고 표시되어 있기 때문에 리뷰를 쉽게 통과합니다. 동적 바인딩은 중간에 숨겨져 있습니다.
Dapper에서 더 안전한 대안
타입된 API
int id = cn.QuerySingle("select Id from Users where IsActive = 1");
작은 DTO에 매핑
public sealed record UserId(int Id);
int id = cn.QuerySingle("select Id from Users where IsActive = 1").Id;
동적 행을 사용해야 한다면 즉시 캐스팅
var row = cn.QueryFirst("select Id from Users where IsActive = 1");
int id = (int)row.Id;
목표는 “동적을 절대 사용하지 않는다”가 아니라 “경계에서 누수를 차단한다”는 것입니다.
The solution: DynamicLeakAnalyzer
DynamicLeakAnalyzer는 이러한 누수를 퍼지기 전에 크게 알리는 Roslyn 분석기입니다. 두 가지 규칙을 보고합니다:
- DSM001 (암시적 dynamic 변환): 정적 타입이 기대되는 곳(반환, 할당, 인수 등)에서
dynamic표현식이 사용됩니다. 코드는 컴파일되지만 변환은 런타임에 발생합니다. - DSM002 (
var가dynamic으로 추론됨):var가 동적 결과를 포착하고 dynamic이 됩니다.
설치 및 적용
분석기를 추가합니다:
dotnet add package DimonSmart.DynamicLeakAnalyzer
.editorconfig를 사용하여 경고를 오류로 만들기:
root = true
[*.cs]
dotnet_diagnostic.DSM001.severity = error
dotnet_diagnostic.DSM002.severity = error
dynamic이 적합한 경우와 부적합한 경우
좋은 경계 예시
- COM 상호 운용
- JSON 어댑터 및 연결 코드
- 데이터베이스 어댑터 (동적 Dapper 행 포함)
dynamic 사용을 피해야 할 경우
- 핵심 도메인 로직
- 핫 루프
- 다른 개발자를 위한 라이브러리
다음 단계
- 실제 코드베이스에서 분석기를 실행하고 동적 누수가 이미 존재하는 위치를 확인하세요.
- Dapper를 사용한다면, 동적 행을 반환하는
QueryFirst(및 비제네릭Query(호출을 검색하세요. - 잘못된 양성 결과나 놓친 경우가 있다면, 최소 재현 예시와 함께 이슈를 열세요.