OZ1NG의 뽀나블(Pwnable)

[Tips][C++][개발] 실시간 로깅 구현 시, std::cerr 대신 fprintf를 사용하면 더 좋은 이유 본문

Tips

[Tips][C++][개발] 실시간 로깅 구현 시, std::cerr 대신 fprintf를 사용하면 더 좋은 이유

OZ1NG 2023. 11. 20. 03:04

[개요]

C++에서는 출력 스트림으로 stdout에 대응하는 std::cout가 존재하듯, stderr에 대응하는 std::cerr가 존재한다.

하지만, std::cerr를 실시간 로깅에 사용하는 경우, 특히 멀티 프로세싱 또는 멀티 스레딩 프로그램 개발에서의 실시간 로깅에 사용하는 경우 다음과 같은 문제가 발생할 수 있다.

 

[문제 코드]

<Child Process 1>
std::cerr << "hello" << "world!" << std::endl;

<Child Process 2>
std::cerr << "HELLO" << "WORLD!" << std::endl;

예를들어 멀티 프로세싱으로 구동되는 프로그램이고, 서로 다른 자식프로세스에서 위와 같은 로깅 문구를 거의 동시에 출력한다고 가정해보자.

 

그 전에 먼저 std::cerr의 출력 방식을 간단하게 설명하고 넘어가도록 하겠다.

std::cerr의 `<<` 연산자로 어떠한 데이터를 출력한다는 것은, stderr 스트림을 사용해서 `<<` 연산자 다음 데이터를 출력하겠다는 뜻이다. 그런데 이때의 의미는 std::endl을 만날 때까지 stderr 버퍼에 문구를 채우고 한번에 출력을 하겠다는 것보다는, 그냥 `<<` 연산자 다음에 위치한 데이터를 '일단' 출력하겠다는 것에 더 가깝다.

 

그래서 [문제 코드]와 같이 std::cerr가 동시에 사용된다면, 다음과 같이 섞여서 출력 될 수 있다.

[출력 예시]
helloHelloworld!World!`\n` // std::endl
`\n` // std::endl

위 문제 코드야 뭐 문자열이기 때문에 좀 어지러워도 이해를 못할 정도는 아니지만, 다음과 같이 std::cerr를 통해 어떤 실시간 숫자 데이터를 출력한다고 생각해보면 얼마나 어지럽게 출력이 될지 가늠조차하기 어려워진다.

 

[어지럼증 유발 출력 예시]

[예시 코드]
<Child Process 1>
int data1 = 12;
std::cerr << data1 << std::endl;

<Child Process 2>
int datd2 = 34;
std::cerr << data2 << std::endl;

[동시 출력 결과 예시1]
3412

[동시 출력 결과 예시2]
1234

data1과 data2와 같이 중요한 데이터를 로깅해야하는데, 어떨때는 예시1처럼 기록되고 어떨때는 예시2처럼 기록된다면, 사실상 로깅을 하는 의미가 없을 정도로 로깅된 데이터를 이해하기 쉽지 않을 것이다.

 

[해결책]

그렇다면 이런 문제를 어떻게 해결할 수 있을까?

바로 근.본 C로 돌아가 fprintf(stderr, ...)로 로깅을 시도하는 것이다.

[수정된 코드]

<Child Process 1>
std::cerr << "hello" << "world!" << std::endl;
-->
fprintf(stderr, "hello world!\n");

<Child Process 2>
std::cerr << "HELLO" << "WORLD!" << std::endl;
-->
fprintf(stderr, "HELLO WORLD!\n");
[출력 예시]
hello world!
HELLO WORLD!

fprintf는 서식문자를 통해 출력할 데이터를 문자열화 한 뒤, 버퍼에 모으고, 한번에 출력을 하는 특징을 가지고 있다.

때문에 std::cerr를 사용할 때의 문제점을 해결 할 수 있다! 

 

[결론]

따라서 이러한 이유 때문에 멀티 프로세싱 또는 멀티 스레딩 환경에서 실시간 로깅을 할때는 편리한 std::cerr 보다는 조금 귀찮더라도 fprintf를 사용하는 것을 추천한다.

Comments