
로그를 남길 때마다 Log($"MyMethod: 작업 시작됨!")처럼
현재 메소드 이름을 직접 문자열로 쓰는 거, 귀찮지 않으셨나요?
혹은 버그를 추적할 때 "이 메소드는 어디서 호출된 거야?"라며 막막했던 경험은요?
C#에는 이런 반복 작업을 마법처럼 해결해 주는 아주 똑똑한 기능이 있습니다.
바로 호출자 정보 애트리뷰트(Caller Information Attributes)입니다.
호출자 정보 애트리뷰트는 메소드가 호출될 때, 해당 메소드를 호출한
소스 코드의 정보를 컴파일러가 자동으로 채워 넣어주는 특별한 애트리뷰트입니다.
중요한 점은 이 모든 과정이 런타임이 아닌 컴파일 타임에 일어난다는 것입니다.
컴파일러는 코드를 분석합니다. "Main메소드 15번째 줄에서 Log를 호출했네?"
그러면 Log메소드의 매개변수에 'Main', 'C:\...\Program.cs', '15'를 넣어주는 것이죠.
C#에서 제공하는 대표적인 호출자 정보 애트리뷰트는 세 가지가 있습니다.
[CallerMemberName]: 메소드를 호출한 메소드 또는 속성의 이름을 제공합니다.[CallerFilePath]: 메소드를 호출한 소스 코드 파일의 전체 경로를 제공합니다.[CallerLineNumber]: 메소드를 호출한 소스 코드의 줄 번호를 제공합니다.이 애트리뷰트들은 메소드의 선택적 매개변수(Optional Parameter)에 적용해야 합니다.
즉, 매개변수에 기본값을 할당해줘야 컴파일러가 그 자리에 값을 채워 넣을 수 있습니다.
간단한 로깅 유틸리티를 코드로 직접 만들면서 확인해 볼까요?
[코드]
using System;
using System.Runtime.CompilerServices; // 호출자 정보 애트리뷰트를 사용하려면 필요
public static class Logger
{
public static void Log(
string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
Console.WriteLine($"[로그] 메시지: {message}");
Console.WriteLine($"호출 위치: {memberName}");
Console.WriteLine($"파일: {filePath}");
Console.WriteLine($"줄 번호: {lineNumber}");
Console.WriteLine();
}
}
class Program
{
static void Main()
{
Console.WriteLine("프로그램 시작!\n");
Logger.Log("첫 번째 작업을 시작합니다.");
DoSomething();
}
static void DoSomething()
{
Logger.Log("두 번째 작업을 처리 중입니다.");
}
}
[실행 결과]
프로그램 시작!
[로그] 메시지: 첫 번째 작업을 시작합니다.
호출 위치: Main
파일: C:\12주차\호출자 정보 애트리뷰트\Program.cs
줄 번호: 25
[로그] 메시지: 두 번째 작업을 처리 중입니다.
호출 위치: DoSomething
파일: C:\12주차\호출자 정보 애트리뷰트\Program.cs
줄 번호: 32
보이시나요? 우리는 Logger.Log()를 호출할 때 메시지 하나만 전달했습니다.
하지만 컴파일러가 나머지 매개변수인 memberName, filePath, lineNumber를
호출한 위치의 정보로 알아서 채워준 덕분에 상세한 로그를 남길 수 있게 되었습니다!
호출자 정보 애트리뷰트가 빛을 발하는 또 다른 대표적인 예는
WPF, MAUI, Xamarin 등 XAML 기반 UI 프레임워크에서
자주 사용하는 INotifyPropertyChanged인터페이스입니다.
using System;
using System.ComponentModel;
// 속성 변경 알림을 위한 INotifyPropertyChanged 인터페이스를 구현합니다.
public class Person : INotifyPropertyChanged
{
// PropertyChanged 이벤트를 정의합니다. UI 컨트롤 등이 이 이벤트를 구독합니다.
public event PropertyChangedEventHandler? PropertyChanged;
private string _name = "";
public string Name
{
get => _name;
set
{
if (_name != value) // 값이 실제로 변경되었을 때만 알림을 보냅니다.
{
_name = value;
// 속성 값이 변경될 때 UI 등에 알려주기 위해 이벤트를 발생시킵니다.
// 속성의 이름을 문자열 "Name"으로 직접 입력!
OnPropertyChanged("Name");
}
}
}
// PropertyChanged 이벤트를 발생시키는 도우미 메서드입니다.
protected void OnPropertyChanged(string propertyName)
{
// 이벤트 핸들러가 등록되어 있는지 확인 후 이벤트를 호출합니다.
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
Console.WriteLine($"-> OnPropertyChanged 호출: '{propertyName}' 속성 변경 알림!");
}
}
이 방식의 가장 큰 문제는 Name속성의 이름을 UserName으로 바꾸면
OnPropertyChanged("Name")부분은 자동으로 수정되지 않으며
최악의 경우, UI가 갱신되지 않는 등 찾기 어려운 버그의 원인이 됩니다.
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
// 속성 변경 알림을 위한 INotifyPropertyChanged 인터페이스를 구현합니다.
public class Person : INotifyPropertyChanged
{
// PropertyChanged 이벤트를 정의합니다. UI 컨트롤 등이 이 이벤트를 구독합니다.
public event PropertyChangedEventHandler? PropertyChanged;
private string _name = "";
public string UserName
{
get => _name;
set
{
if (_name != value) // 값이 실제로 변경되었을 때만 알림을 보냅니다.
{
_name = value;
// 속성 값이 변경될 때 UI 등에 알려주기 위해 이벤트를 발생시킵니다.
// 속성의 이름을 문자열 "Name"으로 직접 입력!
OnPropertyChanged(); // 아무것도 넘기지 않아도 컴파일러가 "Name"을 채워줌!
}
}
}
// PropertyChanged 이벤트를 발생시키는 도우미 메서드입니다.
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
// 이벤트 핸들러가 등록되어 있는지 확인 후 이벤트를 호출합니다.
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
Console.WriteLine($"-> OnPropertyChanged 호출: '{propertyName}' 속성 변경 알림!");
}
}
class Program
{
static void Main()
{
// Person 객체 생성
var person = new Person();
// PropertyChanged 이벤트 구독
person.PropertyChanged += PropertyChanged;
// UserName 속성 변경 (이벤트 발생)
Console.WriteLine("UserName을 '홍길동'으로 변경합니다.");
person.UserName = "홍길동";
// UserName 속성 동일 값으로 변경 (이벤트 미발생)
Console.WriteLine("UserName을 '홍길동'으로 다시 변경합니다.");
person.UserName = "홍길동";
// UserName 속성 다른 값으로 변경 (이벤트 발생)
Console.WriteLine("UserName을 '이순신'으로 변경합니다.");
person.UserName = "이순신";
}
// PropertyChanged 이벤트가 발생하면 호출되는 이벤트 핸들러 메서드
private static void PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
// 변경된 속성의 이름을 콘솔에 출력합니다.
Console.WriteLine($"이벤트: '{e.PropertyName}' 속성이 변경되었음을 감지했습니다!");
}
}
[실행 결과]
UserName을 '홍길동'으로 변경합니다.
이벤트: 'UserName' 속성이 변경되었음을 감지했습니다!
-> OnPropertyChanged 호출: 'UserName' 속성 변경 알림!
UserName을 '홍길동'으로 다시 변경합니다.
UserName을 '이순신'으로 변경합니다.
이벤트: 'UserName' 속성이 변경되었음을 감지했습니다!
-> OnPropertyChanged 호출: 'UserName' 속성 변경 알림!
이제 OnPropertyChanged()를 호출할 때 아무 인자도 넘기지 않으면,
컴파일러가 Name속성의 이름을 자동으로 propertyName매개변수에 넣어줍니다.
덕분에 리팩토링에 안전하고 깔끔한 코드가 완성되었습니다!
디버깅과 로깅, 유지보수의 품질을 높여주는 호출자 정보 애트리뷰트에 대해 알아보았습니다. 컴파일러가 개발자의 작업을 대신해 주는 덕분에 우리는 더 안전한 코드를 작성할 수 있습니다.