카카오페이에서는 얼굴을 탐지하고 검증하는 기술을 활용해 고객 편의성과 안정성을 제공하고 있습니다. 저희는 얼굴 탐지 및 검증 기술을 서비스에 제공하는 프로젝트에서 서버를 개발하고 있습니다. (카카오페이 개발자센터에서 얼굴 인식 API를 제공하고 있으니 사용해보세요. 😎)
저희 서버는 현재 kotlin+spring boot를 사용하고 있습니다. 하지만 이미지 처리 코드 포팅 및 결과값 정합성 등의 이유로 Python 기반의 서버 프레임워크를 사용하는 것으로 결정하였고, 어떤 프레임워크를 사용할지에 대한 고민과 결정을 공유드리고자 합니다.
아래의 고민을 하고 있는 분들에게 많은 도움이 될 것 같습니다. 😀
이미지 처리를 담당하는 서버를 구축해야 하는데, 언어/프레임워크 선정부터 고민이야.
java(kotlin)+spring boot를 사용하고 있는데, 개발 생산성이 부족한 것 같아. 다른 프레임워크 맛이나 볼까?
kotlin+spring boot로 이미지 처리 서버를 개발하기 때문에 3, 4번과 같이 포팅, 정합성 검증하는 flow가 반드시 필요합니다. kotlin에서의 이미지 처리는 OpenCV를 자바에서 사용하기 쉽게 래핑한 JavaCV를 사용했고, 행렬 처리는 native code로 작성했습니다. 아무래도 kotlin에서는 이미지 처리 작업이 main job이 아니기 때문에 정보도 적고, 지원하지 않는 기능인 경우 native code로 작성하는 경우가 발생했습니다. 간단하게 예를 들어보면, 행렬의 축을 바꾸는 전치 코드를 Python에서 kotlin으로 포팅하여 비교해보겠습니다.
Python 코드
import numpy as npnp.transpose(array)
kotlin 코드
fun Array<IntArray>.transpose(): Array<IntArray> {val cols =this.first().sizereturnthis.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가 이렇게 바뀌게 됩니다.
기존 얼굴 인식 서비스는 I/O Bound(이미지 저장, Inference 서버 호출, DB 접근 등)가 CPU Bound(이미지 전처리 등)보다 큰 비중을 가지고 있습니다. 또한 하나의 transaction에서 DB INSERT 1회, UPDATE 1회 발생되며 savepoint, rollback 작업을 필요로 하지 않습니다. 테스트 Flow는 위와 같은 실제 서비스의 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의 설정 값들을 적용해 비교하였습니다.
차이는 극명해졌습니다. DRF의 RPS는 약 130정도, FastAPI와 Sanic의 RPS는 약 160정도로 측정되었습니다. 다만 리서치 과정에서 얻은 벤치마크와는 다르게 세 프레임워크 간의 드라마틱한 성능 차이는 없었습니다. 또한 DRF에서 Timeout이 많이 발생하였고, 예상보다 낮은 부하에서도 서비스가 원활하게 지원되지 않았습니다. 이러한 결과에 의문이 생겨 CPU core 수를 늘려 다시 테스트를 진행했습니다.
이미지가 Request에 담기는 얼굴 인식 서비스 특성상 대용량 Request을 처리해야 하기 때문에 인스턴스 설정(CPU core)이 RPS 성능에 큰 영향을 준 것으로 판단했습니다. 따라서 Request의 크기가 큰 경우 서버 프레임워크 성능 차이는 유의미하게 나지 않는다는 결론을 도출했고, 이에 따라 안정성을 확인하기 위하여 endurance test를 진행했습니다.
개발 관련 소통을 진행해보면, 개발자의 설명도 물론 도움이 되지만 한눈에 들어오는 API 문서가 더 큰 도움을 주는 경우가 많은 것 같습니다. 하지만 개발자가 코드의 품질 관리와 기능 개발에 힘을 쏟다 보면 API 문서 작업은 백로그에 쌓이는 경우가 종종 있습니다. (저만 그렇지 않기를 빕니다. 😅)
기능 개발이 진행되는데 API 문서 최신화가 안된다면 소통 간 혼선을 빚게 되고 이러한 혼선을 막기 위하여 각 프레임워크에서 API 문서 자동화 기능을 제공하고 있습니다. 소통에 큰 도움을 주는 API 문서 자동화 기능 테스트를 진행했습니다. 결과는 아래와 같습니다.
자동화 가능 문서
Request scheme
Response scheme
비고
DRF
Swagger, ReDoc
drf serializer로 작성 / 추가 코드 작업 시 문서에서 스키마 제공
drf serializer로 작성 / 추가 코드 작업 시 문서에서 스키마 제공
규격에 맞게 추가 코드 작업 필요
FastAPI
Swagger, ReDoc
pydantic BaseModel로 작성 시 문서에서 스키마 제공
pydantic BaseModel로 작성 시 문서에서 스키마 제공
추가 코드 작업 없이 간편하게 문서 자동화
Sanic
Swagger, ReDoc
pydantic BaseModel로 작성 시 문서에서 스키마 제공
pydantic BaseModel로 작성 시 문서에서 스키마 제공
추가 코드 작업 없이 간편하게 문서 자동화
세 프레임워크 모두 만족할만한 API 문서 자동화 기능을 지원하고 있었습니다. 사용하기에도 모두 큰 불편함이 없는 관계로 이어서 serializer validation 기능 테스트를 진행했습니다.
DRF에서는 문서 자동화 기능을 사용하기 위해 공식 문서에서 제공하는 third party package인 drf-yasg를 사용했습니다.
기존 저희의 얼굴 인식 서버에서는 서버 통신 간 암호화된 이미지 및 각종 정보들을 JSON String 형태로 request body에 담아 Object로 역직렬화하여 처리했습니다. 이 역할을 DRF에서는 drf serializer, FastAPI/Sanic에서는 pydantic이 담당합니다. 따라서 drf serializer와 pydantic의 validation 테스트를 진행했습니다. 이미지 크기 제한이 가능한지, nested object 구조도 가능한지 등의 테스트를 진행하였고, 두 프레임워크 간 유의미한 기능 차이는 없었습니다. 다만 테스트를 진행하면서 drf serializer는 객체마다 Serializer를 작성해주어야 하는 반면 pydantic은 BaseModel을 상속받기만 해도 되어 개발 생산성은 pydantic이 더 좋았습니다.
추가로 두 프레임워크 간 속도 차이를 알아보기 위한 테스트도 진행했습니다. pydantic, drf serializer를 활용하여 같은 형태의 객체로 각각 역직렬화를 1,000,000건 수행하였고, pydantic이 drf serializer보다 역직렬화 속도가 약 26배 빨랐습니다. 직렬화, 역직렬화 작업 속도는 pydantic이 압도적으로 빠릅니다만, 실제 요청의 latency에 유의미한 영향을 줄만큼 비중이 크지는 않습니다.
참고 1) 역직렬화 테스트 수행 결과는 아래와 같습니다.
역직렬화 테스트 수행 결과
참고 2) 약간의 코드 작업이 필요하지만 validation, deserialization은 DRF에서도 pydantic으로 사용 가능합니다.
지금까지 이미치 처리 서버 개발을 위한 Python 서버 프레임워크에 대한 리서치 및 테스트를 진행해보았습니다.
리서치 시작 단계에서 FastAPI, Sanic의 성능이나 생산성에는 의문이 없었지만 생긴 지 오래되지 않은 프레임워크이다 보니 안정성에 의문과 걱정이 있었습니다. 그렇다 보니 아무래도 ‘높은 안정성과 방대한 생태계를 갖고 있는 DRF를 최종 선택하지 않을까.’ 생각했었습니다. 하지만 테스트를 진행하면서 FastAPI, Sanic의 성능은 물론이며 충분히 안정성도 뛰어나다는 결론을 도출했습니다. 최종적으로는 생산성과 생태계까지 고려하여 FastAPI로 결정하게 되었습니다. 이미지 처리를 담당하는 서버를 구축하시려는 분들, Python 기반의 서버 프레임워크를 개발하고자 하시는 분들에게 좋은 인사이트가 되기를 바라며 이 글을 마칩니다. 다음 글은 FastAPI를 활용한 이미지 처리 서버 개발 및 운영에 대한 회고로 돌아오겠습니다.