발단
최근에 회사에서 fastAPI를 기반으로 한 구글 cloudrun 기반의 서버리스 api서버에 인증 기능을 구현을 해야하는 일이 제 태스크로 주어졌습니다. 원래 mvp의 형태로 구현하기 위해 해당 서버리스 api 백엔드 서버는 프론트엔드에서 gemini 등의 llm 서비스에 api 요청을 보낼 때 일종의 프록시 서버로 활용하기만 하던 중이었고, 저는 해당 기간 동안 다른 작업을 하다가, 제 작업이 끝마쳐지고 mvp로써 기능이 충분히 확인됐다는 판단이 된 회사 입장에서는 추가적인 기능들에 대한 기획들이 제시됐습니다. 그리고 그 정도 기능들이 구현되기 위해서는 인증 기능을 구현하는 것이 가장 먼저 필요하겠다는 것이 개발 팀의 판단이었구요.
llm 서비스에 요청을 보내는 프록시 서버의 역할을 하는 것을 기본의 골자로 하되, 이를 이용하기 위한 다양한 유저들의 접근 등을 제한하기 위해서 처음에는 noSQL을 쓰는 것이 어떨까하다가 결국에는 가장 근본의 형태라 할 수 있는 rdbms를 활용한 서버를 구축하기로 결정했습니다. 이 과정에서 인증은 최초에는 jwt를 쓰는 제안도 나왔지만, 저는 가능한 보안의 관점이나 관리 측면에서 트래픽이 적을 경우에 세션의 형태로 인증을 관리하고 싶다는 주장을 했습니다. 이 때 서버리스 환경인 만큼 인메모리보다는 redis를 세션 저장소로 사용하는 것을 고려했습니다. 그리고 회사에서는 개발 리소스를 줄이고, 추후에 b2b로의 확장성을 고려해서 처음부터 workOS를 활용한 개발을 진행할 것을 제안 받았고 quick start 가이드를 조금 사용해 본 뒤 ‘이정도면 할만할 것 같아요’라는 개발 담당자였던 제 동의 하에 해당 방향으로 진행을 하게 됐습니다.
db 구축
가장 먼저 문제가 됐던 것은 현재 저희 서버가 활용하는 db와 관련한 설정이 전무한 상태였다는 부분이었습니다. FastAPI의 경우에는 일반적으로 sqlAlchemy를 함께 사용하는 것이 권장되는 편입니다. sqlAlchemy는 파이썬 진영에서 굉장히 널리 사용되는 툴킷이고 안정적으로 서비스 돼왔습니다. db를 활용하기 위한 툴킷으로 여러가지를 고민했었는데 아무래도 FastAPI 자체에서 연동이 가장 잘 지원되는 부분이 크게 와닿았고, 다른 것보다 SqlAlchemy Core를 사용하면 ORM을 사용하는 것에 비해 비교적 추상화 계층을 적게 둘 수 있다는 것이 매력적으로 와닿았습니다. 최근의 개발 경험이나 여러 자료들을 통해서 ORM을 통한 개발의 한계를 많이 체감하고 있었던 것이 우선이고, 개인적으로는 아주 개발 초기를 벗어나면 database와 oop 패러다임이 다른 것을 억지로 엮어내는 ORM은 오히려 개발 생산성 측면에서 조차 장점이 모자라다고 생각하기 생각하기 때문입니다.
해당 방식으로 서버를 구축하기로 결정한 뒤에 db의 커넥션과 관련해서는 서버가 시작되는 시점에 엔진 객체를 생성해두고, db 요청이 필요한 api에 따라 커넥션이 매번 생성되는 형태로 구현이 돼있는 상황입니다. 아무래도 커넥션이 생성되는 것 자체가 워낙에 부하가 많이 발생하는 일이고, db 서버의 커넥션 갯수 제한이 있을 것 까지 감안하면 언젠가는 커넥션 풀의 형태로 변경해야한다는 사실은 주지하고 있지만 당장에는 해당 내용을 일단 의존성으로 밀어넣는 작업만 진행했습니다. sqlAlchemy의 engine 설정에서 해당 기능들이 제공된다는 이야기를 최근 접했어서 기회되는대로 관련한 리서치를 진행해보려 합니다.
대신 조금 신경을 썼던 부분은 아무래도 서버리스 환경이면서 FastAPI를 활용하고 있기 때문에 db 엔진을 비동기적으로 관리하는 작업이었습니다. FastAPI의 경우에는 최근 다른 고성능의 언어만큼이나 처리 속도가 빠르다고 각광받고는 있습니다만, 결국 언어 자체의 한계는 극복할 수 없기 때문에 해당 성능을 끌어올린 것은 비동기적인 처리가 우수하기 때문이라고 전에 공부했던 적이 있습니다. 이는 아마도 node.js가 언어와 엔진의 한계를 극복하기 위해 주 프로세스는 싱글 쓰레드를 유지하되 많은 동작을 비동기적으로 처리하는 것과 같은 이치라고 어렴풋이 생각하고 있습니다. 때문에 비동기 처리를 제대로 하지 못하면 FastAPI를 사용한 서버는 결국 언어적인 한계를 벗어나기 힘들 것이라 생각하고 있습니다. 덕분에 최초에는 동기적 드라이브를 통해 작성했던 db쪽 패키지를 비동기적 드라이버를 사용하도록 사양이 변경 되면서 한번 전반적인 수정을 해야하기도 했네요.
여기에 추가적으로 db의 스키마 관리를 migration을 통했으면 좋겠다는 요청도 들어왔습니다. 아무래도 sqlAlchemy의 table을 관리하는 모듈을 통해 마이그레이션이 관리가 될 수 있으니 장점이 있습니다만, 이런 마이그레이션 시스템을 사용할 때마다 그냥 db 스키마 자체를 들고 있고 해당 스키마를 기반으로 코드를 빌드하는 것이 더 깔끔하고 관리하기 용이한 형태가 아닌가 하는 생각을 하게 됩니다. 실물은 db의 스키마이고 이를 받아다 사용하는 것이 문제가 덜한 것 같은데 아마 이렇게 생각하는 것은 제가 db를 사용하면서 장애를 덜 경험해봐서일 지도 모르겠네요.
마이그레이션을 위해서는 많이들 사용하는 alembic이란 패키지를 사용했습니다. 아무래도 sqlAlchemy를 사용하는 경우의 용례의 대부분이 ORM을 기반으로 했기 때문에 core, asynchronous라는 비교적 덜 사용하는 조합을 선택해서 세팅을 하는 부분에 있어서 조금은 애를 먹었습니다.
마지막으로 신경 쓴 부분은 트랜잭션을 위한 db의 커넥션을 관리하는 것이었습니다. 가장 우선으로는 한 api에서는 동일한 db의 커넥션을 사용하도록 하되, 확장성을 고려해서 여러 repository 레이어의 처리를 한 커밋을 통해 관리할 수 있는 구성으로 구축하고 싶었습니다. 이 때문에 현재 서비스 레이어에 의존성으로 주입하고 있는 것은 engine.begin()이 아닌 engine.connect()를 받도록 하고 있습니다. 하나의 라우터에서 여러 서비스 레이어의 기능을 하나의 트랜잭션으로 묶어야하는 경우에 대해서는 당장에는 고려하고 있지 않고, 트랜잭션은 서비스 레이어의 기능 단위로 관리되고 있기 때문입니다.
다행히도 현재까지는 의도한대로 동작을 하고 있어서 트랜잭션이 필요한 write 작업에서는 필요한 만큼의 커밋과 롤백을, 비동기적인 read가 필요한 경우에는 asyncio.create_task(), asyncio.gather() 등을 통해 서비스 레이어가 레포지토리 레이어를 활용하도록 구성이 완료돼있는 상태입니다.
마무리
이번 회고록 1편에서는 FastAPI라는 환경 내에서 제 모자란 경험을 바탕으로 나름대로 구성한 경험을 공유했습니다. FastAPI 기반 서버리스 환경에서 비동기적인 성능 활용을 신경 쓰다보니 SQLAlchemy Core와 비동기 드라이버를 활용하며 평소에 잘 몰랐던 부분들에 대해 경험할 수 있는 기회가 돼 좋았네요.
아무래도 모자란 부분이 많을 수밖에 없는 만큼 개선하면 좋겠다 싶은 내용이 있으면 편하게 말씀해주시면 많은 도움이 될 것 같습니다. 또 혹시 이 글이 비슷한 고민을 하는 분들께 작게나마 도움이 되면 굉장한 기쁨일 것 같네요.
다음 2편에서는 본격적으로 WorkOS를 활용한 인증 API 구현 과정, 그리고 개발 과정에서 마주했던 문제들을 어떻게 해결해 나갔는지에 대한 이야기를 풀어낼 예정입니다