배경
사내에서 개발 완료 후, 테스트를 진행하게 되었습니다. 제가 테스트를 할 때는 잘되었는데, 다른 개발자가 테스트를 할 때 정상적으로 동작하지 않았습니다. 화면에서 서버에 데이터를 넘기는데, body의 특정 속성이 0 ~ 10000 사이즈 까지 넘길 수 있었습니다. 해당 데이터에 500정도 사이즈를 넘겼을 때는 정상적으로 동작을 했지만, 최대값에 근접한 사이즈로 데이터를 넘길 때면 에러가 발생하곤 했습니다. 제가 테스트 할 때는 사이즈를 크게 안 넣었고, 다른 개발자가 테스트 할 때는 최대 사이즈로 값을 넣어서 에러가 발생한 것이었습니다.
Spring 프레임워크를 사용한 서비스에서는 해당 사이즈에 대한 에러가 발생하지 않았지만, python 프레임워크로 개발한 서비스에서는 발생한 점이 신기했습니다. 그래서 왜 Python 프레임 워크로 개발한 서비스만 발생하였는지 확인해보니, WebOb이라는 라이브러리와 관련이 있다는 것을 알게되었습니다.
왜 Python 프레임워크에서 발생했는지 간략하게 얘기해보겠습니다.
Spring
요청 및 응답
대부분 사람들이 알고 있듯이, 보편적인 Spring 프레임 워크의 요청 및 응답 구조를 간략하게 보면 다음과 같습니다.
클라이언트의 요청이 apache/nginx 웹 서버로 요청이 들어옵니다. 정적 파일 요청의 경우 직접 처리가 가능하고, 동적 처리 요청의 경우 tomcat으로 전달합니다. tomcat에서 받은 요청은 Spring application으로 전달을 하고 로직 처리 후에 다시 응답을 하는 구조 입니다.
Python Web application
요청 및 응답
Python 프레임 워크(Django, Flask, FastAPI 등)의 경우 다음과 같은 요청 및 응답 구조를 갖고 있습니다. 위와 동일하게 정적 파일 요청의 경우 직접 처리가 가능하고, 동적 처리 요청의 경우 WSGI 서버로 전달합니다. WSGI는 요청을 Python 애플리케이션(Django, Flask 등)으로 전달합니다. 전달받은 요청을 애플리케이션이 처리하고 응답하는 구조입니다.
※ WSGI란?
Web Server Gateway Interface 약자로, 말 그대로 Web Server와 Python 웹 어플리케이션의 통신을 위한 표준 인터페이스입니다. 다양한 웹 서버와 Pytohn 웹 어플리케이션을 통신해 주기 위해 나오게 되었습니다.
WebOb
WebOb은 WSGI 기반 어플리케이션에서 HTTP 요청과 응답을 처리하기 위한 라이브러리로, Python application에서도 사용 가능합니다. 위 그림으로 보면 WSGI → Python application 이 통신을 할 때 사용하게 됩니다. 표준으로 정해진 것이 아니여서 필수적으로 사용할 필요는 없습니다. 제 프로젝트의 경우에는 WSGI와 Python application 사이에 있는 WSGI middleware에서 WebOb을 통해서 응답과 요청을 주고 받고 있었습니다.
그럼 WebOb의 어떤 코드가 에러와 관련이 있었을까요?
코드는 여기서 확인할 수 있습니다.
https://github.com/Pylons/webob/blob/main/src/webob/request.py#L959
WebOb의 코드 중 다음과 같은 코드를 볼 수 있습니다.
def copy_body(self):
...
tempfile_limit = self.request_body_tempfile_limit
todo = self.content_length if self.content_length is not None else 65535
newbody = b""
fileobj = None
input = self.body_file
while todo > 0:
data = input.read(min(todo, 65535))
...
if fileobj:
fileobj.write(data)
else:
newbody += data
# When we have enough data that we need a tempfile, let's
# create one, then clear the temporary variable we were
# using
if len(newbody) > tempfile_limit:
fileobj = self.make_tempfile()
fileobj.write(newbody)
newbody = b""
...
위 코드를 간략하게 보면, content_length가 65535보다 크면 새로운 tempfile 을 만든다는 코드입니다. Unix 계열의 서버는 /tmp 디렉토리를 사용하고, Windows에서는 C:\Users\<사용자>\AppData\Local\Temp 를 기본적으로 사용한다고 합니다.
왜 이런 제한이 있을까?
왜 WebOb에서는 64KB로 제한을 하였을까요?
서버의 메모리 부담을 줄이고 안정성을 높이기 위해서 제한을 하였는데요. 64KB가 넘어가면 메모리가 아닌 디스크에 임시 파일로 저장을 하기에 메모리 부담을 줄일 수 있습니다.
그럼 spring에서는 제한이 없는지 궁금할 수 있는데요. 보통 spring과 같이 사용하는 was 인 tomcat에도 2MB로 기본 제한이 있습니다. 또한 spring 설정을 통해 자체적으로 제한을 할 수도 있습니다.
여기서의 size 제한은 web server인 apache/nginx에서 요청 크기 제한이랑은 다르다는 점을 알아야합니다.
(apache/nginx의 경우 요청 크기 제한을 걸고 크기가 초과하면 413 ERROR 발생, webob이나 tomcat은 임시 파일에 저장)
마치며...
webob, tomcat 다른 was에도 요청 크기 제한이 있는데, 크기를 초과하면 임시파일에 저장해서 사용하는 것 뿐인데, 이게 왜 에러를 발생하였을까요?
보통 서버를 구동할 때 k8s를 사용하여 구동을 하는데, 해당 pod 에 /tmp 임시 디렉토리에 대한 권한이 없어 64KB 크기를 초과한 요청에 대해서 임시 파일로 저장을 못하여 발생한 문제였습니다.
이번 에러를 해결하면서, 서버마다 크기 제한을 넘으면 임시 파일에 저장하고 처리를 한다는 것을 알게되었습니다. 또한 알고리즘 문제를 풀 때마다, 범위가 주어지면 최소값, 최대값을 넣어서 확인을 했었는데... 기능 테스트할 때도 다양하게 테스트를 하자고 다짐하였습니다.
참고자료
https://github.com/Pylons/webob/blob/main/src/webob/request.py