본문 바로가기

C#

C# 호출자 정보 특성(Caller Info Attributes) 활용

호출자 정보 특성은 호출한 위치(메서드명, 파일 경로, 줄 번호, 인수 표현식)를 컴파일 타임에 문자열/숫자로 주입해 주는 기능입니다. 로깅, 디버깅, 가드(검증) 코드에서 특히 유용합니다.

네임스페이스: System.Runtime.CompilerServices

1. 기본 개념과 사용 형태

특성은 선택적 매개변수(optional parameter)에 적용하며, 호출 시 값을 생략하면 컴파일러가 정보를 채워 줍니다.

using System;
using System.Runtime.CompilerServices;

static class Trace
{
    public static void Log(
        string message,
        [CallerMemberName] string member = "",
        [CallerFilePath] string file = "",
        [CallerLineNumber] int line = 0)
    {
        Console.WriteLine($"[{System.IO.Path.GetFileName(file)}:{line} - {member}] {message}");
    }
}

class Program
{
    static void Main()
    {
        Trace.Log("시작합니다"); // 파일명, 줄번호, 메서드명이 자동 주입됩니다.
        DoWork();
    }

    static void DoWork()
    {
        Trace.Log("작업 처리 중");
    }
}

포인트

  • 런타임 반사가 아닌 컴파일 타임 주입이므로 오버헤드가 거의 없습니다.
  • 파일 경로는 전체 경로가 들어오므로 민감 정보 노출이 걱정되면 Path.GetFileName 등으로 줄여 출력합니다.

2. INotifyPropertyChanged에서 [CallerMemberName]

속성 변경 알림에 속성명을 직접 문자열로 쓰면 오타에 취약합니다. CallerMemberName으로 안전하게 대체합니다.

using System.ComponentModel;
using System.Runtime.CompilerServices;

class Person : INotifyPropertyChanged
{
    private string _name = "";
    public string Name
    {
        get => _name;
        set
        {
            if (_name == value) return;
            _name = value;
            OnPropertyChanged(); // "Name"을 자동으로 채워줍니다.
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

3. [CallerArgumentExpression]으로 가드 강화

C# 10부터 인수로 전달한 표현식 문자열을 받아올 수 있습니다. 검증 실패 시 어떤 식이 실패했는지 명확히 기록/예외에 담을 수 있습니다.

using System;
using System.Runtime.CompilerServices;

public static class Guard
{
    public static T NotNull<T>(T? value,
        [CallerArgumentExpression("value")] string? expr = null)
        where T : class
    {
        if (value is null)
            throw new ArgumentNullException(paramName: expr);
        return value;
    }

    public static void Require(bool condition,
        [CallerArgumentExpression("condition")] string? expr = null)
    {
        if (!condition)
            throw new ArgumentException($"조건 실패: {expr}");
    }
}

class Demo
{
    static void Run(string? path, int count)
    {
        path = Guard.NotNull(path);             // paramName에 "path"가 들어갑니다.
        Guard.Require(count > 0);               // 메시지에 "count > 0"이 들어갑니다.
    }
}

4. 실전 로깅/디버깅 패턴

메서드 진입/종료 추적, 예외 로깅, 성능 측정에서 호출자 정보를 함께 남기면 문제 지점을 빠르게 찾을 수 있습니다.

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

static class PerfLog
{
    public static IDisposable Scope(
        string message,
        [CallerMemberName] string member = "",
        [CallerFilePath] string file = "",
        [CallerLineNumber] int line = 0)
        => new ScopeImpl(message, member, file, line);

    private sealed class ScopeImpl : IDisposable
    {
        private readonly string _prefix;
        private readonly Stopwatch _sw = Stopwatch.StartNew();

        public ScopeImpl(string message, string member, string file, int line)
        {
            _prefix = $"[{System.IO.Path.GetFileName(file)}:{line} - {member}] ";
            Console.WriteLine(_prefix + "ENTER: " + message);
        }

        public void Dispose()
        {
            _sw.Stop();
            Console.WriteLine(_prefix + $"LEAVE: {_sw.ElapsedMilliseconds} ms");
        }
    }
}

class Work
{
    void Process()
    {
        using var _ = PerfLog.Scope("데이터 처리");
        // 작업...
    }
}

5. 제약 사항과 주의점

  • 적용 대상: 선택적 매개변수에만 의미가 있습니다. 호출 시 해당 인수를 생략해야 컴파일러가 값을 넣어줍니다.
  • 타입 제약: MemberName/FilePath/ArgumentExpression은 string, LineNumber는 int 매개변수에 적용합니다.
  • 보안/크기: FilePath는 전체 경로가 포함됩니다. 로그에 그대로 남기면 민감 정보가 노출될 수 있으니 가공하세요.
  • IL에 상수로 주입: 값이 메서드 호출부마다 문자열 상수로 박히므로 바이너리 크기에 영향을 줄 수 있습니다(보통 미미함).
  • 명시적으로 값을 넘기면 특성은 무시됩니다. 필요 시 항상 생략해 자동 주입을 받으세요.
  • [CallerArgumentExpression]은 C# 10 이상에서 사용 가능합니다.

6. 요약

Caller Info 특성은 코드 변경에 강하고(리팩터링 친화적), 성능 오버헤드가 거의 없는 로깅/디버깅 도구입니다. 최소한의 코드로 더 많은 컨텍스트를 로그와 예외에 담아 문제를 빠르게 추적해 보세요.