본문 바로가기

C#

C# 디버깅 심화: Conditional Attribute와 DebuggerDisplay

디버깅 품질은 코드 품질과 생산성에 직접 연결됩니다. 이번 글에서는 조건부 컴파일 호출을 만드는 ConditionalAttribute와 디버거 변수 창을 읽기 좋게 개선하는 DebuggerDisplay를 실무 중심으로 정리합니다.

1. ConditionalAttribute 기본 개념과 사용

ConditionalAttribute는 지정한 컴파일 심볼이 정의되었을 때만 해당 메서드 호출을 컴파일에 포함합니다. 대표적으로 DEBUG, TRACE가 있습니다.

using System; 
using System.Diagnostics;

class Program
{
    [Conditional("DEBUG")] // DEBUG에서만 호출이 살아남습니다.
    static void Log(string message)
    {
        Console.WriteLine($"[DBG] {message}");
    }

    [Conditional("DEBUG"), Conditional("TRACE")] // 두 심볼 중 하나라도 정의되면 호출이 유지됩니다.
    static void TraceOrDebug(string message)
    {
        Console.WriteLine($"[T/D] {message}");
    }

    static void Main()
    {
        Log("시작");           // Release 빌드에서는 통째로 제거됩니다.
        TraceOrDebug("흐름 추적");
    }
}

핵심 요약입니다.

- 메서드 자체는 항상 컴파일되지만, 호출문이 조건에 따라 제거됩니다.

- 반환형은 반드시 void여야 합니다. 반환값이 필요한 곳에는 사용할 수 없습니다.

- Conditional 특성이 여러 개면 OR 조건으로 동작합니다. 하나라도 맞으면 호출이 유지됩니다.

2. 흔한 함정과 주의사항

- 인자 평가도 함께 제거됩니다. 즉, 조건이 맞지 않아 호출이 빠지면 전달 인자 표현식도 평가되지 않습니다.

using System; 
using System.Diagnostics;

class Demo
{
    [Conditional("DEBUG")] 
    static void Log(string msg) => Console.WriteLine(msg);

    static int SideEffect()
    {
        Console.WriteLine("SideEffect 실행");
        return 42;
    }

    static void Run()
    {
        Log($"값={SideEffect()}"); // Release에서는 SideEffect도 실행되지 않습니다.
    }
}

- 복잡한 로직을 Conditional 메서드에 넣지 마십시오. Release에서 호출이 사라지면 기대한 부수효과가 없어집니다.

- async void도 기술적으로 가능하나 예외 전파가 어려워 권장하지 않습니다. 디버그 전용이라도 단순한 동기 로깅 정도로 한정하는 것이 안전합니다.

3. 특성(어트리뷰트) 클래스에 적용하기

ConditionalAttribute는 메서드뿐 아니라 어트리뷰트 클래스에도 적용할 수 있습니다. 이렇게 하면 특정 빌드에서만 어트리뷰트 메타데이터가 포함됩니다.

using System; 
using System.Diagnostics;

[Conditional("DEBUG")]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property)]
sealed class DebugOnlyAttribute : Attribute { }

[DebugOnly] // DEBUG에서만 메타데이터에 포함됩니다.
class Sample
{
    [DebugOnly]
    public void Test() { }
}

메타데이터 크기를 줄이거나, 디버그 전용 마커를 남기고 싶을 때 유용합니다.

4. DebuggerDisplay로 보기 좋은 디버깅

DebuggerDisplay는 디버거(예: Visual Studio)에서 객체가 표시되는 문자열을 커스터마이즈합니다. 복잡한 객체도 한눈에 상태를 파악할 수 있게 만듭니다.

using System.Collections.Generic;
using System.Diagnostics;

[DebuggerDisplay("{Id}: {Name,nq} ({Status}), Count={Items?.Count}")]
class Order
{
    public int Id { get; }
    public string Name { get; }
    public string Status { get; private set; }
    public List<string> Items { get; } = new();

    public Order(int id, string name)
    {
        Id = id;
        Name = name;
        Status = "New";
    }
}

- {표현식} 형태로 필드/속성/간단한 표현식을 사용할 수 있습니다.

- ,nq 서식 지정자는 문자열의 따옴표를 제거합니다.

- 널 가능성이 있다면 ?. 연산자를 사용해 디버거 평가 중 예외를 피합니다.

5. DebuggerDisplay 표현식 팁

- 표현식은 가볍고 부작용이 없어야 합니다. 네트워크/파일 IO, 잠재적 예외를 유발하는 로직은 피합니다.

- 성능에 민감한 타입에는 최소 정보만 노출하고 상세 정보는 ToString 등으로 분리합니다.

- 디버그 중 NRE를 피하려면 널 병합(??)이나 조건 연산자를 활용합니다.

[DebuggerDisplay("User: {Name ?? \"<null>\"}, Roles={Roles?.Count}")]
class User { /* ... */ }

6. DebuggerBrowsable로 컬렉션을 깔끔하게

컬렉션을 바로 펼쳐 보이고 싶다면 DebuggerBrowsable의 RootHidden을 활용합니다.

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

[DebuggerDisplay("Order {Id}, Items={Items.Count}")]
class Order
{
    public int Id { get; }
    public List<string> Items { get; } = new();

    // 디버거에서 Order 하위에 바로 항목들이 나타납니다.
    [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
    public string[] DebugItems => Items.ToArray();

    public Order(int id) => Id = id;
}

실제 공개 API에는 영향을 주지 않으면서 디버거에서만 보기 좋게 정리됩니다.

7. 함께 쓰는 실전 패턴

디버그 빌드에서만 가벼운 로깅을 하고, 객체는 DebuggerDisplay로 요약 정보를 노출합니다.

using System; 
using System.Collections.Generic;
using System.Diagnostics;

static class Dbg
{
    [Conditional("DEBUG")]
    public static void Log(string message) => Console.WriteLine($"[DBG] {message}");
}

[DebuggerDisplay("{Id}: {Name,nq} ({Status}), Items={Items.Count}")]
class Order
{
    public int Id { get; }
    public string Name { get; }
    public string Status { get; private set; } = "New";
    public List<string> Items { get; } = new();

    public Order(int id, string name)
    {
        Id = id; Name = name;
        Dbg.Log($"생성: {Id}, {Name}");
    }

    public void Approve()
    {
        Status = "Approved";
        Dbg.Log($"승인: {Id}");
    }
}

Release 빌드에서는 로그 호출이 제거되어 오버헤드가 없습니다. 디버거에서는 리스트 요약만으로도 객체 상태를 빠르게 파악할 수 있습니다.

8. 마무리 체크리스트

- Conditional 메서드는 void, 인자 평가 부작용 주의

- 여러 심볼을 OR로 조합해 환경에 맞춘 로깅 구성

- DebuggerDisplay는 짧고 안전한 표현식만 사용

- 컬렉션은 DebuggerBrowsable(RootHidden)으로 가독성 개선

- 디버그/릴리스 빌드 간 동작 차이를 항상 염두에 두고 테스트합니다

두 기능을 적절히 조합하면 디버그 경험은 간결해지고, 릴리스 성능과 코드 청결도도 함께 지킬 수 있습니다.