개인 프로젝트를 진행하면서 사용하거나 알게 된 이론
ORM 📌
Q 객체
Django ORM에서 복잡한 데이터베이스 쿼리를 작성할 때 사용하는 강력한 도구
- 기본적으로 Django의
filter()나 exclude() 메서드에
- 여러 개의 키워드 인자를 넘기면
AND(&) 조건으로만 묶임
- 하지만 데이터베이스를 다루다 보면
OR(|) 이나 NOT(~) 같은 조건이 필요할 때가 많은데
언제 사용?
OR 조건이 필요할 때
- "제목에 'Django'가 포함되어 있거나(OR) 조회수가 100 이상인 글"
NOT 조건이 필요할 때
- "작성자가 'admin'이 아닌(NOT) 글"
복잡한 논리 연산이 필요할 때
- "(A OR B) AND (C OR NOT D)" 와 같은 다중 조건
동적 쿼리 생성
- 조건문(if/else)에 따라 쿼리 조건을 동적으로 이어 붙여야 할
기본 사용법 및 연산자
import
django.db.models 에서 임포트하여 사용하며, 파이썬의 비트 연산자를 활용하여 조건을 결합
from django.db.models import Q
from myapp.models import Article
OR 연산 - | (파이프)
filter() 로는 불가능한 OR 조건 검색
NOT 연산 - ~ (틸드)
exclude()를 사용할 수도 있지만, 복잡한 쿼리 내에서 특정 부분만 부정하고 싶을 때 유용
AND 연산 - & (앰퍼샌드)
- 복합 연산 (AND, OR, NOT 조합) - 괄호를 사용하여 연산의 우선순위를 명확히 지정 가능
articles = Article.objects.filter(
Q(title__icontains='Django') | Q(views__gte=100)
)
articles = Article.objects.filter(
~Q(title__icontains='Python')
)
articles = Article.objects.filter(
(Q(title__icontains='Django') | Q(title__icontains='Python')) &
~Q(author__username='admin')
)
동적 쿼리 만들기
- 검색 필터처럼 사용자의 입력에 따라 조건이 달라지는 경우
- 비어있는
Q 객체를 생성하고 조건을 동적으로 추가 가능
search_keyword = "Django"
min_views = 50
query = Q()
if search_keyword:
query |= Q(title__icontains=search_keyword)
query |= Q(content__icontains=search_keyword)
if min_views:
query &= Q(views__gte=min_views)
results = Article.objects.filter(query)
icontains
Django ORM에서 텍스트를 검색할 때 사용하는 필드 룩업(Field Lookup) 중 하나
- 대소문자를 구분하지 않고(Case-insensitive) 특정 문자열이 포함되어 있는지를 확인할 때 사용
ex
title__icontains='django'
- 제목 필드 안에 'Django', 'DJANGO', 'django', 'dJaNgO' 등
- 대소문자 형태와 상관없이 알파벳 배열만 맞으면 모두 검색 결과에 포함시킴
contains
1. "Hello Django"
2. "Learning django"
3. "DJANGO IS AWESOME"
Article.objects.filter(title__contains='Django')
Article.objects.filter(title__icontains='django')
.distinct()
- 쿼리 결과(QuerySet)에서 중복된 데이터를 제거하고 고유한(Unique) 레코드만 반환하도록 하는 메서드
언제 사용?
- 다른 테이블과 조인(Join)을 통해 필터링하거나, 특정 필드만 추출할 때 중복 데이터가 흔하게 발생함
예시
- 1. 게시글(Article)과 태그(Tag)가 다대다(M:N) 관계라고 가정
- 2. 'Python'이나 'Django' 태그가 달린 게시글 검색
- 3. 다중 테이블 조인 시 중복 제거
articles = Article.objects.filter(tags__name__in=['Python', 'Django'])
unique_articles = Article.objects.filter(tags__name__in=['Python', 'Django']).distinct()
———————————————————————————————————————————————————————————————————————————————
- 1. 특정 필드값의 중복 제거
- 2. values와 함께 사용
categories = Article.objects.values_list('category', flat=True)
unique_categories = Article.objects.values_list('category', flat=True).distinct()
주의
성능 문제 (오버헤드)
.distinct() 를 사용하면 데이터베이스 내부적으로 데이터를 정렬하고 비교하여
- 불필요하게 남용하면 쿼리 속도가 느려질 수 있으므로
- 조인으로 인해 중복 발생이 확실한 경우에만 사용하는 것이 좋음
PostgreSQL 전용 기능 (DISTINCT ON)
- SQLite나 MySQL 등 대부분의 데이터베이스에서는
.distinct() 괄호 안에 인자를 넣을 수 없으며,
- 조회된 행 전체의 데이터가 완전히 똑같을 때만 중복으로 간주함
- 하지만
PostgreSQL을 사용 중에는 괄호 안에 특정 필드명을 지정할 수 있음
- 참고
- PostgreSQL에서 특정 필드로
distinct('field_name')을 사용할 때는
- 반드시 해당 필드가
order_by()의 첫 번째 정렬 기준으로 들어가야 에러가 발생하지 않음
Article.objects.order_by('category', '-created_at').distinct('category')
code 📌
Type Hinting
user = cast(User, request.user)
- "실행 시점(Runtime)에는 아무 일도 하지 않지만, 개발 시점(Static Analysis)에는 매우 중요한 역할"
타입 힌트 제공 (Type Hinting)
- Python의
typing.cast 함수는 정적 타입 검사기(Mypy, Pyright 등)와
- IDE(PyCharm / VSCode)에게 "이 변수(
request.user)는 지금부터 무조건 User 클래스의
- 인스턴스로 취급해줘"라고 강제로 지정하는 것
자동 완성 활성화
- 일반적으로 프레임워크에서 request.user는 로그인하지 않은 경우
AnonymousUser일 수도 있기 때문에
- IDE가 User 모델에 정의한 필드(예:
user.nickname)를 자동으로 추천해주지 못할 때가 많음
- cast를 쓰면 이때부터 자동 완성이 완벽하게 동작함
주의 ⚠️
타입 안정성을 확보하는 데는 아주 유용하지만, 남용하면 위험
cast는 강제로 타입을 규정하는 것이라
- 실제로
request.user가 익명 사용자(AnonymousUser)인 상태에서
- 나중에
user.nickname 같은 속성에 접근할 때 비로소 AttributeError가 발생
model 📌
Field
BigIntegerField
PositiveIntegerField()
models.PositiveIntegerField()
- 일반적인 크기의 양수 정수를 저장
- 0부터 2,147,483,647 (약 21억) 까지 저장 가능(보통 4바이트(Byte)를 차지)
- 숫자가 꽤 커질 수 있는 경우에 사용
PositiveSmallIntegerField()
models.PositiveSmallIntegerField()
- 비교적 작은 크기의 양수 정수를 저장(보통 2바이트(Byte)를 차지)
through
through="tags.PostTag" 옵션
- Django가 자동으로 숨겨진 중간 테이블을 만들도록 내버려 두지 않고
- tags 앱에 정의된 PostTag라는 모델을 연결 테이블로 직접 명시해서 사용
class Post(TimeStampedModel):
...
tags = models.ManyToManyField(
"tags.Tag", through="tags.PostTag", related_name="posts"
)
on_delete
models.SET_NULL
on_delete=models.SET_NULL
- 시리즈를 삭제하더라도 그 안에 속했던 포스트들은 지워지지 않고 단지 '시리즈 없음' 상태로 안전하게 남게 됨
class Post(TimeStampedModel):
class Visibility(models.TextChoices):
PUBLIC = "PUBLIC", "전체 공개"
PRIVATE = "PRIVATE", "비공개"
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="posts"
)
series = models.ForeignKey(
Series, on_delete=models.SET_NULL, null=True, blank=True, related_name="posts"
)
series_order = models.PositiveSmallIntegerField(null=True, blank=True)
...
index
UniqueConstraint
UniqueConstraint(fields=["user", "name"])
- "한 명의 유저는 중복된 이름의 시리즈를 만들 수 없지만 서로 다른 유저는 같은 이름의 시리즈를 가질 수 있다"
UniqueConstraint(user, name) 제약 조건오류: IntegrityError
class Series(TimeStampedModel):
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="series"
)
name = models.CharField(max_length=100)
class Meta:
db_table = "series"
constraints = [
models.UniqueConstraint(fields=["user", "name"], name="uk_series_user_name")
]
Image 📌
S3
"용량이 무한대로 늘어나는 인터넷 상의 외장 하드디스크"
- 사진, 동영상, 텍스트 문서 등 어떤 파일(Object)이든 마음껏 저장하고
- 고유한 인터넷 주소(URL)를 통해 언제 어디서나 꺼내볼 수 있는 AWS의 대표적인 클라우드 스토리지 서비스
S3를 사용하는 이유
서버의 휘발성 (데이터 증발 방지)
문제
- 요즘 서버들은 업데이트를 하거나 트래픽이 몰리면 기존 서버를 껐다가 새로운 서버를 켬
- 만약 서버 내부 폴더에 사용자가 올린 이미지를 저장해 뒀다면,
- 서버가 재시작되는 순간 사진이 전부 날아가는 대참사 발생 가능
S3의 해결
- 서버(Django)와 저장소(S3)를 완전히 분리
- 서버가 100번 꺼졌다 켜져도, 사진은 S3라는 튼튼한 금고에 안전하게 보관
서버 확장성 (다중 서버 문제 해결)
문제
- 블로그가 유명해져서 Django 서버를 3대(A, B, C)로 늘렸다고 가정
- 유저가 'A 서버'에 접속해서 사진을 올리면 A 서버에만 사진이 저장됨
- 다음날 유저가 'B 서버'로 접속하게 되면 엑스박스(사진 깨짐) 뜸 (B 서버에는 사진이 없어서)
S3의 해결
- A, B, C 서버 모두 유저가 사진을 올리면 무조건 중앙의 S3로 보냄
- 유저가 어느 서버로 접속하든 똑같은 S3 URL에서 사진을 불러오기 때문에 엑스박스가 뜨지 않음
비용과 성능의 최적화
- 서버 컴퓨터(EC2 등)의 하드디스크 용량을 늘리는 것은 비쌈
- 게다가 이미지를 서빙(전송)하는 데 서버의 CPU와 네트워크를 낭비 가능
- S3는 쓴 만큼만 돈을 내며(GB당 아주 저렴함)
- 서버를 거치지 않고 사용자의 브라우저와 S3가 직접 사진 데이터를 주고받게 할 수 있어
S3의 3가지 핵심 용어
버킷 (Bucket)
- 파일을 담는 '최상위 폴더'이자 '프로젝트 단위' (예: my-django-blog-bucket)
- 전 세계에서 유일한 이름을 가져야 함
객체 (Object)
- 버킷 안에 저장되는 파일 그 자체 (예: profile_img.png, post_thumbnail.jpg)
- S3는 폴더라는 개념이 없고, 모든 것이 객체
엔드포인트 (Endpoint URL)
- 파일에 접근할 수 있는 고유한 인터넷 주소
- (예:
https://my-django-blog-bucket.s3.ap-northeast-2.amazonaws.com/post_thumbnail.jpg)
- 프론트엔드 HTML의
<img src="...">에 바로 이 주소가 들어가게 됨
presigned_url
- S3를 훨씬 더 똑똑하고 안전하게 쓰기 위해 AWS가 제공하는 '핵심 기능(기술)'
기존 이미지 업로드 과정
프론트엔드
- 무거운 고양이 사진(5MB)을 백엔드(Django)로 전송
백엔드
- 그 무거운 사진을 낑낑대며 받아서 메모리에 올린 뒤, 다시 S3로 전송
문제점
- 트래픽이 몰리면 백엔드 서버가 이미지 파일들을 옮기느라 뻗어버립니다.
- 백엔드는 데이터베이스 통신이나 로직 처리를 해야 하는데
- 단순 '택배 배달부' 역할을 하느라 자원을 다 낭비
Presigned URL 방식
프론트엔드
- "백엔드야, 나 '고양이.jpg' S3에 올릴 건데 딱 한 번만 쓸 수 있는 주소(출입증) 좀 만들어줘."
백엔드(Django)
- 자기가 가진 마스터 권한으로 S3에게 물어봅니다.
- "S3야, 얘한테 딱 5분 동안만 업로드할 수 있는 일회용 주소 하나만 발급해 줘."
- 이때 S3가 만들어주는 임시 주소가 바로 presigned_url
프론트엔드
- 백엔드에게 그 presigned_url을 받으면
- 무거운 고양이 사진을 백엔드가 아닌 S3 주소로 직접 전송해 버립니다.
장점
서버 비용 절감 & 성능 향상
- 백엔드(Django)는 아주 가벼운 '문자열(URL)'만 하나 만들어주고 끝납니다.
- 무거운 파일 트래픽은 모두 AWS S3가 감당하므로 백엔드 서버가 쾌적해집니다.
완벽한 보안
- 프론트엔드(클라이언트) 코드에 AWS 해킹의 주범인 '비밀키(Secret Key)'를 숨겨둘 필요가 없음
- 백엔드만 비밀키를 가지고 임시 출입증만 발급해 주면 됨
이미지 url 얻기
방법 1. S3 기능 구현
- 본문(에디터) 입력창에 내가 원하는 이미지를 드래그 앤 드롭으로 끌어다 놓습니다.
- 코드가 잠시 S3에 업로드를 진행한 뒤, 에디터에 아래와 같은 마크다운 코드가 생성됨
- ex.

- 여기서 괄호 () 안에 있는 https://... 로 시작하는 주소만 복사
방법 2. 깃허브(GitHub) 이슈 활용하기
- 내 GitHub 레포지토리 아무 곳이나 들어가서 Issues -> New issue를 누릅니다.
- 내용(Write) 탭에 원하는 이미지를 마우스로 끌어다 놓습니다(드래그 앤 드롭)

- 형태로 텍스트가 생김
- 이슈는 등록하지 않아도 주소는 영구 유지됨
방법 3. 디스코드(Discord) 활용하기
- 디스코드의 '나와의 채팅방'이나 아무 개인 서버에 이미지를 업로드
- 올라간 이미지를 클릭하여 크게 띄운 뒤, 우측 하단의
[브라우저에서 열기] 를 누릅니다.
- 새 인터넷 창이 열리면서 이미지가 보이면
- 맨 위 인터넷 주소창에 있는 주소를 그대로 복사해서 사용
프론트 📌
Image
Toast UI Editor와 이미지 동작
프론트엔드 (글쓰기)
가로채기 (Hook)
- Toast UI Editor가 사진을 화면에 띄우기 전에 만든 함수(addImageBlobHook)가 사진을 가로챔
API 통신
- 프론트엔드가 백엔드(Django)의 이미지 업로드 API로 사진 파일을 보냄
백엔드 (S3/로컬)
에디터 렌더링
- 프론트엔드가 받은 URL을 에디터에
 형태로 넣어주면, 마침내 화면에 사진이 뜸
Serializer 📌
Field
- 클라이언트(프론트엔드)와 서버 간에 "ID(숫자)를 주고받으면
- 백엔드에서 알아서 실제 데이터베이스 객체(인스턴스)로 변환해 주는 마법 같은 번역기" 역할
요청을 받을 때 (Deserialization)
- 프론트엔드에서 {"series": 5} 처럼 시리즈의 ID(PK) 값만 숫자로 보내면
- 이 필드가 데이터베이스를 뒤져서 "5번 시리즈 객체"를 찾아옵니다.
- (만약 5번 시리즈가 DB에 없다면 자동으로 에러를 뱉어냅니다.)
응답을 보낼 때 (Serialization)
- 반대로 백엔드에서 프론트엔드로 데이터를 줄 때는 무거운 전체 객체 정보 대신
- 깔끔하게 ID 숫자(5) 로 변환해서 보내줍니다.
사용 안하면?
- view / service에서 아래와 같은 코드를 추가해야 함
series_id = request.data.get('series_id')
if series_id:
try:
series = Series.objects.get(id=series_id)
except Series.DoesNotExist:
raise ValidationError("존재하지 않는 시리즈입니다.")
EmailField
옵션
series = serializers.PrimaryKeyRelatedField(
queryset=Series.objects.all(),
required=False,
allow_null=True
)
password = serializers.CharField(
write_only=True,
required=True,
style={"input_type": "password"}
)
queryset
- 클라이언트가 보낸 ID(예: 5)가 유효한지 검사할 '탐색 범위'를 지정
required
- 글을 작성할 때 반드시 ~를 지정해야 하는지 묻는 옵션
allow_null
- 클라이언트가 값을 비워서(null) 보내는 것을 허용할지 묻는 옵션
write_only
- Browsable API에서 비밀번호 입력창을 마스킹 처리함
class Meta:
model = Post
fields = [
"title",
"content",
"thumbnail",
"summary",
"is_temp",
"tags",
]
extra_kwargs = {
'summary': {'required': False, 'allow_blank': True},
'is_temp': {'default': False},
}
View 📌
옵션
is_valid(raise_exception=True)
- 유효성 검사 실패 시 자동으로 400 Bad Request를 반환
인증 📌
JWT
uthenticate
authenticate(email=email, password=password)
- 이 함수는 이메일과 비밀번호가 DB와 일치하는지 확인하고, 일치하면 User 객체를 반환함
- 일치하지 않으면 None을 반환함
RefreshToken.for_user(user)
efreshToken.for_user(user) 호출하면 해당 유저를 위한 Refresh/Access 토큰 쌍이 생성됨
service 📌
메서드
create_user
user = User.objects.create_user(
email=email,
nickname=nickname,
password=password
)
create_user 메서드는 UserManager(managers.py)에 정의된 로직을 따름
- 내부적으로
set_password()를 호출하여 비밀번호를 암호화(hashing) 저장
def create_user(self, username, email=None, password=None, **extra_fields):
user = self.model(username=username, email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
create
전달된 인자를 데이터베이스에 그대로 저장
- 비밀번호를 평문(Plain Text)으로 넣으면 암호화되지 않은 채로 저장되어 보안에 치명적
user = User.objects.get(username='gemini')
user.set_password('new_password123')
user.save()
- 위의 예시처럼 이미 존재하는 유저의 비밀번호를 변경할 경우에는
set_password만 따로 사용하기도 함
데코레이터 📌
@property
메서드를 마치 클래스의 속성(Attribute)처럼 접근할 수 있게 만들어줌
- 단순히 값을 읽는 것 이상의 제어권을 가질 수 있어, 데이터 캡슐화와 유지보수 측면에서 필수
핵심역할
- 보통 메서드를 호출할 때는
obj.get_name()처럼 괄호()를 붙여야 하지만
@property 를 사용하면 obj.get_name처럼 괄호 없이 변수처럼 호출할 수 있음
주요 이점
- 캡슐화: 내부 데이터를 보호하면서 외부에는 인터페이스만 제공
- 유효성 검사: 값을 설정할 때 미리 정의한 조건에 맞는지 확인 가능
- 계산된 속성: 실제 변수로 저장되어 있지는 않지만, 호출 시점에 계산된 값을 반환
예시
class SmartPhone:
def __init__(self, price):
self._price = price
@property
def price(self):
"""메서드를 속성처럼 읽게 해주는 Getter 역할입니다."""
print("가격을 조회합니다.")
return f"{self._price:,}원"
@price.setter
def price(self, value):
"""속성에 값을 할당할 때 호출되는 Setter 역할입니다. 유효성 검사가 가능합니다."""
if value < 0:
raise ValueError("가격은 0원보다 작을 수 없습니다!")
print(f"가격을 {value}원으로 업데이트합니다.")
self._price = value
phone = SmartPhone(1000000)
print(phone.price)
phone.price = 1200000
배포 📌
nginx
- 가볍고 높은 성능을 자랑하는 웹 서버(Web Server)이자 리버스 프록시(Reverse Proxy) 서버 프로그램
Nginx의 핵심 이론
어떻게 압도적인 성능을 내는가?
- Nginx가 전 세계적으로 가장 많이 쓰이는 웹 서버가 된 이유는
- '이벤트 기반(Event-Driven)'의 비동기(Asynchronous) 아키텍처를 사용하기 때문
기존 방식 (Apache 등)
- 클라이언트의 요청이 들어올 때마다 새로운 프로세스나 스레드를 생성하는
- 스레드 기반(Thread-Driven) 모델이었음
- 동시 접속자가 많아지면 스레드가 무한정 생성되어 메모리가 고갈되고
- 서버가 뻗어버리는 문제(C10K 문제)가 발생하기 쉬움
Nginx 방식
- 소수의 고정된 워커 프로세스(Worker Process)만 띄워놓고
- 들어오는 수많은 요청들을 '이벤트'로 취급하여 비동기 방식으로 처리
- 하나의 요청이 디스크 I/O 등으로 대기 상태에 들어가면 프로세스가 쉬지 않고
- 즉시 다른 이벤트를 처리함, 덕분에 적은 메모리로 수만 개의 동시 접속을 가볍게 처리할 수 있음
Nginx가 꼭 필요한 이유
- 웹 애플리케이션(Spring Boot, Node.js, Django 등)을 만들고 나면, 그 자체로도 서버를 띄울 수는 있음
- 하지만 실제 서비스 환경에 배포할 때는 반드시 그 앞에 Nginx를 두는 것이 표준 명세처럼 자리 잡았음
이유
① 리버스 프록시 (Reverse Proxy) 역할을 통한 보안 및 포트 숨김
- Nginx를 인터넷 망과 내부 백엔드 서버(앱 서버) 사이에 둠
- 보안 강화
- 사용자는 내부 서버의 진짜 IP나 실행 중인 포트(ex. 3000, 8080)를 알 수 없음
- Nginx가 80(HTTP) 또는 443(HTTPS) 포트로만 요청을 받아
- 내부망에 있는 앱 서버로 몰래 전달(Proxy)해주기 때문에
- 백엔드 서버가 해커에게 직접 노출되는 것을 막아줌
② 정적 파일(Static Content)의 효율적인 분리 처리
- HTML, CSS, JavaScript, 이미지 파일 같은
- 정적 리소스는 굳이 무거운 백엔드 애플리케이션 서버가 처리할 필요가 없음
- Nginx는 웹 서버로서 정적 파일을 제공하는 데 매우 특화되어 있음
- Nginx가 앞단에서 정적 파일을 바로 클라이언트에게 던져주고
- 백엔드 서버는 DB 조회나 복잡한 로직(동적 처리)에만 집중할 수 있게 하여
- 전체 시스템의 퍼포먼스를 극대화
③ 로드 밸런싱 (Load Balancing)
- 블로그가 유명해져서 트래픽이 감당 안 돼 백엔드 서버를 3대(서버 A, B, C)로 늘렸다고 가정
- Nginx는 들어오는 엄청난 양의 트래픽을 서버 A, B, C로 골고루 분산시켜 줌
- 특정 서버에만 부하가 몰려 서버가 다운되는 것을 방지
④ SSL/TLS 암호화 처리 (HTTPS 적용)
- 현대 웹 환경에서 보안을 위한 HTTPS 적용은 필수임
- 이를 위해서는 트래픽을 암호화하고 복호화하는 연산 과정이 필요함
- 이 무거운 암복호화 작업을 백엔드 서버가 직접 하게 되면 서버의 부담이 매우 커짐
- Nginx를 앞에 두면 Nginx가 클라이언트와의 HTTPS 통신(SSL Termination)을 전담하고
- Nginx와 내부 백엔드 서버 사이에는 가벼운 HTTP 통신을 하게 만들어 앱 서버의 성능 저하를 막을 수 있음
⑤ 비정상 트래픽 차단 및 속도 제한 (Rate Limiting)
- 악의적인 디도스(DDoS) 공격이나 비정상적인 크롤링 봇의 접근이 발생했을 때
- Nginx 단에서 특정 IP의 초당 접속 횟수를 제한하거나
- 요청 버퍼 크기를 제한하여 뒤에 있는 진짜 서버를 보호하는 1차 방어선 역할을 함