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

발행: (2026년 1월 14일 오후 07:56 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

C# dynamic은 함정문: 누수가 퍼지기 전에 차단하세요 (Dapper 사용자를 위한 필독)

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 (vardynamic으로 추론됨): 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( 호출을 검색하세요.
  • 잘못된 양성 결과나 놓친 경우가 있다면, 최소 재현 예시와 함께 이슈를 열세요.
Back to Blog

관련 글

더 보기 »

C#로 PowerPoint (PPT/PPTX)를 PDF로 변환

Spire.Presentation을 사용한 C에서 PowerPoint PPT/PPTX를 PDF로 변환하기 일상적인 개발 및 사무 작업에서 PowerPoint 파일을 PDF로 변환하는 것은 높은 우선순위입니다.