# 어제 무엇을 했나요?
- 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
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라는 라이브러리가 반드시 필요
docker-compose down
docker-compose up -d --build
- 웹 클라이언트(브라우저 등)가 서버로 데이터를 전송할 때 사용하는 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_classes 에 MultiPartParser를 등록하면,
- 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_classes 에 MultiPartParser를 등록하면,
- 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. 메서드 호출