NETFLIX 프로젝트 | Back-end(Django)

고준영·2021년 10월 19일
16

WeCode

목록 보기
6/6


NETFLIX Clone Coding

  • 서비스 소개 : NETFILX와 같은 동영상 스트리밍을 제공하는 웹사이트
  • 필수 구현 사항 : 카카오, 구글 API를 이용한 회원가입/로그인, 동영상 스트리밍, 동영상 카테고리별 조회, 위시리스트
  • 구성 인원👨‍👩‍👦‍👦 : Front-end 3명 Back-end 2명
  • 기술 스택🛠 : Django, React, Mysql, EC2, Docker, S3, slack, trello, git

NETFLIX?!

시작하기에 앞서 해당하는 모든 프로젝트는 누군가가 알려주고 따라하는 clone코딩이 아닌 웹 >사이트의 동작 과정을 보고 모델링부터 완성까지 모든 팀원이 스스로 작성한 clone코딩입니다.

개발을 시작하고 넷플릭스 서비스를 이용하면서 동영상 스트리밍 서비의 동작원리에 대한 막연한 궁금증은 있었던것 같습니다. 하지만 구체적으로 고민해본적은 없으나, 이번 프로젝트를 통해서 스트리밍 서비스에 대한 많은 고민을 했던것 같습니다.

  • 스트리밍을 제공하기 위해서 프레임 단위로 쪼개야 하는 것인가?
  • 프레임 단위라면 영상을 재생하기 위한 수많은 사진조각이 나와야 하는가?
  • AWS의 S3와는 어떻게 연동해서 사용해야 할까
  • Django를 이용해서 스트리밍 서비스를 한다?!?!

위와 같은 많은 궁금증을 안고 구글링을 시작해 나갔습니다.
역시나 구글에는 많은 정보가 있었고, 이러한 정보를 바탕으로 어떻게 프로젝트의 난제인 스트리밍을 구현했는지 작성해보려고 합니다.

순서는 아래와 같습니다.

1. 구글링 VS 삽질

2. Django StreamingHttpresponse

3. AWS S3 와 python3

4. Content-type 그리고 코덱


Streaming

1. 구글링 VS 삽질

처음 구글링의 시작은 Django streaming 이었습니다.
생각보다 원하는 정보가 쉽게 나오지 않았고, 혹시나 하는 마음에 유튜브에 관련한 정보가 있을까 찾아보았습니다.
유튜브에는 생각보다 많은 자료가 있었으나, 한글 자막은 커녕 영어자막도 찾기가 힘들었고, 어차피 코드를 보는것인데 하면서 수많은 영상도 시청했습니다.(근데 약간 인도풍의 영어를 사용하시는 분들의 영상이 많더라구요) 결과는? 삽질이었습니다.

대부분이 갖고있는 영상을 쪼개서 전송하는 방식이 아닌, 소형 카메라와 연결하여 스트리밍을 제공하는 방식을 설명하고 있는 영상이었습니다.

이러한 구글링과 유튜브 검색을 계속해서 하면서 Django StreamingHttpresponse이 눈에 들어왔습니다.

2. Django StreamingHttpresponse

장고에 스트리밍을 지원해주는 그러니까 내가 지금 구현해야 할 영상을 쪼개서 원하는 데이터의 크기만큼 응답하는 방식을 찾았습니다.

하지만 여기서 한가지의 고민이 추가됩니다. 어떠한 단위로 영상을 나눌것인가?

프레임 단위로 나누는 방식과 데이터 단위로 나누는 방식이 있다는것을 알게 되었고, 지금 구현하는 기능에는 데이터 단위의 분할이 맞다고 생각하여, 데이터 단위 분할에 대해 공부하기 시작했습니다.

데이터단위로 분할하여 파일일 read하는 방식은 python3에서 지원하는 기능입니다.

아래 코드를 보면서 조금 더 이야기를 해보면 좋을것 같습니다.

class RangeFileWrapper(object):
    def __init__(self, filelike, blksize=10240, offset=0, length=None):
        self.filelike = filelike
        self.filelike.seek(offset, os.SEEK_SET)
        self.remaining = length
        self.blksize = blksize

    def close(self):
        if hasattr(self.filelike, 'close'):
            self.filelike.close()

    def __iter__(self):
        return self

    def __next__(self):
        if self.remaining is None:
            data = self.filelike.read(self.blksize)
            if data:
                return data
            raise StopIteration()

        else:
            if self.remaining <= 0:
                raise StopIteration()
            data = self.filelike.read(min(self.remaining, self.blksize))
            if not data:
                raise StopIteration()
            self.remaining -= len(data)
            return data

위의 코드를 간략하게 설명하자면
1. file전달과 동시에 쪼개려고 하는 block의 사이즈를 결정합니다.
2. offset을 설정함으로써 파일을 읽는 시작점을 부여합니다.
3. python3의 기능인 seek를 이용하며 파일을 쪼개어 읽습니다.

*위의 코드를 이해하기 위해서는 pytohn3의 Iteration을 학습하시는것을 권장합니다.

위에서 만든 RangeFileWrapper 클래스를 사용하는 코드를 살펴보겠습니다.

class ContentStreamingView(View):
    def get(self, request, detail_id):
        video_path = Detail.objects.get(id = detail_id).file
        video      = s3_client.get_video_file(video_path)
        size       = video.get("ContentLength")
        //🍊

        range_re   = re.compile(r'bytes\s*=\s*(\d+)\s*-\s*(\d*)', re.I)

        range_header = request.META.get('HTTP_RANGE', '').strip()
        range_match  = range_re.match(range_header)
        content_type, encoding = mimetypes.guess_type(video_path)
        content_type = content_type or 'application/octet-stream'

        if range_match:
            first_byte, last_byte = range_match.groups()

            first_byte = int(first_byte) if first_byte else 0
            last_byte = int(last_byte) if last_byte else size - 1

            if last_byte >= size:
                last_byte = size - 1

            length = last_byte - first_byte + 1
            result = StreamingHttpResponse(RangeFileWrapper(BytesIO(video["Body"].read()), offset=first_byte, length=length), status=206, content_type=content_type)
            result['Content-Length'] = str(length)
            result['Content-Range']  = 'bytes %s-%s/%s' % (first_byte, last_byte, size)

        else:
            result = StreamingHttpResponse(FileWrapper(BytesIO(video["Body"].read())), content_type=content_type)
            result['Content-Length'] = str(size)

        result['Accept-Ranges'] = 'bytes'
        result['X-Content-Type-Options'] = 'nosniff'
        
        return result

🍊이 있는 부분까지는 추후에 설명하겠습니다.

  1. range_re는 정규식을 사용하여 client로 부터 파일의 range를 (bytes 0-5200/9507307)의 형태로 받기 위해서 사용했습니다.
  2. range_header를 받아와서 위에서 작성한 형식과 일치하는지 확인합니다.
  3. video를 가져와서 데이터로 변환하여 read합니다.
  4. read한 비디오를 위에서 작성한 RangeFileWrapper를 이용하여 부분별로 쪼개서 읽습니다.
  5. 쪼개서 읽은 비디오를 StreamingHttpResponse를 동해서 응답합니다.

3. AWS S3 와 python3

비디오 파일을 계속 우리의 로컬에 저장해서 제공하는 방법은 좋은 방향이 아닙니다.
비디오 파일을 어디에서나 접근 가능한 저장공간에 위치해야 하고, 저는 AWS의 S3(Simple Storage Service)를 사용했습니다.

우리가 스트리밍을 위해 S3에 업로드 된 비디오 파일을 가져올 때 어떻게 가져와야 할지 의문점이 있었습니다.

  • 비디오를 다운로드 하지 않고 데이터로 읽는 방법이 없을까.
  • 이미지처럼 url만 가져와서 쓸 수도 없는 노릇이고 어떻게 해야하는가

위의 의문을 해결하기 위해 아래와 같은 키워드로 다시 구글링을 시작했습니다.
"S3 with Django"
"S3 with Python"
"S3 video with python"
"get video object from s3 with python"
...

검색의 결과 boto3라는 Python3의 라이브러리를 이용하여 비디오를 다운받는 형식이 아닌 데이터의 형태로 가져올 수 있다는 것을 알게 되었습니다.

아래의 코드를 보시죠

class MyS3Client:
    def __init__(self, s3_client, bucket):
        self.s3_client = s3_client
        self.bucket = bucket

    def get_video_file(self, video_path):
        return self.s3_client.get_object(Bucket = self.bucket, Key = video_path)

boto3_s3  = boto3.client("s3", region_name = 'ap-northeast-2', aws_access_key_id = AWS_ACCESS_KEY_ID, aws_secret_access_key = AWS_SECRET_ACCESS_KEY)
bucket    = BUCKET
s3_client = MyS3Client(boto3_s3, bucket)
class ContentStreamingView(View):
    def get(self, request, detail_id):
        video_path = Detail.objects.get(id = detail_id).file
        video      = s3_client.get_video_file(video_path)
        size       = video.get("ContentLength")
  1. boto3의 기능을 이용하여 S3의 region과 접근키, 시크릿 키를 이용하여 S3에 접근합니다.
  2. S3의 수많은 bucket중 어떠한 bucket에 접근할지 정해줍니다.
    *위의 코드에서는 환경변수 파일은 따로 관리하였습니다.
  3. MyS3Client class를 활용하여 video_path(해당 경로는 DB에 저장되어 있습니다)를 전달해주어 video 객체를 불러옵니다.
  4. boto3를 사용하여 객체의 형태로 불러오면 video.get("ContentLength")과 같은 여러가지 기능을 사용할 수 있습니다.

최근에 AWS S3가 아닌 NCP(Naver Cloud Platform)의 Object Storage를 사용해볼 기회가 생겼는데, NCP Object Storage와 Python3를 연결하여 사용할 때 boto3를 지원하는것을 알게 되었습니다. NCP Object Storage가 후발 주자인 만큼 다른 라이브러리에서 S3와 연동하여 사용하는 방식(정말 Storage지정 할 때도 S3로 지정합니다.)을 지원해주는 것 같습니다.

4. Content-type 그리고 코덱

마지막 코덱입니다. 사실 이 부분에서 가장 오랫동안 Blocking되어 있었습니다.
왜냐하면 제 postman(http요청을 할 수 있는 툴)에서는 video가 잘 재생이 되었는데, client로 전송만 하면 재생이 되지 않았습니다.
정말 많은 구글링을 하고, 혹시나 하는 마음에 코덱을 전부 변경했습니다.

코덱을 전부 mp4로 변경하고 나서는 postman, client에서도 모두 잘 동작하는 것을 확인할 수 있었습니다.

저희 프로젝트의 파일이 .mov파일인데 mp4로 반환을 하고 있어서 생긴 문제였습니다.
영상 파일에는 우리가 데이터 통신을 할 때 처럼 Flag가 포함이 되고 이러한 Flag는 mov라고 말하고 있는데, Content-type를 mp4로 보내주고 있었으니 당연히 보이지 않았겠죠..?



소통과 PM

팔로팔로 팔로미

이번 프로젝트에서는 PM의 역할을 맡았습니다. 먼제 결론부터 이야기 하자면 좋은 PM이 있으려면 정말 좋은 팔로워가 있어야 합니다. 나중에 어딘가 동료가 되어 내가 팔로워가 된다면 어떻게 행동하면 좋을지 알게 된 프로젝트였습니다.

에자일?! 스크럼?!

먼저 저희 팀원들은 매일 30분정도 미팅을 하였고, 매주 프로젝트의 진행 및 완료 과정과, 다음주 스프린트를 정하는 시간을 가졌습니다.
해당 회의를 진행하면서 누군가는 프로젝트의 흐름의 위에 있어야 한다고 생각했고, PM인 제가 해야한다고 생각했습니다. 회의를 하기전에 프로젝트를 진행하는 방향과 어느정도 진행했는지 파악하기 위해 프로젝트 모든 부분의 대략적인 로직을 알아야 했습니다.
이러한 과정속에서 많은 지식을 얻게 되었고, 팀 회의 또한 수월하게 진행 되었습니다.
*여기서 회의의 수월한 진행은 그냥 기분좋게 회의를 했다! 가 아니라, 모두가 이해못하고 넘어가는 부분 없이 같은 방향을 바라보고, 같은 내용을 이해하고 있음을 뜻합니다.

트렐로

트렐로라는 툴을 이용하여 진행상황과 회의 방명록등 프로젝트를 진행하는데 있어 필요한 모든 부분을 기록하고 정리할 수 있었습니다.

툴을 사용한다고 해서 정리가 깔끔해지는것이 아니라 팀마다의 규칙이 있어야 한다는 것을 알게 되었습니다.



결론

구선생님은 모든것을 알고있습니다. 하지만 구선생님께 여쭤봐도 내가 이해하고 사용해야 합니다.

구글에 서칭하면 사실 왠만한 자료가 다 나옵니다. 하지만 어떻게 검색을 하느냐, 영문서를 대하는데 있어서 얼마나 마음을 열어놓고 대하느냐의 차이가 큰것 같습니다. 사실 이번 프로젝트를 진행하면서 엄청난 구글링을 했고, 이제서야 구글링을 어떻게 해야할지 눈이 뜨였습니다.

Node.js 활용

사실 Node.js와 express를 사용하면 Django에서보다 조금 더 수월하게 진행 할 수 있었을 것입니다.
왜냐? 자료가 많습니다.
Django로 NETFLIX를 클론코딩 한것을 찾아봐도 사실 스트리밍을 제외한 페이지의 틀만 보여주는 형식이 많습니다.

그 이유는 간단합니다. Node.js의 동작원리가 영상을 받아오고 출력하는데 있어 비동기 처리가 우수하고, 순간순간 데이터를 처리하는데 있어 Python을 사용하는 Django보다 우수하기 때문에 스트리밍 서비스는 사실상 Django보다는 Node.js의 자료가 더욱 많고, 사례도 많았습니다.

js와 python을 사용한 프레임워크의 차이에 대해 정말 조금 찾아보면 Django는 복잡한 수식 계산에 우수하고, admin page를 제공하기에 빠르게 웹 페이지를 제작하는데 장점이 있고, Node.js + express조합은 실시간 데이터 처리, 간단한 사용자의 입출력등을 제공하는데 장점이 있다는 것을 알 수 있습니다.

우리 팀원들 정말 고생했습니다.

사실 프로젝트를 진행하면서 우리 팀원들이 많이 지쳤던것 같습니다. 체력적으로도 버겁고 영상스트리밍 서비스라는 것이 생소하다보니 처음부터 끝까지 하나하나 찾아가면서 해야했습니다. 그럼에도 불구하고 항상 웃는 얼굴로 프로젝트 끝까지 진행해주신 우리 팀원들 정말 감사합니다.

스트리밍에 관심 있으시거나 해당 블로그의 지적사항 또는 문의사항이 있으시몀 언제든 댓글 부탁드립니다!!

profile
코드짜는귤🍊 풀스택을 지향하는 주니어 개발자 입니다🧡

4개의 댓글

comment-user-thumbnail
2021년 10월 21일

실시간 통신으로 리팩토링하러 가시죠

답글 달기
comment-user-thumbnail
2022년 4월 25일

안녕하세요! 혹시 위에서 사용된 코드 볼수있을까요ㅠ???? 코드보고 자세히 배우고싶네요 ㅠㅠ!!

답글 달기
comment-user-thumbnail
2022년 7월 7일

안녕하세요.
해당 코드로 음원 스트리밍을 구현해보고 있습니다.
swagger를 통해 테스트를 해보고 있는데 다음과 같은 상황이 생겨
혹시 유사한 경험이 있으셨거나 원인을 무엇인지 알려주실 수 있으실까요??
문제 상황 :
만든 player api를 요청을 스웨거를 통해 진행하는데 실행을 시키는 순간 request를 무한정 날려
서버가 뻗어 버립니다. 이게 초기에는 잘되다가 갑작스레 해당 현상이 발생합니다 ㅠㅠ

답글 달기
comment-user-thumbnail
2023년 4월 18일

안녕하세요.
좋은글 잘 읽었습니다.
궁금한 점이 있는데, StreamingHttpResponse 를 쓰더라도 청크 단위로 데이터를 generate 하여 던질 뿐 결국 커넥션은 물고 있어야 하며 동기적으로 작업할 수 밖에 없을 것 같다는 생각이 들어서요.
서버 구동 시에 gunicorn 등을 사용하셨다면, 지정하신 워커 프로세스 만큼의 클라이언트에게만 스트리밍을 제공할 수 있을 것 같다는 생각이 드는데 혹시 맞을까요?

답글 달기