Enum

Enum이란 무엇인가

Enum은 열거형Enumeration의 줄임말로, 관련된 상수들의 집합을 정의하는 C#의 특별한 데이터 타입입니다. 특정한 의미를 갖는 값들을 하나의 타입으로 묶어서 관리할 수 있으며 요일, 달의 이름, 색상 등과 같은 그룹화된 상수들을 정의할 때 사용됩니다. 이를 통해 코드의 가독성을 높이고, 오류를 줄이는 데 도움을 줍니다.

Enum 기본 사용법

  • enum은 열거형의 각 항목에 정수 값이 자동으로 할당되며, 첫 번째 항목의 기본값은 0입니다.
public enum MyColor
{
    Red, 
    Green, 
    Blue
}
var color = MyColor.Red;
Console.WriteLine($"ColorID is {(int)color}");
// 출력: ColorID is 0
  • 값은 지정할 수 있으며, 연속적인 값은 생략할 수 있습니다.
public enum MyColor
{
    Red,
    Green, 
    Blue,
    Yellow = 5,
    Orange
}
var color = MyColor.Orange;
Console.WriteLine($"ColorID is {(int)color}");
// 출력: ColorID is 6

Enum의 필요성 및 활용 예시

열거형은 가독성과 유지보수성에서 큰 이점을 제공합니다. 예를 들어, 상태 코드나 특정 옵션을 관리할 때 숫자나 문자열을 사용하는 대신 의미를 갖는 이름을 붙여주면 코드의 의미를 더 쉽게 파악할 수 있고, 잘못된 값을 할당할 수 있는 가능성을 줄여줍니다. 이를 통해 코드 품질을 높이고, 실수를 방지할 수 있습니다.

가독성과 유지보수성성 향상

int color = 1;  // 1은 Red 를 의미한다고 가정합니다.
if (color == 1)
{
    Console.WriteLine("Favorite color is Red");
}

위 코드에서 숫자 1이 Red를 의미하는지 명확하지 않으며, 실수로 잘못된 값을 할당할 가능성이 큽니다.

MyColor color = MyColor.Red;
if (color == MyColor.Red)
{
    Console.WriteLine("Favorite color is Red");
}

열거형을 사용하면 MyColor.Red 와 같이 명확한 이름으로 코드의 의미를 파악할 수 있어 가독성과 유지보수성이 크게 향상됩니다.

안정성 향상

Dictionary<string, int> dicColor = new Dictionary<string, int>
{
    { "Red", 0xFF0000 },
    { "Green", 0x00FF00 },
    { "Blue", 0x0000FF }
};
int colorHexCode = dicColor["Red"];
Console.WriteLine($"Color Hexcode for Red: {colorHexCode}");

위 코드는 키 값으로 문자열을 사용하므로 오타가 발생하거나 잘못된 문자열을 사용할 가능성이 큽니다. 예를 들어, “red"와 같이 잘못된 대소문자를 사용하면 KeyNotFoundException이 발생할 수 있습니다.

Dictionary<MyColor, int> dicColor = new Dictionary<MyColor, int>
{
    { MyColor.Red, 0xFF0000 },
    { MyColor.Green, 0x00FF00 },
    { MyColor.Blue, 0x0000FF }
};
int colorHexCode = dicColor[MyColor.Red];
Console.WriteLine($"Color Hexcode for Red: 0x{colorHexCode:X}");
// 출력: Color Hexcode for Red: 0xFF0000

열거형을 사용하면 키 값으로 잘못된 문자열을 사용할 가능성이 없으므로 코드의 안정성이 크게 향상됩니다.

값의 별칭 지정

열거형을 사용하면 값 자체에 의미 있는 이름을 부여할 수 있습니다. 예를 들어, MyColor에 대한 열거형을 다음과 같이 바꿔 사용할 수 있습니다.

public enum MyColor
{
    Red = 0xFF0000,
    Green = 0x00FF00,
    Blue = 0x0000FF
}
int colorHexCode = (int)MyColor.Red;
Console.WriteLine($"Color Hexcode for Red: 0x{colorHexCode:X}");
//출력: Color Hexcode for Red: 0xFF0000

위 코드처럼 enum 자체에 의미 있는 값을 할당하여 사용할 수 있으므로, 별도의 딕셔너리를 사용하지 않고도 명확하게 값을 관리할 수 있습니다.

Enum의 동작 구조

enum은 C#에서 값 형식Value Type으로 정의된 데이터 타입으로, 열거된 상수들의 집합을 정의합니다. enum의 동작은 다음과 같은 방식으로 이루어집니다.

기본 데이터 타입

  • enum은 기본적으로 int 타입의 상수들로 정의됩니다. 각 열거형 항목은 첫 번째 값이 0부터 시작하여 순차적으로 증가하는 정수 값으로 할당됩니다.
  • enum의 기본 데이터 타입은 변경할 수 있으며, byte, short, long 등 다른 정수형 타입을 사용할 수 있습니다.
public enum MyColor : byte
{
   Red,
   Green,
   Blue
}

컴파일 시 처리 방식

  • enum은 컴파일 시 System.Enum을 상속받는 특수한 값 형식으로 변환되며, 컴파일된 코드에서는 정수 상수로 치환되어 동작합니다.

타입 안정성

  • enum은 타입 안정성을 제공합니다. 열거형 값은 열거형 타입으로만 사용할 수 있기 때문에, 잘못된 값을 할당하거나 사용할 가능성이 줄어듭니다.
  • 예를 들어, MyColor 타입의 변수에 정의되지 않은 임의의 숫자 값을 할당하면 경고가 발생합니다.
MyColor color = (MyColor)10;  
// 유효하지 않은 값 할당 시 경고 발생

박싱과 언박싱

  • enum은 값 형식이지만, System.Enum을 상속하기 때문에 필요에 따라 참조 형식으로 변환Boxing될 수 있습니다.
object boxedColor = MyColor.Red;  // 박싱 발생
MyColor unboxedColor = (MyColor)boxedColor;  // 언박싱 발생

Enum Method 활용

다음의 기능들은 Enum 을 더욱 유용하게 사용할 수 있도록 돕지만, 문자열 변환 기능을 제외한 대부분의 기능들이 리플렉션Reflection을 사용하여 열거형에 대한 메타데이터를 조회하는 방식으로 동작하기 때문에 성능 비용이 높습니다. 따라서 성능이 중요한 상황에서는 이러한 메서드의 사용을 최소화하거나, 필요한 값을 미리 캐싱하여 사용하는 방식으로 성능을 최적화해야 합니다.

열거형과 문자열 변환

열거형 값을 문자열로 변환하려면 ToString() 메서드를 사용하면 됩니다.

string colorName = MyColor.Red.ToString();

문자열을 열거형으로 변환할 때는 Enum.Parse 또는 Enum.TryParse를 사용합니다.

var color = (MyColor)Enum.Parse(typeof(MyColor), "Red");
  • Enum.Parse는 간단하지만 안전성이 떨어지고 예외가 발생할 수 있습니다.
if (Enum.TryParse("Red", out MyColor color))
{
    Console.WriteLine(color);  
    // 출력: Red
}
// 대소문자 무시
if (Enum.TryParse("green", ignoreCase: true, out MyColor color))
{
    Console.WriteLine(color);  
    // 출력: Green
}
  • Enum.TryParse는 안전하고 성능상 이점이 있지만 약간의 추가 코드가 필요합니다.
  • 성능과 안전성을 고려한다면, Enum.TryParse를 사용하는 것이 좋습니다.

IsDefined

특정 값이 열거형에 정의되어 있는지 확인하려면 Enum.IsDefined() 메서드를 사용할 수 있습니다.

bool isDefined = Enum.IsDefined(typeof(MyColor), 0xFF0000);
Console.WriteLine(isDefined);  // 출력: True

GetNames

Enum.GetNames()를 사용하여 열거형에 정의된 모든 항목의 이름을 가져올 수 있습니다.

foreach (var colorName in Enum.GetNames(typeof(MyColor)))
{
	Console.WriteLine(colorName);
	// 출력:
	// Red
	// Green
	// Blue
}

GetValues

Enum.GetValues()를 사용하여 열거형에 정의된 모든 항목의 값을 가져올 수 있습니다.

foreach (var colorHexCode in Enum.GetValues(typeof(MyColor)))
{
    Console.WriteLine((int)colorHexCode);
    // 출력:
    // 16711680
    // 65280
    // 255    
}

TryFormat

.NET 5 이상에서는 TryFormat 메서드를 사용해 열거형 값을 문자열로 변환할 수 있습니다. 이 방법은 GC 부담을 줄이고 성능을 향상 시킬 수 있습니다.

Span<char> destination = stackalloc char[20];
if (MyColor.Red.TryFormat(destination, out int charsWritten))
{
    Console.WriteLine(destination.Slice(0, charsWritten).ToString());  
    // 출력: Red
}
  • TryFormat()Span<char>와 함께 사용되어, 문자열 포맷팅 과정에서 불필요한 메모리 할당을 줄입니다. 자세한 내용은 Span 에서 확인할 수 있습니다.
  • 성공 시 true를 반환하며, destination에 포맷팅된 문자열이 기록됩니다.

Enum Property 활용

Flags

[Flags] 특성을 이용해 열거형 값을 조합하여 비트 플래그로 사용할 수 있습니다. 여러 값이 조합될 수 있는 경우 유용합니다.

[Flags]
public enum FileAccess
{
	None = 0,
    Read = 1,
    Write = 2,
    Execute = 4,
    All = 0xF
}
FileAccess access = FileAccess.Read | FileAccess.Write;
if (access.HasFlag(FileAccess.Write))
{
    Console.WriteLine("Write access is granted.");
}
access &= ~FileAccess.Write; // Write 권한 제거
Console.WriteLine(access); 
// 출력: Read
  • 비트 값들이 겹치지 않도록 신경 써야 합니다. 값들이 겹칠 경우 의도치 않은 결과가 발생할 수 있으므로 각 항목에 2의 제곱 값을 지정하는 것이 좋습니다.
  • 비트 연산으로 결합하거나 분리할 수 있어, 플래그 또는 옵션의 조합을 표현할 때 유용합니다.
  • None, All 등의 값을 통해 모든 옵션을 키거나 끌 수 있습니다. 다음과 같이 작성하면 중복 방지에 유리합니다.
[Flags]
public enum FileAccess
{
	None = 0,
    Read = 1 << 0,
    Write = 1 << 1,
    Execute = 1 << 2,
    Delete = 1 << 3,
    All = Read | Write | Execute | Delete
}
  • 각 값은 비트 위치를 나타내며, 1 << n 표현을 사용하여 겹치지 않는 값을 할당합니다.
  • All은 모든 권한을 포함하는 값으로, 각 권한을 비트 OR 연산으로 결합하여 정의합니다.

Description

System.ComponentModel 네임스페이스의 [Description] 속성을 사용해 열거형 값에 대한 설명을 추가할 수 있습니다. 주로 UI에 표시할 문자열을 지정할 때 유용합니다.

using System.ComponentModel;
public enum MyColor
{
    [Description("Red stands for energy and urgency")] 
    Red,
    [Description("Green symbolizes nature and calm")] 
    Green,
    [Description("Blue represents trust and stability")] 
    Blue
}
public static class EnumExtensions
{
    public static string GetDescription(this Enum value)
{
    var field = value.GetType().GetField(value.ToString());
    if (field == null) return value.ToString();
    
    if (Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) is DescriptionAttribute attribute)
    {
        return attribute.Description;
    }
    return value.ToString();
}
}
var color = MyColor.Red;
Console.WriteLine(color.GetDescription());  
// 출력: Red stands for energy and urgency
  • [Description] 속성은 성능 비용이 높습니다. 반복적으로 사용될 경우 Dictionary를 이용한 캐싱을 활용하거나 다음과 같은 대체 방식을 사용하는 것이 좋습니다.
public static string GetDescription(this MyColor color)
{
    return color switch
    {
        MyColor.Red => "Red stands for energy and urgency",
        MyColor.Green => "Green symbolizes nature and calm",
        MyColor.Blue => "Blue represents trust and stability",
        _ => "Unknown"
    };
}
Console.WriteLine(MyColor.Red.GetDescription());  
// 출력: Red stands for energy and urgency

Obsolete

[Obsolete] 속성은 열거형 값이 더 이상 사용되지 않음을 표시할 때 유용합니다. 코드에서 해당 값을 사용하면 컴파일러 경고를 표시하여 개발자에게 알려줍니다.

public enum MyColor
{
    Red,
    [Obsolete("Use YellowGreen instead")]
    Green,    
    Blue,
    YellowGreen,
}
var color = MyColor.Green;
// 경고: `Program.MyColor.Green` is obsolete : `Use YellowGreen instead`

위 코드는 Green 값을 사용하지 않도록 권고하며, 대체 값으로 YellowGreen을 사용하도록 유도합니다.

Browsable

[Browsable(false)] 속성을 사용하여 특정 열거형 값이 속성 창에 표시되지 않도록 할 수 있습니다. 주로 UI 설계 시 사용됩니다.

using System.ComponentModel;
public enum MyColor
{
    Red,
    Green,
    [Browsable(false)] 
    Blue    
}

위 코드에서 Blue 는 더 이상 속성 창에 표시되지 않습니다.

EnumMember

[EnumMember] 속성은 DataContract와 함께 사용할 때 유용하며, 열거형 값이 직렬화될 때의 이름을 지정할 수 있습니다.

using System.Runtime.Serialization;
[DataContract]
public enum MyColor
{
    [EnumMember(Value = "RedColor")]
    Red,
    [EnumMember(Value = "GreenColor")]
    Green,
    [EnumMember(Value = "BlueColor")]
    Blue,
}

위 코드는 RedGreen 값이 직렬화될 때 각각 “RedColor"과 “GreenColor"로 직렬화되도록 설정합니다.

캐싱을 통한 성능 최적화

IsDefined(), Description 등 리플렉션 기반의 메서드나 속성을 자주 사용하는 경우, 캐싱을 활용하여 성능 비용을 낮출 수 있습니다. 다음은 캐싱을 이용해 Description 의 성능 비용을 낮추는 예제입니다.

using System;
using System.Collections.Generic;
using System.ComponentModel;
public enum MyColor
{
    [Description("Red stands for energy and urgency")]
    Red,
    [Description("Green symbolizes nature and calm")]
    Green,
    [Description("Blue represents trust and stability")]
    Blue
}
public static class EnumExtensions
{
    private static readonly Dictionary<Enum, string> _descriptionCache = new();
    public static string GetDescription(this Enum value)
    {
        if (!_descriptionCache.TryGetValue(value, out string description))
        {
            var field = value.GetType().GetField(value.ToString());
            if (field != null && Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) is DescriptionAttribute attribute)
            {
                description = attribute.Description;
            }
            else
            {
                description = value.ToString();
            }
            // 캐싱하여 이후 동일한 값 요청 시 속도 개선
            _descriptionCache[value] = description;
        }
        return description;
    }
}
public class Program
{
    public static void Main()
    {
        Console.WriteLine(MyColor.Red.GetDescription());  
        // 출력: Red stands for energy and urgency        
    }
}
  • Lazy Caching: _descriptionCache 딕셔너리에 값이 없을 때만 새로 조회하고 캐싱합니다. 이후 요청은 캐시에서 값을 가져오므로 성능이 향상됩니다.
  • 필요한 시점에만 캐싱: 초기화 시 모든 열거형 값을 미리 캐싱하지 않고, 실제로 필요할 때 값을 가져오고 저장하는 방식으로 메모리 사용을 최적화합니다.
  • 런타임 동작 속도가 중요한 경우, 프로그램 시작 시점에 모든 값을 미리 캐싱하는 방식 Eager Caching 을 사용할 수 있습니다.

다형성 구현

  • 열거형은 기본적으로 IConvertible 인터페이스를 구현하고 있어 다른 데이터 타입으로 변환하는 것이 가능합니다.
  • IConvertible을 사용하면 여러 열거형을 하나의 공통된 방식으로 처리할 수 있으며, 다형성을 구현할 때 유용합니다. 예를 들어, 다양한 열거형 타입을 일반화된 방식으로 변환할 수 있습니다.
public enum MyColor
{
    Red,
    Green,
    Blue
}
public enum OrderStatus
{
    Pending = 0,
    Shipped = 1,
    Delivered = 2
}
public static void PrintEnumValue(IConvertible enumValue)
{
    // 공통적인 방법으로 모든 열거형의 값을 정수로 변환하여 출력
    int intValue = enumValue.ToInt32(null);
    Console.WriteLine($"Enum Value as integer: {intValue}");
    // 공통적으로 열거형의 이름을 출력
    string name = enumValue.ToString();
    Console.WriteLine($"Enum Name: {name}");
}
public static void Main()
{
    MyColor color = MyColor.Green;
    OrderStatus status = OrderStatus.Shipped;
    PrintEnumValue(color);
    // 출력:
    // Enum Value as integer: 2
    // Enum Name: Green
    PrintEnumValue(status);
    // 출력:
    // Enum Value as integer: 1
    // Enum Name: Shipped
}

사용자 정의 속성

열거형에 [Description] 속성처럼 사용자 정의 속성을 추가하여 다양한 메타데이터를 정의할 수 있습니다. 이를 통해 열거형의 각 항목에 추가적인 정보를 부여하여 보다 유연한 프로그래밍을 할 수 있습니다.

  • 예시: 사용자 정의 속성을 사용해 카테고리를 지정하고, 어떤 카테고리에 속하는지 확인할 수 있습니다.
using System;
public enum ColorCategory
{
    Warm,
    Cool
}
public enum MyColor
{
    [Category(ColorCategory.Warm)] Red,
    [Category(ColorCategory.Cool)] Green,
    [Category(ColorCategory.Cool)] Blue
}
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
sealed class CategoryAttribute : Attribute
{
    public ColorCategory Category { get; }
    public CategoryAttribute(ColorCategory category) => Category = category;
}
public static class EnumExtensions
{
    public static bool IsCategory(this MyColor color, ColorCategory category)
    {
        var field = color.GetType().GetField(color.ToString());
        if (field != null && Attribute.GetCustomAttribute(field, typeof(CategoryAttribute)) is CategoryAttribute attribute)
        {
            return attribute.Category == category;
        }
        return false;
    }
}
public static void Main()
{
	var color = MyColor.Red;
	if (color.IsCategory(ColorCategory.Warm))
	{
		Console.WriteLine($"{color} is a warm color.");
	}
	// 출력: Red is a warm color.
}
  • CategoryAttribute는 열거형 속성(ColorCategory)을 받을 수 있도록 정의되어 있습니다.
  • MyColor의 각 항목에 ColorCategory 값으로 따뜻한 색상(Warm) 또는 차가운 색상(Cool)으로 지정했습니다.
  • 이 속성을 통해 값이 어떤 카테고리에 속하는지 쉽게 조회할 수 있습니다.

디자인 패턴 활용

열거형은 다양한 디자인 패턴에서 유용하게 사용됩니다. 특히 상태 패턴이나 전략 패턴에서 열거형을 사용하여 상태나 전략의 식별자로 활용할 수 있습니다.

State Pattern

Strategy Pattern

맺음말

열거형은 코드의 가독성을 높이고 오류 가능성을 줄이는 유용한 기능입니다. enum의 기본 사용법과 다양한 기능들을 잘 이해하고 활용하면 더 나은 품질의 코드를 작성할 수 있습니다. 특히 확장 메서드와 최적화 전략을 활용하면 유지보수성을 높이고 성능 저하 없이 코드의 품질을 유지할 수 있습니다.