C++ enum class | "강타입 열거형" 가이드

C++ enum class | "강타입 열거형" 가이드

이 글의 핵심

C++ enum class에 대한 실전 가이드입니다.

enum class란?

enum class (또는 scoped enum)는 C++11에서 도입된 타입 안전한 열거형입니다. 기존 enum의 문제점(암시적 변환, 이름 충돌)을 해결하고, 더 강력한 타입 안전성을 제공합니다.

// 일반 enum (C++03)
enum Color {
    RED,
    GREEN,
    BLUE
};

Color c = RED;  // OK
int x = RED;    // OK (암시적 변환)

// enum class (C++11)
enum class Status {
    SUCCESS,
    ERROR,
    PENDING
};

Status s = Status::SUCCESS;  // OK
// int x = Status::SUCCESS;  // 에러: 암시적 변환 불가
int x = static_cast<int>(Status::SUCCESS);  // 명시적 변환 필요

왜 필요한가?:

  • 타입 안전: 암시적 변환 방지
  • 스코프: 이름 충돌 방지
  • 명확성: Color::RED처럼 명시적 사용
  • 기본 타입 지정: 메모리 절약 가능
// ❌ 일반 enum: 이름 충돌
enum Color { RED, GREEN, BLUE };
enum Status { RED, GREEN };  // 에러: RED 중복

// ✅ enum class: 스코프로 구분
enum class Color { RED, GREEN, BLUE };
enum class Status { RED, GREEN };  // OK

enum vs enum class 비교:

특징enumenum class
암시적 변환✅ 가능❌ 불가
스코프❌ 없음✅ 있음
타입 안전❌ 약함✅ 강함
이름 충돌❌ 발생 가능✅ 방지
사용REDColor::RED

enum class의 장점:

// 1. 타입 안전
enum class Color { RED, GREEN, BLUE };
// int x = Color::RED;  // 에러: 암시적 변환 불가

// 2. 스코프
enum class Color { RED };
enum class Status { RED };  // OK: 다른 스코프

// 3. 명확성
Color c = Color::RED;  // 명시적

// 4. 기본 타입 지정
enum class SmallEnum : uint8_t { A, B, C };  // 1바이트

기본 사용법

enum class Color {
    RED,
    GREEN,
    BLUE
};

Color c = Color::RED;

if (c == Color::RED) {
    cout << "빨강" << endl;
}

// switch문
switch (c) {
    case Color::RED:
        cout << "빨강" << endl;
        break;
    case Color::GREEN:
        cout << "초록" << endl;
        break;
    case Color::BLUE:
        cout << "파랑" << endl;
        break;
}

명시적 값 지정

enum class HttpStatus {
    OK = 200,
    CREATED = 201,
    BAD_REQUEST = 400,
    UNAUTHORIZED = 401,
    NOT_FOUND = 404,
    SERVER_ERROR = 500
};

HttpStatus status = HttpStatus::OK;
int code = static_cast<int>(status);  // 200

기본 타입 지정

// 기본: int
enum class Color : int {
    RED,
    GREEN,
    BLUE
};

// 작은 타입 사용 (메모리 절약)
enum class SmallEnum : uint8_t {
    A,
    B,
    C
};

// 큰 타입 사용
enum class BigEnum : uint64_t {
    LARGE_VALUE = 1000000000000
};

실전 예시

예시 1: 상태 머신

enum class State {
    IDLE,
    RUNNING,
    PAUSED,
    STOPPED
};

class StateMachine {
private:
    State currentState = State::IDLE;
    
public:
    void start() {
        if (currentState == State::IDLE) {
            currentState = State::RUNNING;
            cout << "시작" << endl;
        }
    }
    
    void pause() {
        if (currentState == State::RUNNING) {
            currentState = State::PAUSED;
            cout << "일시정지" << endl;
        }
    }
    
    void resume() {
        if (currentState == State::PAUSED) {
            currentState = State::RUNNING;
            cout << "재개" << endl;
        }
    }
    
    void stop() {
        currentState = State::STOPPED;
        cout << "정지" << endl;
    }
    
    State getState() const {
        return currentState;
    }
};

int main() {
    StateMachine sm;
    sm.start();
    sm.pause();
    sm.resume();
    sm.stop();
}

상태 머신 심화: 전이 테이블과 무효 전이

간단한 if 연쇄도 동작하지만, 상태가 늘어나면 전이 함수를 한곳에 모으는 편이 유지보수에 유리합니다. 아래는 (현재 상태, 이벤트) → 다음 상태를 표로 두는 스케치입니다. 실무에서는 std::optional<State>bool 반환으로 “이 전이는 허용되지 않음”을 표현합니다.

enum class Event { Start, Pause, Resume, Stop };

class StateMachine2 {
    State state_{State::IDLE};

    static bool tryTransition(State from, Event ev, State& out) {
        switch (from) {
        case State::IDLE:
            if (ev == Event::Start) { out = State::RUNNING; return true; }
            return false;
        case State::RUNNING:
            if (ev == Event::Pause) { out = State::PAUSED; return true; }
            if (ev == Event::Stop)  { out = State::STOPPED; return true; }
            return false;
        case State::PAUSED:
            if (ev == Event::Resume) { out = State::RUNNING; return true; }
            if (ev == Event::Stop)   { out = State::STOPPED; return true; }
            return false;
        case State::STOPPED:
            return false;
        }
        return false;
    }

public:
    bool dispatch(Event ev) {
        State next{};
        if (!tryTransition(state_, ev, next)) return false;
        state_ = next;
        return true;
    }
    State state() const { return state_; }
};

이 패턴은 로깅·테스트가 쉽고, 나중에 전이 표를 데이터로 빼거나 그래프 도구로 시각화하기도 좋습니다.

예시 2: 에러 코드

enum class ErrorCode {
    SUCCESS = 0,
    FILE_NOT_FOUND,
    PERMISSION_DENIED,
    NETWORK_ERROR,
    INVALID_INPUT,
    UNKNOWN_ERROR
};

class Result {
private:
    ErrorCode code;
    string message;
    
public:
    Result(ErrorCode c, const string& msg = "") 
        : code(c), message(msg) {}
    
    bool isSuccess() const {
        return code == ErrorCode::SUCCESS;
    }
    
    ErrorCode getCode() const {
        return code;
    }
    
    string getMessage() const {
        switch (code) {
            case ErrorCode::SUCCESS:
                return "성공";
            case ErrorCode::FILE_NOT_FOUND:
                return "파일을 찾을 수 없음: " + message;
            case ErrorCode::PERMISSION_DENIED:
                return "권한 거부: " + message;
            case ErrorCode::NETWORK_ERROR:
                return "네트워크 오류: " + message;
            case ErrorCode::INVALID_INPUT:
                return "잘못된 입력: " + message;
            default:
                return "알 수 없는 오류";
        }
    }
};

Result readFile(const string& filename) {
    ifstream file(filename);
    if (!file) {
        return Result(ErrorCode::FILE_NOT_FOUND, filename);
    }
    
    // 파일 읽기
    return Result(ErrorCode::SUCCESS);
}

int main() {
    Result result = readFile("test.txt");
    
    if (result.isSuccess()) {
        cout << "성공!" << endl;
    } else {
        cout << "에러: " << result.getMessage() << endl;
    }
}

예시 3: 방향

enum class Direction {
    NORTH,
    EAST,
    SOUTH,
    WEST
};

class Player {
private:
    int x = 0, y = 0;
    Direction facing = Direction::NORTH;
    
public:
    void move(Direction dir) {
        switch (dir) {
            case Direction::NORTH:
                y++;
                break;
            case Direction::EAST:
                x++;
                break;
            case Direction::SOUTH:
                y--;
                break;
            case Direction::WEST:
                x--;
                break;
        }
        facing = dir;
    }
    
    void turnLeft() {
        facing = static_cast<Direction>(
            (static_cast<int>(facing) + 3) % 4
        );
    }
    
    void turnRight() {
        facing = static_cast<Direction>(
            (static_cast<int>(facing) + 1) % 4
        );
    }
    
    void print() const {
        cout << "위치: (" << x << ", " << y << ")" << endl;
        cout << "방향: ";
        switch (facing) {
            case Direction::NORTH: cout << "북"; break;
            case Direction::EAST:  cout << "동"; break;
            case Direction::SOUTH: cout << "남"; break;
            case Direction::WEST:  cout << "서"; break;
        }
        cout << endl;
    }
};

int main() {
    Player player;
    player.move(Direction::NORTH);
    player.move(Direction::NORTH);
    player.turnRight();
    player.move(Direction::EAST);
    player.print();
}

예시 4: 로그 레벨

enum class LogLevel {
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
};

class Logger {
private:
    LogLevel minLevel = LogLevel::INFO;
    
public:
    void setLevel(LogLevel level) {
        minLevel = level;
    }
    
    void log(LogLevel level, const string& message) {
        if (level >= minLevel) {
            cout << "[" << levelToString(level) << "] " 
                 << message << endl;
        }
    }
    
    void debug(const string& msg) { log(LogLevel::DEBUG, msg); }
    void info(const string& msg) { log(LogLevel::INFO, msg); }
    void warning(const string& msg) { log(LogLevel::WARNING, msg); }
    void error(const string& msg) { log(LogLevel::ERROR, msg); }
    void fatal(const string& msg) { log(LogLevel::FATAL, msg); }
    
private:
    string levelToString(LogLevel level) const {
        switch (level) {
            case LogLevel::DEBUG:   return "DEBUG";
            case LogLevel::INFO:    return "INFO";
            case LogLevel::WARNING: return "WARNING";
            case LogLevel::ERROR:   return "ERROR";
            case LogLevel::FATAL:   return "FATAL";
            default:                return "UNKNOWN";
        }
    }
};

int main() {
    Logger logger;
    
    logger.debug("디버그 메시지");  // 출력 안됨
    logger.info("정보 메시지");
    logger.warning("경고 메시지");
    logger.error("에러 메시지");
    
    logger.setLevel(LogLevel::DEBUG);
    logger.debug("이제 출력됨");
}

enum class 변환

enum class Color {
    RED,
    GREEN,
    BLUE
};

// enum class -> int
Color c = Color::RED;
int x = static_cast<int>(c);

// int -> enum class
int y = 1;
Color c2 = static_cast<Color>(y);

// 문자열 변환 (헬퍼 함수)
string toString(Color c) {
    switch (c) {
        case Color::RED:   return "RED";
        case Color::GREEN: return "GREEN";
        case Color::BLUE:  return "BLUE";
        default:           return "UNKNOWN";
    }
}

자주 발생하는 문제

문제 1: 암시적 변환

enum class Color {
    RED,
    GREEN,
    BLUE
};

// ❌ 암시적 변환 불가
// int x = Color::RED;

// ✅ 명시적 변환
int x = static_cast<int>(Color::RED);

문제 2: 비교 연산

enum class Color {
    RED,
    GREEN,
    BLUE
};

// ❌ 다른 enum class와 비교 불가
enum class Status {
    OK,
    ERROR
};

// Color c = Color::RED;
// if (c == Status::OK) {}  // 에러

// ✅ 같은 enum class끼리만 비교
Color c = Color::RED;
if (c == Color::RED) {}  // OK

문제 3: 스코프

// ❌ 스코프 없이 사용 불가
enum class Color {
    RED,
    GREEN,
    BLUE
};

// Color c = RED;  // 에러

// ✅ 스코프 필수
Color c = Color::RED;

실무 패턴

패턴 1: 비트 플래그

enum class는 기본적으로 비트 연산을 지원하지 않으므로, 플래그 전용 타입이라면 | & ~ ^를 자유롭게 쓰려면 아래처럼 연산자 오버로딩constexpr 헬퍼를 함께 두는 경우가 많습니다. std::underlying_type_t<E>로 캐스팅하면 의도가 드러납니다.

template<class E>
constexpr auto to_underlying(E e) noexcept {
    return static_cast<std::underlying_type_t<E>>(e);
}

비트 플래그 예시 (기존 패턴 보강)

enum class Permission : uint32_t {
    NONE = 0,
    READ = 1 << 0,   // 1
    WRITE = 1 << 1,  // 2
    EXECUTE = 1 << 2 // 4
};

// 비트 연산자 오버로딩
Permission operator|(Permission lhs, Permission rhs) {
    return static_cast<Permission>(
        static_cast<uint32_t>(lhs) | static_cast<uint32_t>(rhs)
    );
}

Permission operator&(Permission lhs, Permission rhs) {
    return static_cast<Permission>(
        static_cast<uint32_t>(lhs) & static_cast<uint32_t>(rhs)
    );
}

bool hasPermission(Permission flags, Permission check) {
    return (flags & check) == check;
}

// 사용
Permission userPerms = Permission::READ | Permission::WRITE;
if (hasPermission(userPerms, Permission::READ)) {
    std::cout << "읽기 권한 있음\n";
}

enum을 쓰면 암시적으로 정수로 승격되어 실수로 +만 해도 플래그가 꼬일 수 있습니다. enum class정수와 섞이지 않게 막아 주므로, 권한·OpenGL/Vulkan 스타일 비트마스크·파일 open 플래그 등에 적합합니다. 단위 테스트에서는 모든 비트 조합을 다 돌리기보다 대표적인 조합경계(0, all bits) 위주로 검증하면 됩니다.

패턴 2: 문자열 변환 (매크로)

#define ENUM_TO_STRING(EnumType) \
    inline const char* toString(EnumType value) { \
        switch (value) {

#define ENUM_CASE(EnumValue) \
    case EnumValue: return #EnumValue;

#define END_ENUM_TO_STRING \
            default: return "UNKNOWN"; \
        } \
    }

enum class Color { RED, GREEN, BLUE };

ENUM_TO_STRING(Color)
    ENUM_CASE(Color::RED)
    ENUM_CASE(Color::GREEN)
    ENUM_CASE(Color::BLUE)
END_ENUM_TO_STRING

// 사용
std::cout << toString(Color::RED) << '\n';  // "Color::RED"

패턴 3: 범위 기반 순회

enum class Color { RED, GREEN, BLUE, COUNT };

template<typename Enum>
class EnumIterator {
    using value_type = Enum;
    value_type value_;
    
public:
    EnumIterator(value_type value) : value_(value) {}
    
    EnumIterator& operator++() {
        value_ = static_cast<Enum>(static_cast<int>(value_) + 1);
        return *this;
    }
    
    bool operator!=(const EnumIterator& other) const {
        return value_ != other.value_;
    }
    
    Enum operator*() const {
        return value_;
    }
};

template<typename Enum>
class EnumRange {
    Enum begin_;
    Enum end_;
    
public:
    EnumRange(Enum begin, Enum end) : begin_(begin), end_(end) {}
    
    EnumIterator<Enum> begin() const {
        return EnumIterator<Enum>(begin_);
    }
    
    EnumIterator<Enum> end() const {
        return EnumIterator<Enum>(end_);
    }
};

// 사용
for (Color c : EnumRange(Color::RED, Color::COUNT)) {
    std::cout << static_cast<int>(c) << '\n';
}

FAQ

Q1: enum vs enum class?

A:

  • enum: 암시적 변환 가능, 스코프 없음, 이름 충돌 가능
  • enum class: 타입 안전, 스코프 있음, 명시적 변환 필요
// enum: 암시적 변환
enum Color { RED };
int x = RED;  // OK

// enum class: 명시적 변환
enum class Status { RED };
// int x = Status::RED;  // 에러
int x = static_cast<int>(Status::RED);  // OK

권장: 새 코드에서는 enum class 사용

Q2: enum class는 언제 사용해야 하나요?

A:

  • 타입 안전성이 필요할 때: 암시적 변환 방지
  • 이름 충돌을 방지하고 싶을 때: 스코프 제공
  • 명확한 코드를 원할 때: Color::RED처럼 명시적
// 타입 안전
enum class Color { RED };
// if (Color::RED == 0) {}  // 에러

// 이름 충돌 방지
enum class Color { RED };
enum class Status { RED };  // OK

// 명확성
Color c = Color::RED;  // 명시적

Q3: 기본 타입 지정은 어떻게 하나요?

A: : type 문법을 사용합니다. 메모리 절약이나 큰 값 저장에 유용합니다.

// 작은 타입 (메모리 절약)
enum class SmallEnum : uint8_t {
    A, B, C
};  // 1바이트

// 큰 타입 (큰 값 저장)
enum class BigEnum : uint64_t {
    LARGE_VALUE = 1000000000000
};  // 8바이트

// 기본: int (4바이트)
enum class DefaultEnum {
    A, B, C
};

Q4: enum class를 문자열로 변환하려면?

A: switch문이나 map으로 직접 구현해야 합니다. C++에는 내장 기능이 없습니다.

enum class Color { RED, GREEN, BLUE };

// switch문 사용
std::string toString(Color c) {
    switch (c) {
        case Color::RED:   return "RED";
        case Color::GREEN: return "GREEN";
        case Color::BLUE:  return "BLUE";
        default:           return "UNKNOWN";
    }
}

// map 사용
std::map<Color, std::string> colorNames = {
    {Color::RED, "RED"},
    {Color::GREEN, "GREEN"},
    {Color::BLUE, "BLUE"}
};

Q5: enum class로 비트 플래그를 사용할 수 있나요?

A: 가능하지만 비트 연산자를 오버로딩해야 합니다.

enum class Flags : uint32_t {
    NONE = 0,
    FLAG_A = 1 << 0,
    FLAG_B = 1 << 1,
    FLAG_C = 1 << 2
};

Flags operator|(Flags lhs, Flags rhs) {
    return static_cast<Flags>(
        static_cast<uint32_t>(lhs) | static_cast<uint32_t>(rhs)
    );
}

// 사용
Flags flags = Flags::FLAG_A | Flags::FLAG_B;

Q6: enum class의 값을 순회하려면?

A: 범위 기반 for를 직접 구현하거나, 값을 배열에 저장하여 순회합니다.

enum class Color { RED, GREEN, BLUE, COUNT };

// 방법 1: 배열 사용
std::array<Color, 3> colors = {
    Color::RED, Color::GREEN, Color::BLUE
};

for (Color c : colors) {
    // ...
}

// 방법 2: 범위 기반 순회 구현 (위 "실무 패턴 3" 참조)

Q7: enum class 학습 리소스는?

A:

관련 글: Enum Basics, Bit Manipulation.

한 줄 요약: enum class는 타입 안전하고 스코프가 있는 C++11 열거형입니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 범위 기반 for | “Range-based for” 가이드
  • C++ Atomic Operations | “원자적 연산” 가이드
  • C++ explicit Keyword | “explicit 키워드” 가이드

관련 글

  • C++ async & launch |
  • C++ Atomic Operations |
  • C++ Attributes |
  • C++ auto 키워드 |
  • C++ auto 타입 추론 | 복잡한 타입을 컴파일러에 맡기기