2026/02/03 합동프로젝트 - 20

김기훈·2026년 2월 3일

TIL

목록 보기
129/194
# 어제 무엇을 했나요?
- 1. 쿼리 최적화 
- 2. 러닝헬퍼 리뷰 반영
- 3. 이력서 작성

# 오늘은 무엇을 할 것인가요?
- 1. 발표준비 
- 2. 발표자료 수정 및 readme.md 파일 내용 수정

# 진행하는데 어려운 부분(도움이 필요한 부분)이 있나요?


오늘 학습 내용 ✅


readme

model

  • ai
    • game_review_summary
    • user_tendency
  • community
    • comments
    • review_like
    • review
  • game
    • game
    • game_genre
    • game_img
    • game_platform
    • game_tag
    • genre
    • platform
    • tag
    • wishlist
  • preference
    • genre_preference
    • tag_preference
  • user
    • socail
    • user

profile

create

  • 팀원의 토스로 인한 일거리 증가 - 프로필 이미지 업로드 기능 구현

settings

MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "media")

PR리뷰

  • 업로드 시 image_file.read() 로 전체를 메모리에 올려 ContentFile 저장.
  • 현재 5MB 제한이면 괜찮지만 상향 시 메모리 스파이크 가능합니다.
    • 해석
      • "지금은 파일이 작아서(5MB) 괜찮지만,
      • 나중에 큰 파일을 허용하거나 동시에 많은 사람이 업로드하면 서버가 멈출 수 있다"
file_path = f"profile_images/{new_filename}"
saved_path = default_storage.save(file_path, ContentFile(image_file.read()))

file_path = f"profile_images/{new_filename}"
saved_path = default_storage.save(file_path, image_file)
구분ContentFile(image_file.read()) (기존)image_file (수정 후)
작동 방식파일을 통째로 RAM에 읽어 들인 후 저장파일을 조각(Chunk) 내어 조금씩 흘려보내며 저장
메모리 사용량파일 크기만큼 증가 (100MB 파일이면 100MB 점유)아주 적음 (설정된 Chunk 크기, 약 64KB 수준 유지)
위험성대용량 파일 업로드 시 OOM(Out Of Memory) 에러 발생 가능파일 크기가 커져도 서버 메모리에 부담 없음
비유물 10L를 한 번에 입에 머금고 옮기기호스로 연결해서 계속 흘려보내기

새롭게 알게된 내용 ✅

pillow

  • Django에서 ImageField를 사용하여 이미지 파일의 유효성(이미지인지 아닌지 등)을 검사하려면 Pillow라는 라이브러리가 반드시 필요
    • poetry add Pillow
# 1. 실행 중인 컨테이너 중지 및 삭제
docker-compose down

# 2. 이미지 새로 빌드 및 실행 (이 과정에서 Pillow가 설치됩니다)
docker-compose up -d --build

multipart/form-data

  • 웹 클라이언트(브라우저 등)가 서버로 데이터를 전송할 때 사용하는 HTTP Content-Type 중 하나
    • 가장 핵심적인 특징은 "파일 업로드"가 필요할 때 사용된다는 점
    • 텍스트 데이터와 바이너리 데이터(이미지, 영상 등)를 하나의 요청(Request)에 섞어서 보낼 수 있도록 설계된 형식
  • 사용 목적

    • 일반적인 HTML Form의 기본 전송 방식은 application/x-www-form-urlencoded
      • 이 방식은 데이터를 긴 문자열(key=value&key2=value2)로 변환해서 보내는데,
      • 대용량 파일이나 이미지 같은 바이너리 데이터를 이렇게 변환하면 비효율적이고 데이터 크기가 너무 커짐
    • 반면, multipart/form-data 는 데이터를 변환하지 않고
      • 있는 그대로 쪼개서(Part) 보낼 수 있어 파일 전송에 최적화
  • 구조의 핵심

    • 이 형식의 가장 큰 특징은 Boundary(경계선)
      • 여러 종류의 데이터(텍스트, 파일 등)를 한 번에 보내야 하므로
      • 각 데이터가 어디서 시작하고 끝나는지 구분할 식별자가 필요함
    • 헤더(Header)
      • "이제부터 보낼 데이터는 ----WebKitFormBoundary... 라는 구분선으로 나뉘어 있어"라고 선언
    • 바디(Body)
      • 실제로 그 구분선을 사용해 데이터를 구역별로 나눔
  • 실제 형태

POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW  <-- 구분선 정의

------WebKitFormBoundary7MA4YWxkTrZu0gW   <-- 첫 번째 파트 시작 (텍스트 데이터)
Content-Disposition: form-data; name="username"

user123
------WebKitFormBoundary7MA4YWxkTrZu0gW   <-- 두 번째 파트 시작 (파일 데이터)
Content-Disposition: form-data; name="profile_pic"; filename="photo.jpg"
Content-Type: image/jpeg

(이미지의 실제 바이너리 데이터 내용...)
------WebKitFormBoundary7MA4YWxkTrZu0gW-- <--(마지막에 대시 2개가 붙음)
  • 중요

    • 이 형식을 백엔드(서버)에서 처리하려면 데이터를 파싱해서 파일과 텍스트를 분리하는 과정이 필요
      • 때문에 DRF에서는 parser_classes를 사용

parser_classes

  • DRF(Django REST Framework)가 들어오는 요청(Request)의 'Content-Type'을 보고
    • 그 거대한 덩어리의 데이터를 "어떻게 해석해서 Python 변수로 만들지" 결정하는 도구
  • 특히 multipart/form-data처럼 구조가 복잡한 데이터는 MultiPartParser가 없으면
    • 서버가 데이터를 읽을 수 없음
  • Parser가 하는 일 (비포 & 애프터)

    • 서버 입장에서는 데이터가 처음 도착했을 때 그냥 '0과 1로 된 긴 바이트 스트림'
      • 파서(Parser)가 개입해야 우리가 아는 데이터가 됨
  • 즉, parser_classesMultiPartParser를 등록하면,
    • DRF가 알아서 경계선(Boundary)을 기준으로 데이터를 쪼개고
    • 텍스트는 request.data 에, 파일은 request.FILES 에 예쁘게 담아줌
- 1. JSONParser								| - 2. MultiPartParser
  - Raw Data (파싱 전)						|  - Raw Data (파싱 전)
    - {"name": "kim"} (그냥 문자열)			|    - ----WebKitBound... (복잡한 텍스트/파일 혼합)
  - Python 객체 (파싱 후)						|  - Python 객체 (파싱 후)
    - request.data (딕셔너리)					|    - request.data (텍스트)
    										|    - request.FILES (파일)
  • 웹 클라이언트(브라우저 등)가 서버로 데이터를 전송할 때 사용하는 HTTP Content-Type 중 하나
    • 가장 핵심적인 특징은 "파일 업로드"가 필요할 때 사용된다는 점
    • 텍스트 데이터와 바이너리 데이터(이미지, 영상 등)를 하나의 요청(Request)에 섞어서 보낼 수 있도록 설계된 형식
  • 사용 목적

    • 일반적인 HTML Form의 기본 전송 방식은 application/x-www-form-urlencoded
      • 이 방식은 데이터를 긴 문자열(key=value&key2=value2)로 변환해서 보내는데,
      • 대용량 파일이나 이미지 같은 바이너리 데이터를 이렇게 변환하면 비효율적이고 데이터 크기가 너무 커짐
    • 반면, multipart/form-data 는 데이터를 변환하지 않고
      • 있는 그대로 쪼개서(Part) 보낼 수 있어 파일 전송에 최적화
  • 구조의 핵심

    • 이 형식의 가장 큰 특징은 Boundary(경계선)
      • 여러 종류의 데이터(텍스트, 파일 등)를 한 번에 보내야 하므로
      • 각 데이터가 어디서 시작하고 끝나는지 구분할 식별자가 필요함
    • 헤더(Header)
      • "이제부터 보낼 데이터는 ----WebKitFormBoundary... 라는 구분선으로 나뉘어 있어"라고 선언
    • 바디(Body)
      • 실제로 그 구분선을 사용해 데이터를 구역별로 나눔
  • 실제 형태

POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW  <-- 구분선 정의

------WebKitFormBoundary7MA4YWxkTrZu0gW   <-- 첫 번째 파트 시작 (텍스트 데이터)
Content-Disposition: form-data; name="username"

user123
------WebKitFormBoundary7MA4YWxkTrZu0gW   <-- 두 번째 파트 시작 (파일 데이터)
Content-Disposition: form-data; name="profile_pic"; filename="photo.jpg"
Content-Type: image/jpeg

(이미지의 실제 바이너리 데이터 내용...)
------WebKitFormBoundary7MA4YWxkTrZu0gW-- <--(마지막에 대시 2개가 붙음)
  • 중요

    • 이 형식을 백엔드(서버)에서 처리하려면 데이터를 파싱해서 파일과 텍스트를 분리하는 과정이 필요
      • 때문에 DRF에서는 parser_classes를 사용

parser_classes

  • DRF(Django REST Framework)가 들어오는 요청(Request)의 'Content-Type'을 보고
    • 그 거대한 덩어리의 데이터를 "어떻게 해석해서 Python 변수로 만들지" 결정하는 도구
  • 특히 multipart/form-data처럼 구조가 복잡한 데이터는 MultiPartParser가 없으면
    • 서버가 데이터를 읽을 수 없음
  • Parser가 하는 일 (비포 & 애프터)

    • 서버 입장에서는 데이터가 처음 도착했을 때 그냥 '0과 1로 된 긴 바이트 스트림'
      • 파서(Parser)가 개입해야 우리가 아는 데이터가 됨
  • 즉, parser_classesMultiPartParser를 등록하면,
    • DRF가 알아서 경계선(Boundary)을 기준으로 데이터를 쪼개고
    • 텍스트는 request.data 에, 파일은 request.FILES 에 예쁘게 담아줌
- 1. JSONParser								| - 2. MultiPartParser
  - Raw Data (파싱 전)						|  - Raw Data (파싱 전)
    - {"name": "kim"} (그냥 문자열)			|    - ----WebKitBound... (복잡한 텍스트/파일 혼합)
  - Python 객체 (파싱 후)						|  - Python 객체 (파싱 후)
    - request.data (딕셔너리)					|    - request.data (텍스트)
    										|    - request.FILES (파일)

오늘 발생한 문제(발생 했다면) ✅

[ 🔴 문제: ]
프로필 이미지 삭제 요청(DELETE) 시 `500 Internal Server Error`가 발생하며, 서버 로그에 `TypeError: ProfileImageService.delete_profile_image() missing 1 required positional argument: 'user'` 오류가 출력됨.


[ 🟡 원인: ]
`delete_profile_image`는 클래스의 **인스턴스 메서드**(첫 번째 인자로 `self`를 받음)인데, 뷰(View)에서 **객체(인스턴스)를 생성하지 않고 클래스 이름으로 바로 호출**(`ProfileImageService.delete_profile_image(...)`)하여 `self`가 전달되지 않았기 때문임.


[ 🔵 해결: ]
`user/views/profile_img_view.py` 파일의 `delete` 메서드 내에서 **먼저 Service 클래스의 인스턴스를 변수에 할당(생성)한 뒤, 그 변수를 통해 메서드를 호출**하도록 코드를 수정함.

# (수정 예시)
service = ProfileImageService()  # 1. 인스턴스 생성
service.delete_profile_image(request.user)  # 2. 메서드 호출
profile
안녕하세요.

0개의 댓글