벌크액션 감상문
개요
벌크 액션 서버를 짜고 이후 관리하면서 생긴 후일담에 대한 개인적인 회고록입니다. 최근에 팀 내에서 급증하는 트래픽 안정적으로 처리하기라는 내용으로 테크 블로그 글을 작성했습니다.
처음의 예상과는 다르게 약 3만 줄의 코드 정도의 복잡한 시스템이 탄생하게 되었고, 이를 실제 프로덕션에서 운용하고 마이그레이션 하는 경험은 한 명의 엔지니어로서 소중한 경험이라고 생각되어 이렇게 추가적으로 후기글을 남깁니다.
golang, 탁월하다
실제 팀 내에 높은 트래픽을 견디는 golang 서버에 대한 경험치가 크게 없어서, 처음에 작성하면서 꽤나 의심을 했었다. 특히 golang이 추구하는 단순성이란 이름 아래 정말 적은 키워드와 모든 코드에서 묵시적인 걸 피하려고 하는 코딩 스타일은 자바를 주언어로 사용하는 내 입장에선 못생겨 보였다. [1] 개인 프로젝트에서는 러스트도 사용하는데, 뭔가 러스트보다 더 손이 안 갔다. 그 특유의 투박함 떄문일까
그럼에도 테크 블로그에서도 언급했듯이 막강한 동시성 지원 및 추상화 정도는 정말 놀라웠다. 자바에서 ThreadPool을 짠다고 하면 고려할 게 정말 많은데, 100줄 내외로 이러한 스레드풀을 구성할 수 있음에 감동했다. goroutine이 가진 추상화 정도와 성능은 정말 대단했다.

그림 1. Go GMP Scheduler
추가적으로 내부 부하테스트 중에 비정상적인 cpu 사용률이 보여서 이를 pprof를 이용해 개선했었는데 진짜 “딸깍” 했더니 프로파일링이 되었다.[2] 이러한 Obserability를 쉽게 제공하는 건 엄청난 접근성과 사용성이라고 생각한다. 실제로 해당 프로파일링 적용 및 문제 원인 파악 및 해결까지 1시간 30분 걸렸다. 물론, 실제 문제가 예상하던 범위에 있어서 그랬던 것도 있겠지만 엄청난 접근성이다. 이러한 사용성은 분명히 golang의 매력이다. 결론적으로는 해당 서버를 작성하면서 golang 언어에 대한 생각이 꽤나 바뀌었고, 좋은 선택지 중에 하나가 되었다.
개발자들이 의도적으로 불편하게 만들어야한다
변정훈님의 내가 생각하는 플랫폼 엔지니어링이라는 글을 작년에 처음 읽었을 때는 “아 그래야지, 그렇겠지 아마.“정도의 감상이었다면, 벌크액션 작업을 하면서 계속해서 곱씹게 되었던 거 같다.[3] 복잡해져만 가는 시스템 위에서 적절한 추상화를 만들고, 개발자들에게 이를 강제한다. 그리고 이 추상화 위에서도 반드시 알아야 하는 개념들을 API 스펙으로 정의하고, 개발자들이 이에 대해 고민하고 코드를 작성할 수 있도록 유도한다.
벌크 액션을 작성하면서 이게 어느정도의 RPS(Request Per Second)를 가져야하며, DB 부하에 대해 고민해야한다. 일종의 메시지 큐와 비슷한 추상화 정도를 제공해, 재시도 가능성과 at-least once semantic을 지켜야하는 코드를 제공한다. 결론적으로 일관적인 인터페이스와 sdk 제공을 통해 동일하게 코드를 작성할 수 있지만, 코드 작성이 의도적으로 개발자가 생각할 포인트를 제공한다.
예전에 단위 테스트를 전파하려고 팀 내에서 기여자들을 기반으로 팀 내에 여러 문화를 전파했던 기억이 있는데, 그 때는 일종의 문화나 개발자 인식을 바꾸기 위해서 발로 뛰었다면, 이제는 그냥 서비스로 만드는 게 얼마나 강력한 인식 변화를 이끄는지 이해했던 것 같다.
테스트 환경의 중요성
벌크 액션 서버는 내부에서 철저한 부하테스트를 거쳐서, 고루틴 워커의 개수와 서버 스펙을 정했고 예상되는 문제를 발견하고, 재현해서 해결해나갔다. 부하 테스트 전용 리모트 서버를 띄어서 실제 프로덕션 환경을 충분히 재현할 수 있는 테스트 환경을 만들었는데, 이 환경이 프로젝트 개선에 있어서 정말 중요한 역할을 했다.
최근에는 정말로 Obserability의 중요성과 테스트 환경 세팅을 통한 무한 피드백 사이클을 만들고 거기서 실제 문제들을 해결해나가는 건, Agile한 환경을 만드는 것이라 생각한다. 일종의 메타 인지를 높여주는 도구인 것이다. 이전에 코끼리를 눈 감고 더듬던 개발 환경에서 주로 일했던 사람으로서 감격스러운 순간이었다.
엔지니어는 실질적인 문제를 풀어야한다
소프트웨어 엔지니어는 실제로 있는 문제만을 해결해야하며 그 문제를 발견하고, 검증하고 재현한 후에 이를 개선해야한다. 이전에 리누스 토발즈가 구글 개발자에게 폭언한 메일링 리스트를 꽤나 인상 깊게 보았는데 불필요한 추상화를 줄이고 실질적인 문제 해결을 위한 코드 작성을 강요하는 것이 얼마나 중요한지를 나타내지 않나 싶다.[5]

그림 2. your code IS GARBAGE.
하지만, 미래 지향적인 설계는 역시나 가져가야한다. 기존의 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계가 되어야 한다. 객체지향의 OCP 원칙을 그래서 나는 매우 좋아한다. 복잡한 추상화는 피하되, 반드시 미래에 추가될 것이라고 예측되는 문제를 미리 대응해두는 것이 필요하다.
결론
두서 없이 생각나는 걸 몇 가지 적어보았다. 아마도 이러한 생각은 또 언젠가 달라질 수도 있겠지만 이것도 마치 소프트웨어의 생명 주기와 비슷한 게 아닐까? 계속해서 변화해가는 생각이지만, 추후의 확장을 위해 여유를 남겨두듯이.
References
[1] https://go.dev/doc/faq#principles
[2] https://go.dev/blog/pprof
[3] https://blog.outsider.ne.kr/1736
[4] https://en.wikipedia.org/wiki/Occam%27s_razor
[5] https://lkml.org/lkml/2024/1/26/1013