
C# 애트리뷰트와 리플렉션의 세계를 탐험하는 마지막 여정입니다.
이번에는 우리가 직접 우리만의 규칙을 담은 애트리뷰트를 만들고, 리플렉션을 이용해
이 규칙을 검사하는 유효성 검사(Validation) 프레임워크를 흉내 내보겠습니다.
우리가 만들 최종 목표는 다음과 같습니다.
[StringLength]애트리뷰트를 만든다.[Range]애트리뷰트를 만든다.Validator클래스를 만든다.먼저, 우리만의 규칙을 담을 애트리뷰트 클래스들을 정의해 보겠습니다.
애트리뷰트는 System.Attribute를 상속받는 클래스라는 점, 기억하시죠?
using System;
using System.Reflection;
// 이 애트리뷰트는 프로퍼티에만 적용할 수 있도록 제한합니다.
[AttributeUsage(AttributeTargets.Property)]
public class StringLengthAttribute : Attribute
{
public int MaxLength { get; }
public StringLengthAttribute(int maxLength)
{
MaxLength = maxLength;
}
}
// 이 애트리뷰트도 프로퍼티에만 적용합니다.
[AttributeUsage(AttributeTargets.Property)]
public class RangeAttribute : Attribute
{
public int Min { get; }
public int Max { get; }
public RangeAttribute(int min, int max)
{
Min = min;
Max = max;
}
}
이제 우리의 규칙을 담은 '꼬리표'들이 준비되었습니다. 간단하죠?
생성자를 통해 규칙(최대 길이, 최소/최대 범위)을 받고,
나중에 검사기가 읽어갈 수 있도록 공개 속성(Property)으로 값을 저장해 둡니다.
이제 이 꼬리표들을 유효성 검사를 할 모델 클래스에 붙여봅시다.
회원가입 폼을 나타내는 RegisterForm클래스를 예로 들어볼게요.
public class RegisterForm
{
[StringLength(10)]
public string Username { get; set; }
[StringLength(20)]
public string Password { get; set; }
[Range(1, 150)]
public int Age { get; set; }
}
코드가 정말 깔끔하고 선언적으로 바뀌었습니다!
Username은 10자를 넘으면 안 되고, Age는 1에서 150 사이여야 한다는
규칙이 코드만 봐도 명확하게 보입니다. 이것이 바로 애트리뷰트의 힘이죠.
꼬리표를 붙이는 것만으로는 아무 일도 일어나지 않습니다.
이 꼬리표를 읽고 실제로 규칙을 검사하는 '리플렉션'이 필요합니다.
public static class Validator
{
public static bool Validate(object obj)
{
// 1. 객체의 Type 정보를 가져옵니다.
Type type = obj.GetType();
// 2. 해당 Type의 모든 프로퍼티를 순회합니다.
foreach (PropertyInfo property in type.GetProperties())
{
// 3. 각 프로퍼티에 적용된 모든 애트리뷰트를 가져옵니다.
foreach (Attribute attribute in property.GetCustomAttributes())
{
// 4. 애트리뷰트 타입에 따라 분기하여 유효성을 검사합니다.
if (attribute is StringLengthAttribute stringLengthAttr)
{
string value = (string)property.GetValue(obj);
if (value.Length > stringLengthAttr.MaxLength)
{
Console.WriteLine(
$"[유효성 오류] " +
$"{property.Name}의 길이가 너무 깁니다. " +
$"(최대: {stringLengthAttr.MaxLength})");
return false;
}
}
else if (attribute is RangeAttribute rangeAttr)
{
int value = (int)property.GetValue(obj);
if (value < rangeAttr.Min || value > rangeAttr.Max)
{
Console.WriteLine(
$"[유효성 오류] " +
$"{property.Name}의 값이 범위를 벗어났습니다. " +
$"(범위: {rangeAttr.Min}~{rangeAttr.Max})");
return false;
}
}
}
}
Console.WriteLine("[유효성 검사] 모든 항목이 유효합니다.");
return true;
}
}
이제 Main메소드에서 유효한 경우와 유효하지 않은 경우를 모두 테스트해 봅시다.
class Program
{
static void Main()
{
// 경우 1: 유효한 데이터
var validForm = new RegisterForm
{
Username = "guest",
Password = "password123",
Age = 30
};
Console.WriteLine("--- 유효한 폼 검사 시작 ---");
Validator.Validate(validForm);
Console.WriteLine("\n");
// 경우 2: 유효하지 않은 데이터 (사용자 이름이 너무 김)
var invalidForm = new RegisterForm
{
Username = "this_username_is_too_long",
Password = "password123",
Age = 99
};
Console.WriteLine("--- 유효하지 않은 폼 검사 시작 ---");
Validator.Validate(invalidForm);
}
}
[실행 결과]
--- 유효한 폼 검사 시작 ---
[유효성 검사] 모든 항목이 유효합니다.
--- 유효하지 않은 폼 검사 시작 ---
[유효성 오류] Username의 길이가 너무 깁니다. (최대: 10)
우리가 직접 만든 애트리뷰트와 검증기가 완벽하게 동작하는 것을 확인했습니다.
이 원리를 이해한다면, 단순히 프레임워크를 사용하는 것을 넘어,
프레임워크가 어떻게 동작하는지 그 내부를 이해하게 된 것입니다.
이제 여러분은 프로젝트에 필요한 반복적인 로직을 자동화하고,
더 깔끔하고 유연한 코드를 작성할 수 있는 강력한 무기를 손에 넣으신 겁니다.
리플렉션과 애트리뷰트를 활용해서 여러분의 코드를 한 단계 더 발전시켜 보시길 바랍니다.