이미지 처리를 위한 파이썬 서버 프레임워크 선정기 with Django, FastAPI, Sanic

이미지 처리를 위한 파이썬 서버 프레임워크 선정기 with Django, FastAPI, Sanic

시작하며

안녕하세요. 카카오페이 데이터실 Jenson, Todd입니다.

카카오페이에서는 얼굴을 탐지하고 검증하는 기술을 활용해 고객 편의성과 안정성을 제공하고 있습니다. 저희는 얼굴 탐지 및 검증 기술을 서비스에 제공하는 프로젝트에서 서버를 개발하고 있습니다. (카카오페이 개발자센터에서 얼굴 인식 API를 제공하고 있으니 사용해보세요. 😎)

저희 서버는 현재 kotlin+spring boot를 사용하고 있습니다. 하지만 이미지 처리 코드 포팅 및 결과값 정합성 등의 이유로 Python 기반의 서버 프레임워크를 사용하는 것으로 결정하였고, 어떤 프레임워크를 사용할지에 대한 고민과 결정을 공유드리고자 합니다.

아래의 고민을 하고 있는 분들에게 많은 도움이 될 것 같습니다. 😀

  • 이미지 처리를 담당하는 서버를 구축해야 하는데, 언어/프레임워크 선정부터 고민이야.
  • java(kotlin)+spring boot를 사용하고 있는데, 개발 생산성이 부족한 것 같아. 다른 프레임워크 맛이나 볼까?

Why?

왜 저희가 Python 기반의 서버 프레임워크를 사용하게 되었는지 말씀드리겠습니다.

현재 저희 개발 Flow 입니다.

  1. 모델 학습 및 검증 (By data scientist)
  2. 이미지 처리 로직 Python으로 작성 (By data scientist)
  3. Python으로 작성된 코드 kotlin으로 포팅 (By ML engineer)
  4. 결과 정합성 검증 (By data scientist & ML engineer)
  5. 서버 개발 (By ML engineer)
  6. 실제 서비스 적용

kotlin+spring boot로 이미지 처리 서버를 개발하기 때문에 3, 4번과 같이 포팅, 정합성 검증하는 flow가 반드시 필요합니다. kotlin에서의 이미지 처리는 OpenCV를 자바에서 사용하기 쉽게 래핑한 JavaCV를 사용했고, 행렬 처리는 native code로 작성했습니다. 아무래도 kotlin에서는 이미지 처리 작업이 main job이 아니기 때문에 정보도 적고, 지원하지 않는 기능인 경우 native code로 작성하는 경우가 발생했습니다. 간단하게 예를 들어보면, 행렬의 축을 바꾸는 전치 코드를 Python에서 kotlin으로 포팅하여 비교해보겠습니다.

  • Python 코드
import numpy as np

np.transpose(array)
  • kotlin 코드
fun Array<IntArray>.transpose(): Array<IntArray> {
    val cols = this.first().size

    return this.fold(List(cols) { IntArray(0) }) { list, arr ->
        list.zip(arr.toList()) { a, b -> a + b }
    }.toTypedArray()
}

위와 같이 Python numpy에서 제공하는 행렬 연산들을 kotlin으로 포팅하기 위하여 native code를 작성하거나 JavaCV의 연산을 사용하게 됩니다. 포팅이 끝나면 이어서 결과값 검증을 진행합니다. 검증이 한번에 잘 끝나면 좋겠지만, 다르게 나오는 경우에는 과장을 약간 보태서 지옥을 맛보게 됩니다.(이미지 or 행렬 디버깅이 쉽지가 않습니다. 😭) 새롭게 이미지 처리 로직을 개발한다거나, 이미 개발되어 있는 이미지 처리 로직을 수정할 때마다 이러한 포팅 및 검증 작업을 세부적으로 나눠 작업하는 것이 서버 개발을 하는 ML engineer에게 항상 큰 부담이었습니다. 저희는 이러한 부담을 없애기 위하여 Python 기반의 서버 프레임워크를 사용하기 위해 테스트를 진행했습니다.

Python 기반의 서버 프레임워크를 사용하게 되면 개발 Flow가 이렇게 바뀌게 됩니다.

  1. 모델 학습 및 검증 (By data scientist)
  2. 이미지 처리 로직 Python으로 작성 (By data scientist)
  3. 이미지 처리 로직 적용 & 서버 개발 (By ML engineer)
  4. 실제 서비스 적용

사전 리서치

주요 팩터

저희 프로젝트에서 프레임워크 선정을 위한 팩터는 크게 4가지로 설정하였습니다.

  • 실제 운영 환경에서 사용할 수 있는 안정성
  • 기존 서비스보다 뛰어난 성능
  • 개발할 때 서로 이해하기 쉽고 작성하기 쉬운 생산성
  • 널리 사용되며 다양한 정보를 접하기 쉬운 생태계

이 글에서는 전반적으로 위의 팩터를 가지고 리서치 및 프레임워크 테스트를 진행하려 합니다.

성능

일단 가장 먼저 Python 서버 프레임워크의 벤치마크1를 확인했습니다. 실제 운영 환경과는 많이 다르겠지만 성능은 대충 이 정도라 예상하면 되겠구나~라고 참고해주세요. 😀

프레임워크 벤치마크
프레임워크 벤치마크

그래프를 봤을 때, 역시 동작 방식이 비동기인 Sanic, FastAPI의 성능이 가장 빨랐고, 동기인 Django, Flask가 뒤를 이었습니다.

생태계

이어서 대체로 프레임워크 생태계와 비례한 상관관계를 가지고 있는 GitHub stars2를 확인해봤습니다.

프레임워크 star history
프레임워크 star history

Djangoflask는 비슷한 시기에 개발이 시작되어 선형적으로 상승하고 있고, SanicFastAPI는 비교적 최근에 개발이 시작됐지만 Sanic에 비해 FastAPI가 훨씬 빠른 속도로 상승하고 있는 것을 확인했습니다.

결론

사전 리서치 단계에서의 두 가지 표를 참고하고 리서치 자료를 종합하여 아래 세 가지 프레임워크에 대한 테스트를 진행하기로 결정하였습니다.

  • Django: 이미 많은 기업에서 사용해 안정성이 보장되어 있으며 뛰어난 문서와 큰 생태계를 가지고 있는 동기 API 서버
  • FastAPI: 요즘 가장 빠른 상승세를 타고 있으며 빠른 성능높은 생산성을, 뛰어난 문서와 큰 생태계를 가지고 있는 비동기 API 서버
  • Sanic: 간단하고 가벼운 API로 개발이 가능하고 가장 빠른 성능높은 생산성을 가지고 있는 비동기 API 서버

저희는 RESTful API 개발을 목적으로 테스트를 진행하기 때문에 Django에서 RESTful API 서버를 간편하게 구축할 수 있도록 도와주는 Django Rest Framework(이하 DRF로 표기)를 사용했습니다.

테스트

4가지 주요 팩터인 안정성/성능/생산성/생태계안정성/성능은 실제 얼굴 인식 flow를 간단하게 구현하여 테스트를 진행하고, 생산성/생태계는 기능 테스트 및 테스트 개발하면서 느낀 점을 토대로 기술하도록 하겠습니다.

테스트 환경

test environment
test environment

테스트는 AWS Cloud 환경에서 진행하였습니다. 테스트 성능 측정에 사용될 클라이언트는 Locust를 이용하여 EC2(m5.2xlarge)에 구축했습니다.

용어 정의

테스트를 수행하기에 앞서, 자주 언급할 용어와 성능 지표에 대해서 정리하도록 하겠습니다.

  • User: Locust에서 사용되는 용어로, 요청을 보내는 유저로 생각하시면 됩니다. 유저의 수만큼 클라이언트에서 서버로 동시에 요청을 보내게 됩니다.
  • Throughput (RPS: Request Per Second): 초당 서버에서 평균적으로 처리한 요청 수를 의미합니다.
  • Latency: 클라이언트에서 보낸 요청이 처리되어 응답받는 데까지 걸린 평균 시간을 의미합니다. 단위는 ms입니다.

결과

1. 성능/안정성 테스트

기존 얼굴 인식 서비스는 I/O Bound(이미지 저장, Inference 서버 호출, DB 접근 등)가 CPU Bound(이미지 전처리 등)보다 큰 비중을 가지고 있습니다. 또한 하나의 transaction에서 DB INSERT 1회, UPDATE 1회 발생되며 savepoint, rollback 작업을 필요로 하지 않습니다. 테스트 Flow는 위와 같은 실제 서비스의 Flow를 담아내어 구성하였습니다.

test flow
test flow

  • I/O Bound는 mock 처리하여 테스트에 영향을 미치지 않도록 했고, Django는 sync로, FastAPI, Sanic은 async로 구현하여 테스트했습니다.
  • 얼굴 탐지, 얼굴 검증 및 스푸핑 탐지 모델 등 다양한 API call을 테스트하기 위하여 ML Model Inference API call을 여러 번 진행했습니다.
  • Response Timeout은 3000ms로 고정시키고 MultipartFile에 크기가 약 400KB인 이미지를 담아 진행했습니다.
    • 기존 얼굴 인식 서비스의 평균 이미지 크기보다 살짝 큰 400KB 크기의 이미지를 테스트 이미지로 선정했습니다.
  • 각 테스트 시 EC2 인스턴스는 고정하고 각 프레임워크마다 높은 RPS의 설정 값들을 적용해 비교하였습니다.
1.1. Load test

Load test는 정해진 환경에서 약 30분 동안 진행하였고, 임계점 판단을 위하여 4단계로 나눠 테스트했습니다.

1.1.1. Instance(CPU 2 core, mem 1GiB) - 8, User - 256

테스트 결과 - RPS, Latency
테스트 결과 - RPS, Latency

테스트 결과 - RPS median
테스트 결과 - RPS median

DRF, FastAPI, Sanic 모두 안정적인 RPS와 Latency를 보여주었습니다.

User 수를 늘려 더 극한 상황에서 테스트를 진행해보았습니다.

1.1.2. Instance(CPU 2 core, mem 1GiB) - 8, User - 512

테스트 결과 - RPS, Latency
테스트 결과 - RPS, Latency

테스트 결과 - RPS median
테스트 결과 - RPS median

차이는 극명해졌습니다. DRF의 RPS는 약 130정도, FastAPISanic의 RPS는 약 160정도로 측정되었습니다. 다만 리서치 과정에서 얻은 벤치마크와는 다르게 세 프레임워크 간의 드라마틱한 성능 차이는 없었습니다. 또한 DRF에서 Timeout이 많이 발생하였고, 예상보다 낮은 부하에서도 서비스가 원활하게 지원되지 않았습니다. 이러한 결과에 의문이 생겨 CPU core 수를 늘려 다시 테스트를 진행했습니다.

1.1.3. Instance(CPU 8 core, mem 1GiB) - 8, User - 512

테스트 결과 - RPS, Latency
테스트 결과 - RPS, Latency

테스트 결과 - RPS median
테스트 결과 - RPS median

다른 변경사항 없이 CPU core 수를 늘리니 RPS가 모두 증가했습니다. 세 프레임워크 모두 안정적인 모습을 보여줬기 때문에 더 큰 부하를 주어 테스트를 진행해보겠습니다.

1.1.4. Instance(CPU 8 core, mem 1GiB) - 8, User - 1024

테스트 결과 - RPS, Latency
테스트 결과 - RPS, Latency

테스트 결과 - RPS median
테스트 결과 - RPS median

Sanic, FastAPI, DRF 순서대로 RPS가 높았고, 세 프레임워크의 기대했던 드라마틱한 성능 차이는 없었습니다. 원하는 수준의 RPS까지 도달하는 것을 확인했습니다. 세 프레임워크 모두 여기서 부하를 더 주면 Error rate가 급격하게 증가했습니다.

1.1.5 결론

이미지가 Request에 담기는 얼굴 인식 서비스 특성상 대용량 Request을 처리해야 하기 때문에 인스턴스 설정(CPU core)이 RPS 성능에 큰 영향을 준 것으로 판단했습니다. 따라서 Request의 크기가 큰 경우 서버 프레임워크 성능 차이는 유의미하게 나지 않는다는 결론을 도출했고, 이에 따라 안정성을 확인하기 위하여 endurance test를 진행했습니다.

1.2 Endurance test

Endurance test는 정해진 환경에서 약 24시간 동안 진행했습니다. 운영 환경에서도 장시간 이슈없이 서비스가 제공되는지 확인하고자 테스트를 진행했습니다.

테스트 결과 - RPS, Latency
테스트 결과 - RPS, Latency

테스트 결과 - RPS median
테스트 결과 - RPS median

RPS, Throughput 그래프에서 세 프레임워크 모두 안정적으로 응답하며 서비스가 이슈없이 유지되는 것을 확인하였습니다.

정리하면

프레임워크 모두 실제 운영 환경에 올라가도 될만큼 성능이 준수하며 안정적임을 확인했습니다. 이에 따라 남은 팩터인 생산성생태계를 고려하여 최종 판단하고자 합니다.

2. 생산성/생태계 테스트

생산성, 생태계 테스트는 API 문서 자동화 기능과 serializer validation 기능에 대해 진행했습니다. 기능 테스트 결과와 테스트를 개발하면서 느낀 점도 함께 공유드리고자 합니다.

2.1. API 문서 자동화

개발 관련 소통을 진행해보면, 개발자의 설명도 물론 도움이 되지만 한눈에 들어오는 API 문서가 더 큰 도움을 주는 경우가 많은 것 같습니다. 하지만 개발자가 코드의 품질 관리와 기능 개발에 힘을 쏟다 보면 API 문서 작업은 백로그에 쌓이는 경우가 종종 있습니다. (저만 그렇지 않기를 빕니다. 😅)

기능 개발이 진행되는데 API 문서 최신화가 안된다면 소통 간 혼선을 빚게 되고 이러한 혼선을 막기 위하여 각 프레임워크에서 API 문서 자동화 기능을 제공하고 있습니다. 소통에 큰 도움을 주는 API 문서 자동화 기능 테스트를 진행했습니다. 결과는 아래와 같습니다.

자동화 가능 문서Request schemeResponse scheme비고
DRFSwagger, ReDocdrf serializer로 작성 / 추가 코드 작업 시 문서에서 스키마 제공drf serializer로 작성 / 추가 코드 작업 시 문서에서 스키마 제공규격에 맞게 추가 코드 작업 필요
FastAPISwagger, ReDocpydantic BaseModel로 작성 시 문서에서 스키마 제공pydantic BaseModel로 작성 시 문서에서 스키마 제공추가 코드 작업 없이 간편하게 문서 자동화
SanicSwagger, ReDocpydantic BaseModel로 작성 시 문서에서 스키마 제공pydantic BaseModel로 작성 시 문서에서 스키마 제공추가 코드 작업 없이 간편하게 문서 자동화

세 프레임워크 모두 만족할만한 API 문서 자동화 기능을 지원하고 있었습니다. 사용하기에도 모두 큰 불편함이 없는 관계로 이어서 serializer validation 기능 테스트를 진행했습니다.

DRF에서는 문서 자동화 기능을 사용하기 위해 공식 문서에서 제공하는 third party package인 drf-yasg를 사용했습니다.

2.2. Serializer validation

기존 저희의 얼굴 인식 서버에서는 서버 통신 간 암호화된 이미지 및 각종 정보들을 JSON String 형태로 request body에 담아 Object로 역직렬화하여 처리했습니다. 이 역할을 DRF에서는 drf serializer, FastAPI/Sanic에서는 pydantic이 담당합니다. 따라서 drf serializerpydantic의 validation 테스트를 진행했습니다. 이미지 크기 제한이 가능한지, nested object 구조도 가능한지 등의 테스트를 진행하였고, 두 프레임워크 간 유의미한 기능 차이는 없었습니다. 다만 테스트를 진행하면서 drf serializer는 객체마다 Serializer를 작성해주어야 하는 반면 pydantic은 BaseModel을 상속받기만 해도 되어 개발 생산성은 pydantic이 더 좋았습니다.

추가로 두 프레임워크 간 속도 차이를 알아보기 위한 테스트도 진행했습니다. pydantic, drf serializer를 활용하여 같은 형태의 객체로 각각 역직렬화를 1,000,000건 수행하였고, pydanticdrf serializer보다 역직렬화 속도가 약 26배 빨랐습니다. 직렬화, 역직렬화 작업 속도는 pydantic이 압도적으로 빠릅니다만, 실제 요청의 latency에 유의미한 영향을 줄만큼 비중이 크지는 않습니다.

참고 1) 역직렬화 테스트 수행 결과는 아래와 같습니다.

역직렬화 테스트 수행 결과
역직렬화 테스트 수행 결과

참고 2) 약간의 코드 작업이 필요하지만 validation, deserialization은 DRF에서도 pydantic으로 사용 가능합니다.

종합하면

하나의 표로 정리하면 아래와 같습니다.

DRFFastAPISanic비고
안정성GoodGoodGoodEndurance test 결과 참고
성능SosoGoodGoodLoad test 결과 참고
생산성SosoGoodGood
생태계GoodGoodBad필자의 주관이 소량 첨가되었습니다. 😅

저희는 테스트 결과에 따라 이미지 처리를 위한 서버 프레임워크를 안정성, 성능, 생산성, 생태계 모두 만족스러웠던 FastAPI로 결정하였습니다.

마치며

지금까지 이미치 처리 서버 개발을 위한 Python 서버 프레임워크에 대한 리서치 및 테스트를 진행해보았습니다.

리서치 시작 단계에서 FastAPI, Sanic의 성능이나 생산성에는 의문이 없었지만 생긴 지 오래되지 않은 프레임워크이다 보니 안정성에 의문과 걱정이 있었습니다. 그렇다 보니 아무래도 ‘높은 안정성과 방대한 생태계를 갖고 있는 DRF를 최종 선택하지 않을까.’ 생각했었습니다. 하지만 테스트를 진행하면서 FastAPI, Sanic성능은 물론이며 충분히 안정성도 뛰어나다는 결론을 도출했습니다. 최종적으로는 생산성과 생태계까지 고려하여 FastAPI로 결정하게 되었습니다. 이미지 처리를 담당하는 서버를 구축하시려는 분들, Python 기반의 서버 프레임워크를 개발하고자 하시는 분들에게 좋은 인사이트가 되기를 바라며 이 글을 마칩니다. 다음 글은 FastAPI를 활용한 이미지 처리 서버 개발 및 운영에 대한 회고로 돌아오겠습니다.

감사합니다.

Footnotes

  1. 출처: https://www.techempower.com/benchmarks/#section=data-r20&hw=ph&test=fortune&l=zijzen-sf&f=zhawhj-zik0zj-zik0zj-zijzen-zijunz-zik0zj-zik0zj-ziimf3-zik0zj-zih7nj-73-0

  2. 작성 날짜인 2022년 7월 기준의 history 입니다. (출처: https://star-history.com/#tiangolo/FastAPI&Django/Django&Sanic-org/Sanic&pallets/flask&Date)

jenson.lee
jenson.lee

카카오페이 얼굴인식 기술을 바탕으로 다양한 서비스를 만들어나가고 있는 젠슨입니다.

todd.zz
todd.zz

제가 개발하는 서비스가 세상에 이로운 영향을 주기를 바라는 토드입니다. 지금은 얼굴인식 기술을 바탕으로 실생활을 더욱 편리하게 만들어 주는 서비스를 개발하고 있습니다.