이 글은 PyCon Korea 2025에서 진행된 <초짜 파이썬 개발자가 FastAPI로 서비스 만들면서 겪은 좌충우돌 삽질기> 세션을 정리한 내용입니다. 처음으로 파이썬과 FastAPI를 이용해 백엔드 서비스를 만들어야 했던 발표자의 솔직한 경험들을 소개합니다. 발표 자료는 PyCon Korea 2025 공식 홈페이지에서 확인할 수 있으며, 추후 파이콘 한국 유튜브 채널을 통해 영상으로도 만나보실 수 있습니다.
초짜 파이썬 개발자가 FastAPI로 서비스 만들면서 겪은 좌충우돌 삽질기
김대현 개발자
안녕하세요, 17년 차 개발자 김대현입니다. 꽤 오랜 시간 개발자라는 이름으로 살아왔지만, 저의 기술 스택은 꽤나 복잡합니다. Visual Basic, C++, Java, PHP, NodeJS 등 다양한 언어와 환경을 거쳐 지금은 Python 기반의 백엔드 프로젝트를 주로 맡고 있습니다.
오늘 여러분께 들려드릴 이야기는, 깊이 있는 지식 없이 파이썬과 FastAPI를 이용해 백엔드 서비스를 만들어야 했던 저의 경험담입니다. 맛만 보던 파이썬을 본격적으로 다루면서 겪었던 시행착오 등을 솔직하게 공유하고자 합니다. 특히나 저처럼 파이썬과 FastAPI를 이용해, 웹서비스를 처음 개발해야 하는 분들에게 도움이 되길 바랍니다.
제가 만들게 된 것은 공장 자동화 시스템의 백엔드 서비스입니다. 파이썬 생태계에서 Django가 여전히 최고의 지위를 가지고 있지만, 학습 곡선이 상대적으로 낮고 React와 앱으로 프론트엔드가 분리되어 REST API만 제공하면 되는 프로젝트의 특성을 고려했을 때, FastAPI가 매력적인 선택이었습니다. 그리고 개인적으로는 코드 구조가 한번 경험해 봤던 NodeJS의 Express와 비슷한 느낌을 받기도 했습니다.
어떤 기술 기반을 선택하든 결국 중요한 것은 잘 만들어내는 것이겠죠. 아래는 저희 프로젝트의 초기 패키지 구조입니다.

여기서 FastAPI 공식 문서에서는 API endpoint들을 모아놓은 패키지에 "router"라는 단어를 사용하지만, 저는 "controller"를 사용했습니다. 개발자 관점에서 "router"는 네트워크 용어에 가깝다고 생각했고, 설계 목표를 달성하기 위해 단어 선택에도 신중을 기했다 말하고 싶습니다. 이 프로젝트를 진행하면서 스스로 세운 목표는 기초적인 CRUD 작업을 4시간 안에 끝내는 것이었고, 정말로 4시간 안에 되긴 됐습니다. 물론 '되긴 됐다'라는 말에 담긴 수많은 문제들은 나중에야 알게 됐습니다.
FastAPI를 처음 다루면서 생각지도 못한 DB 예외를 만났습니다. 프론트엔드에서 API를 두 번 호출하면 “This session is provisioning a new connection; concurrent operations are not permitted”라는 SQLAlchemy 예외가 발생하는 것이었습니다. 저는 FastAPI 들어오는 HTTP 요청이 고유의 작업 단위로 처리되고, 요청마다 새로운 데이터베이스 연결을 생성하도록 구현했으니 이런 예외가 발생할 리 없다고 생각했습니다.
결론적으로 제가 잘못 이해하고 있었습니다. 웃으셔도 좋습니다. 제 무지함이 빚어낸 참사였죠. 차라리 이렇게 “잘못” 코딩했다면, 불필요한 세션 낭비는 있을지언정 예외는 안 터졌을 것 같습니다.

익숙하지 않은 언어의 함정이었죠. 파이썬의 클래스 변수와 인스턴스 변수의 차이를 제대로 이해하지 못했던 것이 원인이었습니다. 그러니까 똑바로 알고 써야 합니다. 모르면 AI를 붙잡고서라도 물어봐야 합니다.

저는 ORM을 쓰는 쪽이 편합니다. 간단한 CRUD 작업에 용이하고 테이블 구조 파악이 쉽기 때문이죠. 하지만 쿼리가 복잡해지면 ORM은 몇 가지 불편함을 초래합니다. 조인 쿼리 생성을 위한 코드가 길어진다거나, Lazy Loading으로 인한 N+1 쿼리 문제가 대표적입니다.
처음에는 ORM 클래스 안에 데이터를 조회하는 코드를 넣는 시도를 했습니다. 문제는 이런 참조 관계들이 서로 얽혀 상호 참조 문제를 만들어냈다는 것이죠.

결국 쿼리하는 코드를 전부 분리하게 되었습니다.

하지만 이 접근 방식에도 아쉬움이 있었습니다. 처음에는 DB 쿼리를 모두 함수로 랩핑하니 코드가 짧을 때는 깔끔했지만, 코드가 늘어날수록 문제가 생기기 시작했습니다. 불필요한 데이터를 조회하거나, 함수를 수정했더니 다른 부분에서 오류가 발생하고, 새 함수를 만들었더니 이미 비슷한 코드가 존재하는 중복 문제도 생겼습니다. 결국 설계상의 트레이드오프만 더 힘들어지는 현상이 발생했습니다.

그리고 결정적으로 의도한 코드가 아니었습니다. 비즈니스 로직은 모델을 모르게 하고 싶었는데, 실제로는 그렇지 못했습니다. 그래서 지금은 Service에서 직접 데이터를 조회합니다. 한 번 더 DTO를 끼운 레이어를 나눌까 싶었으나, 코드 분량만 증가하는 결과를 만들어 냈거든요. 그래서 필요에 맞춰 작동해야 하는 코드를 분산시킬 것이 아니라, 필요를 가지고 있는 Service에 집중시키자는 결론에 도달했습니다. 그래도 이 과정속에 작은 성과가 있다면, DB를 쿼리하는 코드들을 하나로 묶어 SqlAlchemy 쿼리 빌더형태로 변환해 주는 라이브러리를 만들 수 있었다는 겁니다.
파이썬에서는 Everything in python is an object라는 말이 있습니다. 그래서 그런가 대부분의 파이썬 코드 예제는 함수로 되어있습니다. FastAPI 예제도 예외는 아니죠. 물론 데이터 객체는 클래스로 선언하지만, 그 외는 모두 함수입니다. 이렇게 만들면 "코드가 짧아서 가독성과 유지보수가 좋다"고 하지만, 더 큰 이유가 있지 않을까 생각했습니다. 그럼에도 불구하고 가급적 모든 코드들을 클래스로 묶은 구조로 만들었습니다.
이렇게 클래스로 묶어 두는 것도 이점은 분명히 있습니다. 비즈니스 로직의 성격에 따라 하나의 클래스에 모을 수 있고, 무엇보다 클래스 생성자 DB 세션과 사용자 인증 상태를 전달하여 State로 관리할 수 있었습니다. 추가적으로 패키지의 목적성에 집중하는 데에도 도움이 됐습니다. Controller는 REST API 처리에 집중하고, Service는 비즈니스 로직에 집중하는 것이죠.
Test Driven Development(TDD)는 늘 지향하지만, 막상 해보려면 잘 안되는 주제라고 생각합니다. 물론 저 역시 잘 못합니다. 테스트 코드의 중요성에는 동의하지만, 처음 파이썬, FastAPI, SQLAlchemy를 한 번에 다루다 보니 아무래도 한계가 오더군요. 무엇보다 '패키지 단위로 테스트하려면 어떻게 하지?'라는 고민에 불필요할 정도로 집착하다, 결국 초반에는 Postman에 의존해서 작업했습니다.
여기서 초기 학습 단계에서 누구나 겪는 '고통의 수레바퀴'를 소개해 보겠습니다. 이 수레바퀴는 다음과 같은 구조로 되어있습니다.
이 과정을 반복하는 것이죠. 두 번 정도 이 과정을 반복하다 보니, 테스트 코드 도입이 시급함을 느꼈습니다. 수동으로 테스트하다 보니 여기에 소모되는 시간이 기하급수적으로 늘어나고 있었거든요. 그래서 개발 건을 미뤄놓고 테스트 코드부터 작성하기 시작했습니다. 처음에는 Pytest만 쓰다가 곧이어 Tox를 추가했습니다.
Tox는 다양한 환경(버전)과 패키지에 맞춰 테스트가 가능하며, 실제로는 pytest를 실행합니다. Tox의 효과를 느낄 기회는 생각보다 빨리 찾아왔습니다. 실수로 파이썬 3.9에서 3.11로 버전업 시킨 적이 있었는데, Tox 덕분에 상황에 대응할 수 있었거든요.
그래서 처음엔 급한 대로 테스트 코드를 추가하고, 지속적으로 작업을 조금씩 진행하여 최종적으로 테스트 커버리지를 86%까지 끌어올렸습니다. 지금은 조금 떨어졌지만요. 그래도 테스트 코드 덕분에 반복적으로 코드 구조를 개선하는 데 부담이 없다는 것이 가장 큰 이득이었습니다. 늦게라도 테스트 코드를 넣기로 한 것은 정말 좋은 선택이었다고 생각합니다.

위 이미지는 IT업계에서 잘 알려진 밈으로, PHP에 대한 비판을 유머러스하게 표현한 그림입니다. 실제로 PHP와 CodeIgniter로 개발할 때는 HTML 안에 PHP 스크립트를 직접 끼워 넣는 구조 때문에, 서버가 200 응답을 반환하지 않는 문제를 단순히 try-catch로 해결하기 어려운 상황이 자주 발생했습니다.
이런 사정을 모르면 엉뚱한 답을 하게 됩니다. 예를 들어, “서버는 어떤 상황에서도 정상적으로 작동한다고 가정하고, 클라이언트에서 응답의 body를 기준으로 판단하는 게 더 이상적입니다.” 같은 말이죠. 부끄럽지만, 아무것도 모르던 시절의 제 이야기이기도 합니다. 이 프로젝트도 처음엔 그렇게 흘러갈 뻔했지만, 프론트엔드를 담당하던 동료의 피드백 덕분에 다행히 실수를 피할 수 있었습니다. 결론적으로, 좋은 동료가 있다는 건 잘못된 설계를 막아주는 가장 큰 안전장치입니다.
물론 REST API 규격 자체가 강제성은 없기에, 200으로 응답하는 내부 페이로드에 404 같은 에러 코드를 포함해도 기술적으로는 문제가 없습니다. 하지만 이런 방식은 표준적인 규격이라고 보기 어렵습니다. 그렇다면 프론트엔드에서 매번 페이로드를 확인해, 오류를 판단하는 것이 과연 올바른 접근일까요?
이 프로젝트에서는 Controller와 Service에 각각의 DTO가 존재합니다. Controller DTO는 JSON을 Object로 변환하여 유효성 검사 등을 처리하고, Service DTO는 Model에 전달해 주는 데이터이자 Response body를 구성하는 Object의 역할을 합니다.

이것을 전부 직접 구현한다면 [Controller DTO -> Service DTO -> ORM Model] / [ORM Model -> Service DTO -> Controller DTO]로 변환하는 과정에서 유효성 검사, 데이터 복사 코드가 통제 불능으로 늘어나기 시작합니다.
하지만 Pydantic에 이미 필요한 기능들이 있었습니다. 데이터 변환 시에는 model_validator, 값을 체크할 때는 @field_validator, 합계, 평균 같은 간단한 연산을 묶을 때는 @computed_field를 활용하면 됩니다. 있는 것을 잘 쓰면 되죠.
오늘 이야기한 삽질은 빙산의 일각에 불과합니다. 다시 한번 꺼내보는 '고통의 수레바퀴'는 왜 반복되는 걸까요? 흔히 "코딩은 문제 해결 도구"라고 하지만, 우리는 종종 "문제 해결"을 잊어버립니다. 마치 코딩에 중력이라도 있는 듯이 말이죠.
저도 종종 '잘못된 결정’을 저지릅니다. 작은 에피소드입니다만, 백엔드에서 생성한 파일을 S3에 저장하는 기능을 만들 때의 일입니다. 처음엔 파일명의 중복을 피하기 위해 UUID로 파일이름을 생성하도록 했습니다. 문제는 파일 이름이 UUID이다보니 무슨 파일인지 파일명만 보고는 알 수가 없었죠. 그러나 조금만 생각해 보면, S3는 오브젝트 이름이 unicode가 기본이기에, 파일명을 한글로 만들어도 문제가 되지 않습니다. 재생성 시 기존 파일을 덮어쓰는 문제를 피하려면, 타임스탬프를 pre/postfix로 넣으면 되고요. 이런 간단한 문제 해결을 잊어버리는 것이죠.
문제 해결의 본질을 잊어버리면, 처음 미숙했던 시절의 ‘시행착오’가 어느새 익숙한 습관적 코딩, 즉 ‘코딩 차력쇼’로 변해버립니다. 그래서 저는 늘 ‘흰 띠를 매는 자세’, 초심으로 돌아가는 태도가 필요하다고 생각합니다.
책 <프로그래머의 길, 멘토에게 묻다>에는 이런 문장이 나옵니다.
“새로운 것을 배우려면 우리는 과거의 경험과 선입견을 한켠으로 밀어 둘 수 있어야 한다.”
여기에 한 가지를 덧붙이자면, 우리가 배우는 목적은 코딩 자체가 아니라, ‘문제 해결’을 위한 것임을 잊지 말아야 한다는 점입니다. 그러면 기술자로서의 성장이 따라옵니다. 아직도 코드는 엉망진창이지만, 이 엉망진창을 조금이라도 ‘유려하고 깔끔함’으로 만드는 법을 배우는 과정이죠. 우리 선배들이 결국 답을 찾았듯, 우리도 언젠가는 그 답을 찾아갈 수 있을 겁니다.
©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.