고수들이 절대 가르쳐 주지 않는 C/C++ 프로그래밍 팁 #6 - 로깅 라이브러리 설계 방향

Posted at 2007. 7. 30. 17:18 // in S/W개발/고절가주팁 // by 김윤수


고절가주팁 여섯번째글입니다. 지난 글에서 몇 가지 질문을 드렸던 걸 기억하실 겁니다. 이번에는 겉으로 잘 드러나지 않는 비기능적 요구사항들을 고려하면서 그 질문에 대해 답변해 보고, 본격적인 설계 작업을 한 번 시작해 보도록 하겠습니다. (처음부터 예상하긴 했지만 역시 이 간단한 로깅 라이브러리를 구현하는 것도 그리 쉬운 일은 아니네요...)

다시 한 번 정리하는 의미에서 요구사항을 나열하면 다음과 같습니다.

1. 어떤 모듈, 어떤 소스 파일의 어느 함수, 어느 위치에서 발생한 정보인지 출력할 것. 주로 디버깅 시에 유용함
2. 로깅 정보가 다양한 목적지로 저장 또는 전송될 수 있을 것
3. 모듈별로 로깅을 활성화 또는 비활성화할 수 있을 것
4. 로깅의 상세한 정도를 조정할 수 있을 것
5. 아주 상세한 디버깅 정보들은 Production 시스템에서는 자동으로 제외될 것
6. 모듈별 로깅 활성화나 로깅의 상세한 정도는 실행시에도 조정할 수 있을 것
7. 원격지로 로깅을 전송할 때 암호화/복호화을 수행할 것(어쩌다가..코더 님)
8. 로그를 기록할 때 날짜와 시간을 기록할 것(추링님)
9. 로그를 날짜별, 시간별로 다른 파일로 기록할 것(추링님)
10. 로그 파일이 지정된 크기를 넘을 경우 자동으로 다른 파일에 기록을 시작할 것(추링님)
11. 반복되는 로깅 정보를 축약해서 기록할 것(추링님)

그리고 제가 질문했던 건 다음과 같습니다.

1. 요구사항 3. 과 관련하여 모듈이라는 걸 어떤 단위로 정의해야 하는 걸까요 ? 로깅 라이브러리를 사용하는 S/W 내에 모듈 구조가 flat 한 구조인 걸 가정해야할까요 ? 아니면 계층적인 구조를 가정해야 할까요 ? 계층적인 구조라면 상위 모듈의 로깅을 활성화/비활성화시키면 그 안에 포함된 모듈에 대한 로깅은 어떻게 작동해야 할까요 ?

2. 로깅의 상세한 정도는 어떻게 나누어야 할까요 ? 그냥 1 ~ 255 정도로 나눌까요 ? 아니면 몇 가지 잘 알려진 수준을 두는 것으로 할까요 ?

3. 암호화/복호화, 로깅 정보 축약, 날짜별/시간별 별도 파일 기록, 일정 크기를 넘을 때 다른 파일로 기록하는 등을 로깅 라이브러리 자체에서 구현하도록 할 까요 ? 아니면 별도의 Extension Mechanism 을 두고 로깅 라이브러리 사용자가 직접 구현하도록 할까요 ?

rein은 이 질문에 대해 다음과 같이 답변해 주시더군요.

1. 모듈이란 단위는 java나 python 처럼 팩키지에 가까운 개념이 있지 않는한 쉽게 끊어서 나타내기는 힘들다고 생각합니다. 차라리 개별 서브 시스템 단위에서 논리적으로(쉽게 말해서 사용자 정의에 떠넘기기) 표현하는 단위를 찾아서 설계하는 수 밖에 없다고 생각합니다 - 물론 이렇게 사용자에게 떠 넘기려면 설정 파일? 형식이 유연해야할 것입니다.

Apache project에서 진행되고있는 로거 프로젝트인 log4j의 C++버젼인 log4cxx에서도 그냥 사용자가 자바 팩키지스러운 포맷으로 모듈을 정의할 수 있게 해 주고 있습니다.
(예를 들어 <logger='subsystem1'/> <logger='subsystem1.module_group1'/> 하는 식으로요)

2. 로깅 수준이 너무 많으면 실제로 사용하는데 어렵지 않을까 라고 생각합니다. 256단계의 구분을 사용자가 기억하고 적용하기는 괴롭지 않을까요 :) 실제로 많은 경우에 로깅 수준은 Trace(or Debug?), Info., Warn., Error, Fatal 정도인 것 같습니다. (PHP나 apache같은 시스템을 놓고 보면)

3. 이 부분은 log sinker 들의 인터페이스를 제공하고, 해당 인터페이스를 구현한 클래스를 추가하기 쉽게 하면 어떨까 합니다. 암호화나 log rotation같은 부분을 기본적으로 제공하기는 너무 라이브러리가 방대해지지 않을까요. 물론 일차(?)적으로 라이브러리를 완성한 후에 붙이기 쉬운 상태에서 계속 붙여가는 형식이라면 가능할 듯도 합니다

저도 rein님과 거의 같은 생각을 하고 있었습니다. 그런데, 이렇게 바로 결론을 내기 보다는 왜 그렇게 결론을 내리게 됐는지를 확실하게 밝히는 것이 제가 설계/구현하게 될 로깅 라이브러리의 사용 용도 또는 사용 가능성을 명확하게 해 줄 것입니다.

로깅 라이브러리를 설계/구현 함에 있어 저의 Design Goal은 다음과 같습니다. 제 나름의 우선 순위를 두고 나열해 봤습니다.

1. 로깅 라이브러리가 최대한 다양한 환경에서 사용될 수 있도록 한다.

솔직히 이 Design Goal 은 그리 간단한 목표가 아닙니다. 최대한 다양한 환경이라 함은 OS 내지는 Platform, 컴파일러를 말하는 것일텐데... 이걸 위해서는 이식성도 있어야 할 것이고, Embedded System 에 적용할 수 있으려면 성능도 상당히 Optimize 되어야 할 것이기 때문입니다. 그렇지만 이 로깅 라이브러리의 사용자를 최대한 많이 확보하기 위해서는 당연히 고려해야할 사항이라고 생각합니다.

2. 사용하기 쉽게 만든다.

이  Design Goal 대해서 무슨 라이브러리에서 사용성을 생각하느냐하고 반문하실 분도 있겠지만, 라이브러리의 인터페이스도 사용하기 쉽고, 일관성이 있고, 잘못 사용할 수 있는 가능성을 최대한 줄여야 할 것입니다. 이것도 마찬가지로 더 많은 사용자를 확보하기 위한 전략입니다.

3. 로깅 라이브러리의 크기를 최대한 작게 만든다.

마지막으로 로깅 라이브러리에서 너무 많은 일을 하다보면 특정 사용자들은 자신에게는 필요 없는 기능들을 어쩔 수 없이 포함시켜야 하는 일이 발생하므로 차라리 핵심적인 기능만을 제공하고, 사용자들이 직접 기능을 추가하거나 또는 차후 릴리즈에 포함시키는 것이 좋을 것입니다.

이와 같은 Design Goal 을 고려할 때 저도 rein 님과 같은 비슷한 답변을 해야할 것 같습니다.

1. 요구사항 3. 과 관련하여 모듈이라는 걸 어떤 단위로 정의해야 하는 걸까요 ? 로깅 라이브러리를 사용하는 S/W 내에 모듈 구조가 flat 한 구조인 걸 가정해야할까요 ? 아니면 계층적인 구조를 가정해야 할까요 ? 계층적인 구조라면 상위 모듈의 로깅을 활성화/비활성화시키면 그 안에 포함된 모듈에 대한 로깅은 어떻게 작동해야 할까요 ?

모듈이라는 단위는 솔직히 소프트웨어마다 설계하기 나름이므로 어떤 단위를 모듈로 정한다라고 라이브러리에서 미리 정의해 놓는다는 건 현실적으로 불가능할 것입니다. 그러니 사용자가 모듈이라는 단위를 자유롭게 지정할 수 있도록 하는 것이 좋을 것이고, 라이브러리의 Guideline 같은 곳에서 모듈을 어떤 단위로 정하는 것이 좋다 이렇게 언급하는 게 좋을 것입니다.

이렇게 모듈을 어떤 단위로 정해야 할 것이냐는 차라리 문제가 되지 않을 것 같은데... 대규모의 소프트웨어에서는 모듈에 자연스럽게 계층 구조가 형성되기 마련이라서 이걸 로깅과 관련해서 어떻게 처리할 것이냐가 이슈가 될 것입니다.

이에 대해 Design Decision을 내리기 위해서는 아무래도 제가 제시했던 세 가지 Design Goal 을 한 번 되새겨 볼 필요가 있어 보입니다. 우선 2. 번 사용하기 쉽게 만든다는 측면에서는 상위 계층의 모듈에 대한 설정이 아래 모듈의 설정에도 자동으로 적용되어야 할 것입니다. 그래야 계층 구조를 알고 있는 개발자 입장에서 자연스러울 것이기 때문입니다. 예를 들어, 다음과 같은 모듈 계층 구조가 있다라고 가정해 보죠(그리 현실적인 모듈 구조는 아닙니다 -.-).

Comm
  |
  +-- Socket
  |    |
  |    +-- TCPSocket
  |    |
  |    +-- UDPSocket
  |         |
  |         +-- UnicastUDPSocket
  |         |
  |         +-- MulticastUDPSocket
  |
  +-- IPC

개발자 입장에서 봤을 때, Comm 모듈에 대한 로깅 수준 및 로깅 여부를 설정하면 자연스럽게 아래 있는 모든 모듈들에 대해서 동일한 설정이 적용되는 것으로 인식하게 될 것입니다. 그래야 로깅 라이브러리의 사용성이 Scalable 하게 될 테구요. 일일이 중간에 있는 모듈뿐 아니라 제일 하부에 있는 모든 모듈들에 대해 로깅 설정을 따로 해야 한다면 그 로깅 라이브러리를 사용하기 위해서는 초기화 과정 자체가 너무 힘들어서 로깅 라이브러리를 사용하지 않게 될 것입니다. 사용하기가 너무 불편해지는 것이죠. 그래서 "사용하기 쉽게 만든다"라는 측면에서는 계층 구조를 가정하고, 계층 구조 상의 상위 모듈에 대해 로깅 수준 및 로깅 여부를 설정하면 하위 모듈에 대해서도 해당 설정 내용이 적용되도록 설계/구현하는 것이 바람직할 것입니다. 물론 하위 모듈에서 설정을 하면 상위 모듈의 설정 내용을 Override 하는 것으로 해야겠지요.

다음으로 1번 Design Goal 측면에서 다양한 환경에서 로깅 라이브러리가 사용될 수 있도록 한다는 측면에서는 어떨까요 ? 1번 Design Goal 과 관련된 세부 사항으로는 이식성과 성능을 언급했는데요... 이식성 측면에서는 계층 구조를 지원하는 것이 별 문제가 될 것 같진 않습니다. 다만 성능 측면에서는 로깅을 시도할 때마다 현재 로깅하는 모듈에 대해 로깅 설정 내용이 없다면 상위 모듈의 설정 내용이 있는지 살펴 보고, 그래도 없다면 차상위 모듈을 살펴보고 이런식으로 상당히 복잡한 검색 및 비교 과정이 필요하게 될 것입니다. 따라서 단순 성능 측면을 고려한다면 계층 구조를 지원하지 않는 것이 바람직해 보입니다.

마지막으로 3번 코드 크기 측면을 고려한다면 계층 구조를 고려하기 위한 코드-로깅을 시도할 때마다 현재 로깅하는 모듈에 대해 로깅 설정 내용이 없다면 상위 모듈의 설정 내용이 있는지 살펴 보고, 그래도 없다면 차상위 모듈을 살펴보고 이런식으로 상당히 복잡한 검색 및 비교 과정을 위한 코드-양이 늘어나게 될 것이므로 마찬가지로 계층 구조를 지원하지 않는 것이 바람직해 보입니다.

계층 구조를 지원하는 기능에 대해 우리 Design Goal 세 친구에게 물어 봤더니 한 친구만 찬성하고 두 친구는 반대를 하네요. 여러분은 어떻게 결정하시겠어요 ? 그냥 다수결로 하실 건가요 ? 저는 찬성하는 친구말을 듣기로 했습니다. 왜냐구요 ? 음... 그건 엿장수 맘이지요. 제가 그렇게 설계/구현한다는데... 그리고 돈 받고 하는 것도 아닌데 누가 뭐라고 하겠어요 ? 라고 말하면 돌 날라 올 것이 분명하므로... 성능 측면에서는 복잡한 검색 과정이 필요하더라도 성능을 최적화할 수 있는 방안을 뭔가 마련할 수 있을 것으로 보이구요. 그 복잡한 검색 과정을 구현하는데, C++의 표준 라이브러리를 활용한다면 코딩량이 그렇게 많아질 것 같진 않기 때문입니다. 물론 이 판단은 설계 및 구현해 가면서 차차 검증해 봐야 하는 부분이라고 생각합니다.

그래서 계층 구조를 지원하기로 결정!!!

2. 로깅의 상세한 정도는 어떻게 나누어야 할까요 ? 그냥 1 ~ 255 정도로 나눌까요 ? 아니면 몇 가지 잘 알려진 수준을 두는 것으로 할까요 ?

이건 두 말하면 잔소리일 것 같습니다. 사용성 측면에서는 당연히 잘 알려진 로깅 수준-예를 들어, DEBUG, TRACE, INFO, WARNING, ERROR, FAULT 등의 수준-으로 나누는 것이 나을 것이고, 성능 측면이나 코드 크기 측면에서는 1 ~ 255와 같은 상세한 수준으로 나누는 것과 거의 차이가 없을 것입니다.

그래서 로깅 수준은 DEBUG, TRACE, INFO, WARNING, ERROR, FAULT 로 나누기로 결정!!!

3. 암호화/복호화, 로깅 정보 축약, 날짜별/시간별 별도 파일 기록, 일정 크기를 넘을 때 다른 파일로 기록하는 등의 기능을 로깅 라이브러리 자체에서 구현하도록 할 까요 ? 아니면 별도의 Extension Mechanism 을 두고 로깅 라이브러리 사용자가 직접 구현하도록 할까요 ?

이런 기능들은 1) 사용성 측면에서도 너무 많은 기능으로 인해 설정 내용이 너무 복잡해질 염려가 있고, 2) 성능 측면에서도 너무 많은 기능이 기본으로 로깅 라이브러리에 포함된다면 좋지 않은 영향을 받을 것이고, 3) 코드 크기 측면에서도 코딩량이 너무 많아질 것입니다. 다만, 이런 다양한 요구사항을 수용할 수 있는 구조로 로깅 라이브러리를 설계할 필요성은 명백해 보입니다. 그렇지 않으면 우리가 설계/구현하려고 하는 로깅 라이브러리의 유용성이 많이 떨어질 것이기 때문입니다. 따라서 Log Sink,  Log Formatter, Log Filter 쪽에 잘 정의된 Extension Mechanism 을 두는 것이 바람직할 것입니다. 이걸 구조설계적인 관점에서 본다면 Log Filter, Log Formatter, Log Sink 를 잘 정의된 추상 클래스로 설계해야 한다는 것으로 해석할 수 있을 것입니다.

여기서 막바지 반전하나! 그렇다고 하더라도 기본적인 Log Sink, Log Formatter, Log Filter 는 로깅 라이브러리 내에서 구현해야할 것입니다. 많은 경우에 기본적으로 사용될 수 있는 합리적인 기본값이 있어야 하는 것이지요. 이렇게 합리적인 기본값이 있을 경우, 처음으로 라이브러리를 사용하는 사람이 라이브러리에 접근하기가 용이해 질 것입니다. 그렇지 않고, 뭔가를 일일이 설정해 주고, 추가적으로 뭔가를 구현해 줘야 한다면 그것처럼 귀찮은 일이 없지요. 단순히 쉽게 사용만 하려는 사람에게 이것저것을 하라고 요구하게 되는 것이니깐요.

결론적으로 Log Sink, Log Formatter, Log Filter 를 위해 잘 정의된 Extension Mechanism 을 정의하기로 결정하되 기본적인 Log Sink, Log Formatter, Log Filter 는 제공하기로 결정!!!

이상으로 로깅 라이브러리를 위한 기본적인 설계 방향을 정해 보았습니다. 여러분이 로깅 라이브러리를 구현한다면 어떤 식으로 결정할지 여러분의 생각도 알려주시면 감사하겠습니다.

이 글을 처음 시작할 때는 본격적인 설계를 시작해 보려고 했는데, 기본적인 설계 방향을 정하는 것으로 이만 글을 줄여야 할 것 같습니다. 본격적인 설계를 시작하려면 글이 너무 길어질 듯한데다 이 글을 쓰면서 저도 생각하지 못했던 사항들이 이것 저것 튀어 나와서... 생각할 시간이 더 필요할 것 같습니다.

그럼 지금까지 제 글을 읽어 주셔서 감사드리고, 이 글이 맘에 드신다면 살짝 여기 저기 추천 부탁드립니다. 고절가주팁이 쭉~ 갈 수 있도록 많은 관심 부탁드리고, 혹시 잘못된 부분이나 보완할 부분이 있으면 언제라도 알려주세요.

제 글이 유익하셨다면 오른쪽 버튼을 눌러 제 블로그를 구독하세요. ->
블로그를 구독하는 방법을 잘 모르시는 분은 2. RSS 활용을 클릭하세요.
RSS에 대해 잘 모르시는 분은 1. RSS란 무엇인가를 클릭하세요.