Could not emit tick 에러 메세지를 뱉고나서 어플리케이션은 메모리에 byte 형 데이터를 누적하기 시작했다. 에러를 발생시킬 요소와 어플리케이션에서 byte형 데이터를 사용하는 요소를 힌트로 문제가 되는 코드를 찾을 수 있었다.
문제가 되는 코드는 interval job에 대한 것이었다. 이 코드가 하는 동작은 간단하다. 주기적으로 hashmap에 쌓여 있는 데이터를 읽어와 데이터를 처리하고 hashmap에서 읽어온 데이터를 제거한다. 코드상 로직은 다음과 같다.
1. Interval로 tick 발생
2. 데이터 처리 및 읽은 데이터 삭제
이러한 상황에서 문제는 interval job이 어떠한 이유로 인해 '정지'하고, 데이터 처리 및 삭제 로직이 동작하지 않아 메모리에 계속 데이터가 쌓이는 것이다.
자, 문제는 찾았다. 그러면 이제 문제의 원인을 찾아봐야겠다. 위 코드 상 로직에서 숨겨진 내용은 다음과 같다.
1. interval로 tick 발생
2. Buffer에 tick push
3. 다음 step에서 자신이 할 일을 다 처리했다면 Buffer에서 tick pop
4. 데이터 처리 로직 수행
여기서 만약 tick을 발행하는 주기보다 tick을 처리하는 로직의 수행 시간이 더 오래걸린다면 어떻게 될까? 구체적으로 예를 들어보자. 10초에 한 번씩 tick을 발생시키는 job이 있다. 이때, 처리 로직이 50초가 걸린다고 가정 해보자. 그러면 buffer 에 tick은 10초에 한 번씩 push가 되는데 처리 로직은 50초에 하나씩 pop을 한다. 정리하자면, 50초 후에 buffer에는 tick이 4개가 쌓이고, 100초 후에는 tick이 8개가 쌓인다. 즉, 시간이 지나면 언젠가 buffer의 용량을 초과해버리는 사태가 발생할 것이다.
buffer over flow. 이렇게 정리해두고 보면 이런 일이 발생하도록 코드를 작성하는게 멍청한 것 아닌가? 싶다. 하지만, 처리 로직이 외부 서비스에 의존적이라면(예측이 불가능하다면) buffer over flow는 충분히 발생가능하다.
해결방법은 여러가지가 있을 수 있다. 필자의 상황에서 tick은 중요하지 않다. 주기적으로 메모리의 데이터를 '처리'하는 것에 의미가 있었다. 그렇다면 해결 방법은 간단하다. buffer가 가득찰 경우 buffer를 비워버리면 된다. 각자의 상황에 따라 어떻게 문제를 해결할지는 달라지겠다.
일단, 코드 상의 문제와 원인을 파악하고 해결방법을 적용해서 문제를 해결했다. 해결했다고 여기서 작업의 끝은 아니다. 이 다음에 해야할 작업은 기능의 존재 목적과 현 시스템 구조가 맞는가 이다. memory에 데이터를 저장해서 처리하는 것이 맞는가? 어떠한 이유로 시스템이 종료되어 메모리에 저장된 데이터를 처리하지 못해도 괜찮은가? interval로 처리하는 것은 맞는가? 메모리에 데이터를 쌓지 않고 그때 그때 바로 처리하면 안 되는 것인가? 이러한 질문들에 답변해 나가며 Next Task를 뽑아 볼 수 있겠다.
시스템의 처음부터 만드는 경우는 많지 않다. 기존 팀에 조인하며 기존에 만들던 시스템을 이어서 만들게 되는 경우가 대부분일 것이다. 시스템에 대해 완전히 이해하기 전에 시스템에서 문제가 발생하는 경우도 종종 있을 것이다. 이럴 경우, 우리가 해야할 일은 전임자를 찾기보다 콤팩트하게 고통받으며 시스템을 이해하려는 노력이 아닐까 싶다.