Logging

로깅은 프로그래밍 전반에 걸쳐 매우 중요하게 여겨지는 요소이다. 실제 핵심적인 로직에 반영되는 것은 아니지만, 안정적이고 에러에 강한 프로그램을 만들기 위해서라면 로그는 분명 필요하다.

Why we log

왜 로깅이 필요한지 돌이켜 생각해보면, 컴퓨터가 처음 만들어지는 시대로 돌아가야 한다. 우리는 전기 신호를 통해 특정 연산을 수행하는 작은 장치들을 통해, 인간이 수작업으로 진행해야 했던 일들을 점차 자동화해나가기 시작했다.

그러나 전기 신호는 가시성은 있으나 가독성이 없어, 단순한 전기 신호만으로는 해당 프로그램이 어떤 작업을 수행하고 있는지 파악하기 매우 어렵다. 또한, 시간이 지나며 자그마한 일들을 수행하던 프로그램들이 합쳐지고 거대해지기 시작했다. 더 이상 프로그램의 내부를 모두 펼쳐 들여다보고, 연산이 이루어지는 과정 과정을 사람이 전부 확인하기 어려워지게 되었다. 전기 신호는 빛과 같으니, 여러 연산을 수행하는 전기 신호의 흐름을 사람이 파악한다는 것이 불가능함을 직관적으로 이해하기만 하더라도 이러한 시기가 오기까지는 그리 많은 시간이 걸리지 않았음을 알 수 있을 것이다.

아마도 이것이 로깅을 필요로하는 처음 이유였을 것이다(사실 그냥 뇌피셜이지만, 어렵지 않게 받아들일 수 있다). 처음의 로깅은 각 과정 중간중간에 사람이 인식 가능하도록 표시자를 만들고, 표시자가 출력되도록 하여 사람이 이를 인지하는 과정을 통해 해당 연산이 올바르게 이루어지고 있음을 통하여 이루어졌다. 연산의 과정을 확인하고자 했던 시도는 현대에 와서 결과를 비롯하여 다양한 정보들을 함께 저장하고 관리하는 중요한 분야로 자리잡게 되었다.

로깅의 시작을 말하고자 하면 더욱 많은 얘기를 할 수 있을 듯 보이지만, 역사 이야기는 이쯤에서 접어두고 로깅에 대해 더욱 깊게 알아보자.

Log

로그는 다양한 분야에서 사용되며, 실제로 쓰이지 않는 분야는 거의 존재하지 않는다. 견고한 프로그래밍을 위해서는 필수적인 요소인 만큼, 어느 곳에서도 빼먹지 않고 사용되는 개념이 되었다. 데이터베이스 엔진, 분산 컴퓨팅, 버전 관리, 합의 알고리즘 등 복잡하면서도 무결성을 필요로 하는 시스템에는 빠짐없이 들어간다. 이는 다양한 서비스가 얽혀있는 곳일 수록 더욱 로깅의 중요성이 대두되기 때문일 것이다.

Logging at Systems

데이터베이스 시스템에서는 무결성을 높이고자 로깅이 활용된다. ext 파일 시스템을 예로 들면, 유명한 방식인 저널링이 등장한다.

ext는 저널이라고 불리우는 로그를 데이터 변경 이전에 먼저 작성하여, 실제 데이터 변경 사항을 기록한다. 이때의 기록 대상에는 모든 파일 시스템 데이터를 비롯하여 메타데이터까지 포함되늗데, 저널링 모드는 변경 내용의 손실 가능성을 최소화한다. 데이터가 두 번 작성되는 것이나 다름없는 만큼 속도 측면에서 이점을 가져오지 못할 수 있으나, 데이터의 무결성을 최우선으로 하는 시스템에서 채택하기 좋은 방식임을 알 수 있다. 이러한 저널과 같은 로그를 선행 기입 로그(write-ahead log, WAL)이라고 부른다. PostgresQL과 같은 데이터베이스 엔진도 WAL에 먼저 기입한 뒤 데이터를 변경하는 방식을 사용한다.

WAL은 분산 시스템 등에서의 복제에서도 사용된다. 이 경우 로그를 디스크에 작성하는 대신 외부 복제본에 작성한다. 이는 다시 기존의 데이터에 로그를 반영하고, 전체의 상태를 동일하게 맞추는 작업을 반복한다.

Raft와 같은 알고리즘의 경우 이러한 아이디어를 차용하여 분산 서비스 내에서 합의를 진행한다. Raft의 노드들은 로그를 입력으로 하는 state machine을 두는데, Raft는 state machine에 입력되는 상태 변경 요청들을 로그에 추가하고 팔로워 노드들에 복제를 요청한다. 이후 이 명령이 합의된 이후 상태 머신에 실제 적용된다. 이 글은 로깅에 대해 다루므로, Raft의 동작과 합의 과정에 대해서는 추후 다시 다루자.

중요한 것은, Raft가 상태 머신에 변경 사항을 적용하기 이전에 로그를 추가하고 이를 전체 전파하여 로그를 동일하게 유지하는 과정이 수행된다는 것이다. 그냥 값을 변경해도 되는데 굳이 로그를 통해 먼저 합의를 진행하고 이를 반영하는 이유는 무엇일까.

  1. 분산 환경에서 일관성 보장
  • 분산 시스템은 여러 노드가 서로 다른 물리적 위치에 있을 가능성이 높아, 동기화를 진행하면서 다양한 상황에서의 장애가 발생할 가능성이 높다. 이러한 장애들은 각 분산 시스템에서의 일관성 보장 능력을 크게 저해한다. 따라서 Raft와 같은 알고리즘은 로그 복제를 통해 일관된 명령 순서를 보장하고자 한다.
  1. 재해 복구
  • 로그를 유지하면 노드가 failure에 대해 장애 복구가 용이해진다. 그만큼 고수준의 fault tolerance를 지닌 내구성이 향상된 시스템을 구축할 수 있다.
  1. 순서 보장 및 결정론적 state machine
  • 로그를 통해 명령의 순서를 보장 가능하다. 또한 state machine은 결정론적으로(deterministic) 동작하기 때문에, 모든 노드가 동일한 순서로 로그를 처리하여 일관된 상태를 지니도록 보장할 수 있다.

이외에도 리더 선출 및 변경 시 일관성 유지 등 다양한 이유에서 로그는 강력하게 작용한다. 특히 분산 컴퓨팅에서 로그가 강력하게 작용하는 듯 한데,,,이와 관련된 내용은 추후 추가로 기록해보겠다!! 오늘은 로깅에 조금 더 집중하자.

로깅은 프론트엔드 단에서도 사용된다. 프론트엔드는 수우우우많은 상태가 존재하는 stateful한 곳이기 때문에, 다양한 도구들을 활용해서 상태를 잘 관리하는 것이 주요 과제로 주어진다.

이러한 상태의 무결성과 불변성을 위해서 우리는 redux와 같은 라이브러리를 사용하여 상태를 관리한다.(flux 패턴이 적용된 라이브러리를 예시로 들었다)

redux의 경우 변경 사항을 object로 로그에 저장하고, 이러한 변경사항에 대한 반영을 순수함수로 처리하여 애플리케이션 상태에 대해 일관성을 보장하게 된다.


이렇게 확인 가능하듯 로그는 순서가 있는 데이터를 다루는 경우 강력한 힘을 발휘한다.

How does log work

로그는 단순하다. 추가만 가능한 레코드의 연속이다. 위의 복잡한 경우들은 로그를 사용하는 곳에서 어떻게 사용하냐에 따라 복잡성이 생기는 것이지, 로그 자체는 단순한 레코드의 나열일 뿐이다.

기존 로그는 사람이 읽기 위해 사람이 읽을 수 있는 형태로 저장되어왔으나, 최근에는 다른 프로그램이 읽는 것이 매우 중요해져 바이너리 형태로 인코딩된 경우가 많아졌다. 또한 로그는 레코드에 순차적으로 고유 번호인 오프셋을 부여하고, 생성 시간을 함께 기록한다. 결국 로그는 레코드와 생성 시간으로 정렬된 데이터베이스 테이블과 다르지 않음을 알 수 있다.

로그는 추가만 가능한 간단한 형태이기에, 로그를 관리한다는 것은 다른 데이터의 관리와는 사뭇 다른 이야기일 수 있다. 로그를 특정 시점이나 기준을 가지고 제거하는 과정이 필수적이며, 매우 많이 적재된 로그들을 어디까지 어떻게 보관하고 다룰지에 대한 다양한 논의가 필요하다.

일반적으로는 로그를 여러개의 세그먼트로 나눈다. 로그가 큰 용량을 차지하게 되는 경우, 이미 역할을 다했거나 다른 공간에 백업된 경우 오래된 세그먼트들부터 삭제하면서 디스크 용량을 확보한다.

세그먼트들을 보면, 기록이 가능한 활성 세그먼트는 오직 하나이다. 세그먼트는 저장 파일과 인덱스 파일로 개념을 분리할 수 있는데, 저장 파일은 실제 데이터가 적재되는 부분, 인덱스 파일은 그러한 레코드의 인덱스를 기록한다. 이는 레코드의 오프셋을 저장 파일의 실제 주소로 매핑해주어 읽기 작업의 수행 속도를 향상시킨다. 인덱스 파일은 오프셋과 저장 파일의 위치 정보만을 가지기 때문에 실제 저장 파일보다 가볍다. 따라서 메모리 맵 파일 형태로 만들어 메모리 데이터 다루듯이 인덱스를 다룰 수 있다.