에러 객체란?

" 에러객체 = 에러코드 + 에러문자열 "

프로그래밍을 하다보면 종종 에러코드를 정의해서 써야하는 경우가 있다. 종종이라기보다는 항상 에러코드를 정의해야한다. C++을 사용하는 나의 경우에는 enum을 주로 이용하여 에러코드가 겹치는 것을 방지하고, 다른 언어를 사용하는 사람들도 각자의 언어에서 제공하는 여러가지 방법을 사용하여 에러 코드를 정의해 쓸 것이다. 이렇게 에러코드들을 주루룩 정의해서 쓰다보면 한가지 아쉬운 점이 꼭 생각난다.

"에러 코드만 넣으면 무슨 에러인지 알수 있는 방법은 없을까?"

오늘 소개해볼 '에러 객체'라는 것은 위의 불편함을 조금이라도 해결해 보기 위해 만들어진 몸부림 중에 하나다.

에러 객체에게 필요한 것들?

중복은 절대 안되!!
에러 코드는 중복되어서는 안된다. 한 가지의 에러를 나타내는데 하나의 코드만 쓰여야하고, 하나의 코드는 하나의 에러만을 가리켜야한다. 그래서 사람들은 enum 같은 곳에 에러 코드들을 정의함으로써 컴파일 타임에 자동적으로 중복되는 에러 코드를 피하려한다. 우리가 필요한 에러 객체에 중복된 코드가 있는 경우엔 컴파일 타임 에러가 발생할 수 있게 해야 한다.

코드만으로 무슨 에러인지 알고 싶어요!!
MS의 에러 코드를 생각 해 보라. 당최 알 수 없는 에러 코드가 떨어진다. 그 코드가 무엇인지 알기위해서는 MSDN을 뒤져 보거나 Error Lookup 툴을 이용하여야만 한다. 물론, 범용성을 제공하기 위해서는 정수형태의 에러코드 말고 다른 형태의 뭔가를 지원한다는 것이 상당히 어렵다. 하지만 나는 내 프로젝트에서 사용할 코드들을 정의하는 것이다. 문자열이 한글이든, 영어든 한가지만 선택하면 되는 것이고, 내가 알 수 있는 내용을 써 넣으면 된다.
지금 나에게 필요한 것은 범용성 보다는 별다른 툴 없이 바로 에러 문자열을 출력할 수 있는 방법이다.

에러 코드와 에러 문자열을 한자리에서 정의 하고 싶어요!!
만일 enum으로 에러 코드를 정의하고 문자열 배열로 에러 문자열들을 정의 하는 것도 괜찮은 방법이라 생각한다. 다만 에러 정의 들이 늘어나면 늘어 날 수록 유지 보수가 힘들어진다(진짜다 직접 해봐라!! 500개가 넘는 문자열 배열 과 에러코드를 동시에 관리하면 머리에 쥐난다).

♥ 어떻게 생겼을까?
위의 요구사항을 토대로 만들어진 코드를 살펴 보자. 장황한 서두와는 달리 실상 코드는 너무나도 간단하게 만들 수 있다.

에러 객체 기본 :
enum XXX_ERRORTYPE
{
     XXX_ERRORTYPE_SUCCESS, 
     XXX_ERRORTYPE_INFO,  
     XXX_ERRORTYPE_WARN,  
     XXX_ERRORTYPE_ERROR  
};

template <int TYPE, int SERVER, int ERRNO> 
struct XXX_ERROR {
     enum {
          ErrorCode = ((TYPE << 8 * 3) | (SERVER << 8 * 2) | ERRNO)
     };
     static const char* Desc;
};

#define XXX_MAKE_ERRORCODE(name, type, server, errno, description) \
const char* XXX_ERROR<type, server, errno>::Desc = #description; \
typedef XXX_ERROR<type, server, errno> name;

// example
XXX_MAKE_ERRORCODE(XXX_ERROR_SOMEERROR,   TZ_ERRORTYPE_ERROR, 1,  1, "XXX 에러.")

그냥 ERROR이라고 하면 다른 뭔가와 충돌이 날것 같아 앞에 XXX를 붙였다. XXX에 큰 의미를 두지는 말자.

먼저 XXX_ERROR 객체를 살펴 보자.
ErrorCode라는 enum을 선언하고 있다. ErrorCode는 값이 아닌 하나의 타입으로 인식되어 컴파일 타임에 결정 된다. TYPE, SERVER, ERRNO 같은게 나오고 이리 저리 시프트가 있어서 복잡해 보이기는 하지만 그것은 개인적으로 에러 코드를 카테고라이징하기 위해 사용하는 것이지 특별한 의미가 있는 것은 아니다. 필요 없다면 ERRNO 하나만으로 써도 무방하다.

Desc는 description의 약자로 static const* 타입으로 선언되어 있다. 꼴에 문자열인지라 아무래도 컴파일 타임에 뭔가를 할 수 없어 대신 런타임에 아무도 내용을 바꾸지 못하도록 const를 사용했다.

XXX_MAKE_ERRORCODE 매크로로 눈을 돌려 보자. 이 매크로는 XXX_ERROR 템플릿 클래스를 찍어내는데 사용된다. 내부에는 static 변수의 초기화와 typedef 외에 다른 일은 하지 않는다. 하지만 한가지 기억할 것이 있다.

"템플릿 클래스는 인자가 다른 경우 다른 타입으로 여겨진다."

위에서 type, server, errno에 다른 경우 동일한 템플릿 클래스로 만들고 있지만 항상 다른 타입으로 여겨진다는 것이다. 만일 위의 세 인자가 같은 경우(혹 하나로 해도 상관은 없다)라면 동일 타입의 static 변수를 두번 초기화하려 한다고 컴파일 에러가 발생한다. 만일 name이 같은 경우 역시 typedef를 두번 하려한다고 컴파일 에러를 발생 시킨다. 어쨋든 중복된 선언을 하게 된다면 컴파일이 안되게 만들었다는 말이다.

사용은 어떻게?
에러 객체라고 하지만 실상 아무런 객체도 생성되지 않는다. 그냥 타입의 enum 상수와 static 변수에 접근하는 것 뿐이다 :

if(XXX_ERROR_SOMEERROR::ErrorCode == SomeFunction())
{
     std::cout << XXX_ERROR_SOMEERROR::Desc << std::endl;
}

위에서 사용된 SomeFunction()은 에러 객체 템플릿 클래스에 대한 어떠한 정보도 없어도 된다. 단순히 정수형태의 에러 코드를 리턴하는 것이면 된다. 리턴하는 것은 에러 객체가 아닌 에러 객체에 정의된 enum을 리턴 한다.

문자열의 출력 역시, 에러 객체에 선언된 static 변수에 접근하여 초기화된 문자열을 출력하는 간단한 형태다.

위와 같은 방식으로 에러 정보를 관리하게 되면 에러 코드 넘버와 에러문자열을 항상 한쌍으로 관리 할 수 있으므로 관리에 용이성과, 컴파일 타임에 중복 체크를 보장 받을 수 있다.

다만 단점으로는 static 변수들의 무수한 생성과 enum 처럼 자동으로 에러 코드를 증가 시켜주는 도구가 없다는 불편함이 있다.



참조 :
 - Modern C++ Design - 안드레 알렉산드레쿠스
 - [진리는어디에] - 템플릿 특화를 이용한 튜플 클래스
 - [진리는어디에] - 템플릿 특화(Template specialize)
 - [진리는어디에] - 템플릿 특화를 이용한 멀티 키 맵(Multi Key Map)
Posted by kukuta

댓글을 달아 주세요

이전에 템플릿 특화(template specialization)특화를 이용한 튜플(Tuple)클래스를 만들어 본적이 있다. 오늘은 비슷한 원리를 이용해 N개의 키를 가질 수 있는 멀티 키 맵을 만들어 보겠다.


멀티키 맵이라고 해서 특별이 다를 것은 없다. 다만 단일 키만을 제공하는 std::map을 좀 더 확장하여 N개의 키를 타입 리스트를 통해 넘겨주는 것 뿐이다. 물론 이런 방법 말고도 구조체를 키로 쓰는 std::map을 활용하거나, 바이너리 메모리로 모든 키들을 복사해서 쓰는등 구현에는 다향한 취향과 트레이드 오프가 존재한다. 오늘 이 포스트에서 소개하는 것은 그 방법들 중 한 가지 방법일 뿐이다.

멀티키 맵에 관련된 설명에 앞서 이전에 다뤘던 Typelist라던지 템플릿 특화의 개념은 이미 이전 포스트에서 익혔다고 가정하고 간단하게 설명하거나 다루지 않고 넘어 가도록 하겠다.

1. 기본 개념은 '상속' - 상속을 통한 코드의 자동 생성

이전의 튜플(Tuple)클래스와 마찬가지로 멀티키 맵도 기본 뼈대는 상속을 통해 만들어 진다. 멀티키 맵의 기본 구조를 보자면 각 키 마다 하위 계층 맵을가지고 있고, 가장 하위 키는 따로 맵을 가지지 않고 바로 값을 가지고 있는 형태다. 한 마디로 계층 구조를 가지는 std::map들을 템플릿을 이용해 자동으로 생성하는 방식이다.

1 : template <class T, class U, class Value>
2 : struct MultiKeyMap<Typelist<T, U>, Value> : private MultiKeyMap<U, Value>
3 : {
4 :    typedef Typelist<T, U> TYPELIST;
5 :    typedef std::map<T, MultiKeyMap<U, Value> > CONTAINER;
6 :    CONTAINER m_mapContainer;
...     .....

위 코드 라인 2번을 보면 여러 개의 키중 한개 키만을 떼어 자식 클래스 내부 std::map의 키 값으로 쓰고 나머지 타입 리스트는 부모 클래스로 올려 버린다. Value는 최상위 클래스의 값으로 저장되어야 하므로 모든 계층구조를 통해 끝까지 올려 보낸다.

위와 같은 상속을 통해 각 키에 해당하는 std::map을 자동으로 생성할 수 있다(이렇게 하지 않았다면 직접 각 계층 구조 마다 std::map을 생성 해 줘야 하는 불편함이 있었을 것이다.)

코드 라인 5번은 멀티키 맵 내부에 값을 저장 할 std::map을 생성한다. 위 코드를 보면 어떻게 계층 구조를 이루는 맵을 생성하는지 알 수 있다.

2. 멀티 키 맵에서의 데이터 접근
데이터의 접근은 위의 개념보다 좀 더 쉽다. 단순히 std::map의 계층 구조를 따라 다 맨 마지막 실제 값에 다다르면 값을 리턴하는 방식이다.

template <class Value>
struct MultiKeyMap<NullType, Value>
{
    Value m_value;
    Value& get(NullType key)
    {
        return m_value;
    }
};

template <class T, class U, class Value>
struct MultiKeyMap<Typelist<T, U>, Value> : private MultiKeyMap<U, Value>
{
    ....
    template <class PList>
    Value& get(PList key)
    {
         MultiKeyMap<U, Value>& mkm = m_mapContainer[key.first];
         return mkm.get(key.second);
    }
    ....
};


3. 멀티 키 맵의 사용
int main() {
    MultiKeyMap<TYPELIST_3(std::string, std::string, int), int> multiKeyMap;
  
    multiKeyMap[PARAMLIST_3("key1", "key2", 0)] = 1000;;
    std::cout << multiKeyMap[PARAMLIST_3("key1", "key2", 0)] << std::endl;
    multiKeyMap.erase(PARAMLIST_3("key1", "key2", 0));

    return 0;
}

멀티 키 맵의 템플릿 인자를 다양화 하기 위해서 Modern C++ Design에서 소개 되고 있는 타입리스트(type list) 를 이용한다. 그리고 타입 리스트에 따라 실제 인자도 다양화하기 위해 PARAMLIST 를 사용하고 있다. 타입리스트와 파라메터 리스트의 개념은 특화를 이용한 튜플(Tuple)클래스 를 참고하면 되겠다.

나머지 상세 코드는 첨부 파일을 참조 바란다.
Posted by kukuta

댓글을 달아 주세요