본문 바로가기

C#

C# 플래그 Enum과 비트 필드 활용 패턴

플래그 Enum은 다수의 옵션을 비트 단위로 조합해 표현하는 데 유용합니다. 성능과 메모리를 아끼면서 가독성도 유지할 수 있어 권한, 상태, 기능 토글 등에 많이 사용합니다. 핵심은 [Flags]와 2의 거듭제곱 값, 그리고 안전한 비트 연산 패턴입니다.

1. [Flags] 기본과 값 설계

각 항목은 2의 거듭제곱으로 지정하고, None=0을 반드시 포함합니다. All은 정의된 플래그들을 OR로 묶어 명시적으로 구성합니다.

using System;

[Flags]
public enum FilePermission : ushort
{
    None    = 0,
    Read    = 1 << 0,
    Write   = 1 << 1,
    Execute = 1 << 2,
    Delete  = 1 << 3,
    All     = Read | Write | Execute | Delete
}

팁: All을 ~0으로 만들기보다 실제 정의된 플래그의 조합으로 두면 향후 검증과 유지보수가 쉬워집니다. 부호 문제를 피하려면 ushort, uint 같은 부호 없는 기반 형식을 고려합니다.

2. 자주 쓰는 연산 패턴(Set/Clear/Toggle/Check)

HasFlag는 간단하지만 상대적으로 느릴 수 있습니다. 확장 메서드로 자주 쓰는 연산을 묶어 두면 안전하고 빠릅니다.

public static class FilePermissionExtensions
{
    public static bool IsAnySet(this FilePermission value, FilePermission flags)
        => (value & flags) != 0;

    public static bool IsAllSet(this FilePermission value, FilePermission flags)
        => (value & flags) == flags;

    public static FilePermission Set(this FilePermission value, FilePermission flags)
        => value | flags;

    public static FilePermission Clear(this FilePermission value, FilePermission flags)
        => value & ~flags;

    public static FilePermission Toggle(this FilePermission value, FilePermission flags)
        => value ^ flags;

    public static bool IsValid(this FilePermission value)
        => (value & ~FilePermission.All) == 0;

    public static FilePermission Sanitize(this FilePermission value)
        => value & FilePermission.All;
}

// 사용 예
var p = FilePermission.Read.Set(FilePermission.Write);            // Read | Write
bool canRW = p.IsAllSet(FilePermission.Read | FilePermission.Write); // true
p = p.Clear(FilePermission.Execute);                               // Execute 제거
p = p.Toggle(FilePermission.Delete);                               // Delete 토글

3. HasFlag vs 비트 연산 성능

HasFlag는 가독성이 좋지만, (value & flag) != 0 같은 비트 연산이 일반적으로 더 빠릅니다. 성능이 중요한 경로에서는 비트 연산을 권장합니다.

bool ok1 = p.HasFlag(FilePermission.Read);              // 간단하지만 느릴 수 있음
bool ok2 = (p & FilePermission.Read) != 0;             // 권장: 박싱 없이 빠름
bool all = (p & (FilePermission.Read | FilePermission.Write))
           == (FilePermission.Read | FilePermission.Write);

4. 유효성 검사와 안전한 병합

정의되지 않은 비트가 섞이지 않도록 검증과 정규화를 수행합니다.

static bool IsValid(FilePermission value)
    => (value & ~FilePermission.All) == 0;

static FilePermission Normalize(FilePermission value)
    => value & FilePermission.All;

입력 수신 시 IsValid로 검증하거나 Normalize로 정리하면 안정성이 올라갑니다.

5. 비트 필드로 값 압축하기(패킹/언패킹)

여러 작은 정수/불리언을 하나의 정수에 담아 저장/전송 비용을 절감하는 패턴입니다.

public static class PackedSettings
{
    private const int LevelBits = 3;   // 0..7
    private const int ActiveBits = 1;  // 0..1
    private const int RegionBits = 4;  // 0..15

    private const int LevelShift = 0;
    private const int ActiveShift = LevelShift + LevelBits;     // 3
    private const int RegionShift = ActiveShift + ActiveBits;   // 4

    private const int LevelMask = (1 << LevelBits) - 1;   // 0b111
    private const int ActiveMask = (1 << ActiveBits) - 1; // 0b1
    private const int RegionMask = (1 << RegionBits) - 1; // 0b1111

    public static int Pack(int level, bool active, int region)
    {
        level = level & LevelMask;
        int a = active ? 1 : 0;
        region = region & RegionMask;

        return (level << LevelShift)
             | (a << ActiveShift)
             | (region << RegionShift);
    }

    public static int GetLevel(int value) => (value >> LevelShift) & LevelMask;
    public static bool GetActive(int value) => ((value >> ActiveShift) & ActiveMask) != 0;
    public static int GetRegion(int value) => (value >> RegionShift) & RegionMask;
}

// 사용 예
int packed = PackedSettings.Pack(5, true, 9);
int level = PackedSettings.GetLevel(packed);   // 5
bool active = PackedSettings.GetActive(packed); // true
int region = PackedSettings.GetRegion(packed);  // 9

6. 직렬화/DB 저장 팁

System.Text.Json에서 문자열로 플래그 이름을 쓰려면 컨버터를 추가합니다. 플래그 조합은 "Read, Write"처럼 직렬화됩니다. EF Core는 기본적으로 숫자로 저장합니다.

using System.Text.Json;
using System.Text.Json.Serialization;

[Flags]
public enum Permission { None = 0, Read = 1, Write = 2, Execute = 4 }

public record Role(string Name, Permission Permissions);

var role = new Role("dev", Permission.Read | Permission.Write);
var options = new JsonSerializerOptions { WriteIndented = true };
options.Converters.Add(new JsonStringEnumConverter());
string json = JsonSerializer.Serialize(role, options);
// { "Name": "dev", "Permissions": "Read, Write" }

// EF Core 예시
public class User
{
    public int Id { get; set; }
    public Permission Permissions { get; set; } // DB에 정수로 저장됨
}

7. 플래그가 맞지 않은 경우

상호 배타적인 상태(예: Draft/Published/Archived)는 일반 Enum이 적합합니다. 플래그는 여러 상태가 동시에 의미 있게 켜질 수 있을 때만 사용합니다.

8. 빠른 동작 확인

간단한 테스트로 동작을 점검합니다.

static void Main()
{
    FilePermission p = FilePermission.Read | FilePermission.Write;
    Console.WriteLine(p); // Read, Write
    Console.WriteLine(p.IsAllSet(FilePermission.Read | FilePermission.Write)); // True
    Console.WriteLine((p & FilePermission.Execute) != 0); // False
}

정리: 플래그 Enum은 올바른 값 설계, 안전한 비트 연산, 유효성 검사, 직렬화 전략을 갖추면 강력한 도구가 됩니다. 빈번한 경로에서는 HasFlag 대신 비트 연산을 사용하고, 비트 필드 패킹은 공간 효율이 중요할 때만 적용합니다.