웹 서비스 배포 플랫폼 Luidium 개발기 - 2
최근에는 개발에만 파묻혀 하지 못했던 제품 자체에 대한 생각을 많이 했던 것 같다. 기존에 많이 사용하지 않던 Mircosoft Azure, Amazon Web Service, Google Cloud Service 등 웹 인프라 서비스 등에 대해서도 조사하며 내가 지금 만들고 있는 것이 이러한 서비스들의 완벽한 부분집합이 될 것이라는 예상도 할 수 있었다. 따라서 처음에 제품을 기획한 동기를 잃지 않으면서 이들과의 차별점을 마련해야겠다는 생각이 들었다.
또한 크게 중요한 요소는 아니지만 개발 스택에 대해서도 고민해보게 되었는데, 개인용 서버에서 서비스를 돌리는 입장에서 개발이 느리고 대규모 트래픽 처리가 빠른 Rust 서버와 Scale-out에 특장점이 있는 ScyllaDB와 같은 목적에 맞지 않는 프레임워크를 사용하는 것이 아닌가라는 고민이 많이 들었다. Rust로 개발하는 것이 성가신 부분이 없지는 않았기에, Node.js 기반 서버에 MongoDB 혹은 Go기반 서버에 PostgreSQL 등 비교적 쉽고 빠르게 개발할 수 있는 선택지를 고려해보기도 했다. 하지만 Rust라는 언어를 학습하고 싶었던 욕구도 컸을 뿐더러, CI/CD 플랫폼으로서 기능하려면 CLI(Command Line Interface)도 개발해야 할 것으로 판단했기 때문에, 강력한 cli app까지 개발할 수 있는 Rust를 메인 서버 개발 언어로 도입하기로 결정하였다.
Rust Axum 도입기
스타트업에서 제품을 개발하면서 가장 많이 쓰고, 접했던 서버 언어는 단연 Node.js 기반 Javascript(Typescript)라 할 수 있다. 언어 자체의 퍼포먼스는 낮더라도 클라이언트와 서버사이드 모두에 사용할 수 있으면서 양 방면의 생태계가 모두 거대한 점과 언어 자체의 난이도가 낮다는 점이 매력적이었다.
이번에 Luidium의 서버사이드를 재구축하게 되면서 Rust를 이용한 서버를 구축하였는데, 개발의 초기 단계임에도 왜 많은 사람들이 Java Spring, Node.js 기반 서버를 사용하는지 알 수 있었다. 나의 부족한 Rust 언어에 대한 숙련도도 문제일 수 있지만, 확실히 다른 언어에 비해 코드 작성 자체에 드는 노력이 많아 새로운 기능 개발과 같은 생산적인 작업에 조금은 무뎌졌던 경험을 하게 되었다. 특히 Npm과 같은 Javascript의 생태계는 Rust와는 천지차이였고, 구글링 몇 번, 생성형 AI를 이용해 쉽게 끝나던 작업도 시간을 더 할애하게되는 문제가 있었다.
특히 PostgreSQL, MySQL, MongoDB와 같은 비교적 인지도가 높고 큰 생태계를 가진 데이터베이스를 사용하지 않고, Cassandra기반의 CQL을 사용하는 NoSQL인 ScyllaDB를 도입하면서 비주류 언어 + 비주류 데이터베이스라는 환상적인 시너지가 발생해 꽤나 고생을 했다. (다행히 공식 드라이버는 있었다)
좋았던 점은 Python 기반의 FastAPI, Django, Node.js 기반의 Express, Nest.js, Java의 Spring Boot 등은 모두 광범위하게 이용되는 웹 서버 프레임워크로 파일의 Naming Convention 이라던지, 코드 작성 패턴 등이 존재하지만, Rust Axum의 경우 비교적 신생 프레임워크이기도 하고, 참고할 만한 코드도 웹 상에 많지 않아 내 입맛에 맛게 코드 베이스를 디자인하는 경험을 할 수 있었다. 개인적으로 Nest.js의 Modular 구조를 매우 좋아했고, Nest.js의 Dependency Injection 유사 기능이 Axum에도 존재하여 이를 차용한 Modular 구조를 가진 코드를 작성하게 되었다.
Minio Object Store
대량의 파일을 서버에 업로드하고, 다운로드해야 하기 때문에, 메일 서버와 파일 업로드/다운로드를 관장하는 서버를 분리하여야 한다는 생각을 가지고 있었다. 이에 처음에는 여러개의 Rust 서버를 만들어 마이크로서비스 형태로 기능을 분리하려고 하였다.
하지만 Minio를 Rust와 다루며 조금 성가신 문제에 맞닥뜨렸는데, Minio와 Rust를 연결할 드라이버를 다루기가 어려웠다는 점이었다. 결국 SSL/TLS 설정을 하던 중 Docker bridge network 환경 내에서 minio client 생성 시 Certification 관련 설정에서 개발이 막혀버렸고, 이와 더불어 다수의 파일을 업로드할 Presigned Url을 발급하고, bucket을 생성하고, bucket에서 object key를 이용해 다수의 파일을 클라이언트로 전송하는 과정이 생각보다 시간이 오래 걸린다는 것을 파악하였기 때문에, 이를 모두 Storage Server라는 마이크로서비스로 분리하기로 결정하였다. 그리고 웹 서버로써의 퍼포먼스가 매우 높고 Minio Driver 생태계가 큰 Go 언어를 이용하기로 결정하였다.
또한 Storage에서 파일을 모두 내려받아 이를 빌드하고, Docker Image로 만드는 과정은 가장 시간을 많이 차지할 작업이기 때문에, 이 또한 별도의 Go 마이크로 서비스로 분리하여야겠다는 판단을 하였다. Client로부터 Main Server로 Block 단위의 Deploy call이 발생하면 Kafka를 이용해 Build Server에 빌드 요청을 보내고, Build Server는 해당 메시지를 구독하고 있다가 Storage 서버를 이용하여 해당 Block의 모든 파일을 임시 디렉토리에 내려받고 Docker Image로 빌드한 후, 이를 다시 Storage에 업로드한 뒤 Host machine의 Docker Daemon을 이용하여 container로 실행한다.
Enjoy Reading This Article?
Here are some more articles you might like to read next: