콘텐츠로 건너뛰기
Home » aiohttp에서 큰 파일을 업로드하는 법

aiohttp에서 큰 파일을 업로드하는 법

웹브라우저를 비롯하여 HTTP를 사용하는 애플리케이션에서 서버로 파일을 업로드하려 할 때에는, 업로드하려는 파일은 이 요청의 botdy 부분에 붙여서 POST 요청으로 서버에 전달된다. 서버 사이드에서는 이러한 요청에서 첨부 파일을 얻기 위해서는 요청 본문을 파싱하여 처리한다. aiohttp에서는 다음과 같이 post를 처리하는 request.post()라는 핸들러를 사용해서 POST 요청의 데이터 본문을 얻게 된다. 폼 전송으로 전송된 파일은 HTTP multipart/form-data 방식으로 필드별로 구분하여 구분되기에 데이터 본문 중에서 파일에 해당하는 필드를 찾아서 데이터를 읽을 수 있다.

async def store_mp3_handler(request):
  data = await request.post()
  mp3 = data['mp3']
  filename = mp3.filename
  mp3_file = mp3.file
  content = mp3_file.read()
  return web.Response(body=content, headers=
     MultiDict({ 'CONTENT-DISPOSITION': mp3_file}))

여기서 문제는 request.post() 메소드가 요청 데이터를 한꺼번에 메모리로 읽어들이기 때문에 메모리 부족으로 서버가 죽을 수 있는 상황이 있다는 것이다. 따라서 aiohttp에서는 기본적으로 처리할 수 있는 요청의 크기는 2MB로 제한하고 있다. 하지만 이 크기는 어지간한 사진 하나의 용량도 감당하기 어렵기 때문에 뭔가 다른 방법이 필요하다. (보통은 일종의 옵션 값 같은 걸로 최대 처리 요청 크기를 변경할 수 있을 줄 알았는데, 없었다.)

request.multipart는 HTTP의 multipart-formdata와는 다르게 전체 데이터를 여러 조각으로 나누고 이를 처리하는 방식을 제공하는 API이다. 따라서 2MB보다 큰 요청이 왔을 때, 이를 처리하기에 알맞다. 먼저 리더 객체를 생성하고, 이 리더를 통해서 데이터를 조각조각별로 조금씩 처리하여 불러올 수 있다. requrest.multipart() 메소드는 멀티파트 요청을 각 파트별로 리턴하는 비동기 제너레이터인데, 각각의 파트(필드)는 read() 메소드를 통해서 통째로 읽어들이거나, read_chunk() 메소드를 통해서 필드의 일부분을 순차적으로 버퍼에 읽어들일 수 있다.

BodyPartReader.read_chunk(size=chunk_size::int)


https://docs.aiohttp.org/en/stable/multipart_reference.html#aiohttp.BodyPartReader.read_chunk

await request.post() 는 요청 전체를 한 번에 처리하기 때문에, POST 요청 데이터의 각 필드를 모두 구분하여 값을 가지고 있다. 하지만 멀티파트 리더를 사용하는 경우에는 아직 요청을 처리하지 않았기 때문에 data['mp3'] 와 같이 특정한 필드를 바로 얻을 수가 없다. 앞에서부터 조금씩 읽으면서 필요한 필드가 나올 때 까지 넘어가야 하는 것이다.

아래 예제에서는 reader.next() 를 사용해서 multipart-formdata 에서 각각의 파트(필드)를 처리하거나 건너뛸 수 있다. 이 요청에서 첫번째 필드는 파일이름, 두 번재 필드가 파일 데이터라고 가정한다. 먼저 reader.next() 를 사용하여 첫번째 필드를 선택하고 데이터를 추출한 다음, 다시 reader.next() 를 호출하여 파일을 선택한다. 파일을 선택한 다음부터는 field.read_chunk() 를 사용하여 요청 데이터를 조금씩만 읽어들여서 메모리 스루풋을 줄일 수 있다.

async def store_mp3_handler(request):
  reader = await request.multipart()
  field = await reader.next()
  assert field.name == 'name'
  name = await field.read(decode=True)
  
  field = await reader.next()
  assert field.name == 'mp3'
  filename = filed.filename
  size = 0
  with open(os.path.join('/spool/yarrr-media/mp3/', filename), 'wb') as f:
    while True:
      chunk = await field.read_chunk()
      if not chunck:
        break
      size += len(chunk)
      f.write(chunk)
  return web.Response(text=f'{filename} sized of {size} successfully stored.')

따라서 이와 같은 방법을 사용하면 2MB 제한을 우회할 수 있다. 단, 웹서버 자체가 받을 수 있는 요청의 크기는 aiohttp와는 별개로 제한되는 경우가 있으니, 아주 큰 파일을 업로드 받기 위해서는 웹 서버의 설정 역시 확인해야 한다.