Django ORM에서 _, __, . 제대로 이해하기
user_id, user.id, user__id 까지 한 번에 정리
Django 코드에서 이런 것들 한 번쯤 보셨을 겁니다.
Post.objects.filter(user_id=1)
Post.objects.filter(user__id=1)
post.user.id
post.user_id
언더스코어 _, 더블 언더스코어 __, 점 . 이 다 섞여 있으니
헷갈리기 딱 좋죠. “다 같은 거 아닌가?” 하면서 막 섞어 쓰기 시작하면
미묘한 동작 차이 + 성능 이슈까지 같이 따라옵니다.
이 글에서는:
- Django ORM에서
_/__/.이 각각 정확히 무엇을 의미하는지 user_id/user.id/user__id의 의미와 성능 차이- 실무에서 언제 어떤 걸 쓰면 좋은지 기준
까지 한 번에 정리해 보겠습니다.
1. 큰 그림: 세 가지 기호의 역할
먼저 세 개를 아주 간단히 정리하면:
-
_(한 개 언더스코어) -
이름의 일부일 뿐입니다.
- 예:
created_at,user_id -
특히 Django에서
ForeignKey는<필드명>_id라는 실제 DB 컬럼을 자동으로 만듭니다. -
__(두 개 언더스코어) -
Django ORM 전용 “룩업 구분자(lookup separator)” 입니다.
filter(),exclude(),order_by(),values()등에서 쓰는 키워드 인자 이름 안에서만 의미가 있습니다.-
예:
age__gte=20(필드 + 조건)user__email='a@b.com'(관계 타고 다른 필드 접근)created_at__date=...(변환/트랜스폼 후 조건)
-
.(점, dot) -
파이썬의 속성 접근 연산자입니다.
obj.user,obj.user.id,qs.filter(...).order_by(...)이런 것들 전부 점 연산자.- ORM 입장에서는 “지금 메모리에 있는 파이썬 객체의 속성에 접근”하는 것일 뿐입니다. (필요하면 이 과정에서 쿼리가 추가로 나가기도 함 – Lazy Loading)
이제 각각을 Django ORM 관점에서 좀 더 자세히 보죠.
2. _ 와 _id: ForeignKey 주변에서의 의미
2.1 모델 정의 예시
from django.contrib.auth import get_user_model
from django.db import models
User = get_user_model()
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=100)
이렇게 정의하면 DB에는 대략 이런 컬럼이 생깁니다:
-
post테이블 -
id(PK) user_id(실제 FK 컬럼 이름)title
Django는:
- 파이썬 객체에서
post.user라는 속성을 만들어주고 (User 인스턴스) - 동시에
post.user_id라는 정수(혹은 PK 타입) 속성도 같이 만들어줍니다.
즉:
post.user # -> User 객체 (필요 시 쿼리 1번 더 나감)
post.user_id # -> User의 PK 값 (이미 가지고 있는 값, 추가 쿼리 없음)
2.2 쿼리에서 _id 쓰기
ForeignKey인 user 에 대해 쿼리할 때는 여러 가지 표현이 가능합니다.
# 1) FK 필드에 User 객체를 넘기기
Post.objects.filter(user=request.user)
# 2) FK 필드에 PK 값을 넘기기 (동일하게 동작)
Post.objects.filter(user=request.user.id)
# 3) _id 컬럼 이름을 직접 쓰기
Post.objects.filter(user_id=request.user.id)
이 세 가지는 결국 같은 WHERE 조건으로 컴파일됩니다. (단순 PK 비교인 경우)
정리:
_id는
- “FK 필드가 실제로 DB에 가진 컬럼 이름”이고
- ORM에서는 그 컬럼을 직접 쓰고 싶을 때 사용하는 이름입니다.
3. __ 더블 언더스코어 체인: Django ORM 룩업 구분자
이제부터가 Django ORM만의 문법입니다.
Django 공식 문서에서 __ 는 “ORM lookup separator” 라고 부릅니다.
필드 이름을 이어 붙여 관계를 타거나, 조건/변환을 붙일 때 사용합니다.
3.1 기본 패턴
형식은 다음 세 가지가 거의 전부입니다.
필드명__lookup
-
예:
age__gte=20,name__icontains='kim'2.관계필드__다른필드 -
예:
user__email='a@b.com',profile__company__name='...'3.필드__transform__lookup -
예:
created_at__date=date.today(),created_at__year__gte=2024
예제:
# 1) 단순 비교
Post.objects.filter(title__icontains="django")
# 2) 관계 타고 조건
Post.objects.filter(user__email__endswith="@example.com")
# 3) 날짜 필드의 연도만 꺼내서 비교
Post.objects.filter(created_at__year=2025)
이때 user__email 처럼 관계를 타면, DB 입장에서는 JOIN 이 발생합니다.
핵심:
__가 나오면 “ORM이 SQL을 만들 때 뭔가 더 복잡한 일을 하고 있겠구나 (JOIN / 함수 / 조건)” 정도로 이해하시면 됩니다.
4. . 점 연산자와 user.id, user_id
이제 user.id 와 user_id 를 함께 보겠습니다.
4.1 Django 인스턴스에서의 차이
post = Post.objects.first()
post.user # User 객체 (필요하면 이 순간 SELECT 쿼리 1번 더 나감)
post.user.id # 위에서 가져온 User 객체의 PK
post.user_id # Post 테이블에 이미 있는 FK 값
중요한 포인트:
post.user를 처음 접근하는 순간, 해당 User가 아직 로딩되지 않았다면 추가 쿼리가 나갑니다. (Lazy Loading)post.user_id는 이미post객체에 들어있는 정수 값이라 추가 쿼리 없음.- 따라서 “PK 값만 필요하다면”
user_id를 쓰는 게 더 가볍습니다.
4.2 성능 예시: 리스트 페이지에서의 차이
# views.py
posts = Post.objects.all() # select_related 안 씀
for post in posts:
print(post.user.id)
posts를 불러오는 쿼리: 1번- 루프를 돌면서
post.user에 처음 접근할 때마다 User 테이블 조회 쿼리가 1번씩 추가 → Post가 100개면 User 쿼리도 최대 100번… (전형적인 N+1 문제)
반면:
for post in posts:
print(post.user_id)
user_id는 이미 Post 테이블에서 가져온 컬럼이라 추가 쿼리 없음.
물론 아래처럼 select_related 를 써서 미리 JOIN 해 두면 post.user 를 써도 추가 쿼리는 발생하지 않습니다.
posts = Post.objects.select_related("user")
for post in posts:
print(post.user.id) # 추가 쿼리 없이 사용 가능
5. user_id, user.id, user__id 정확히 비교하기
이제 헷갈리는 세 가지를 딱 붙여서 보겠습니다.
5.1 user_id – DB 컬럼 직접 사용
-
어디서?
-
모델 인스턴스에서:
post.user_id - ORM 쿼리에서:
filter(user_id=1),order_by("user_id")등 -
의미
-
post테이블의 실제 FK 컬럼 (user_id) 를 그대로 쓰는 것. -
성능
-
인스턴스에서 읽을 때는 추가 쿼리 없음.
- 쿼리에서 쓰면 결국
WHERE "post"."user_id" = 1같은 조건이 됨.
5.2 user.id – 관계를 타서 PK에 접근
-
어디서?
-
파이썬 객체에서만 씁니다. (
post.user.id) -
의미
-
먼저
post.user(User 객체)를 불러오고, 그 객체의id속성을 읽는 것. -
성능
-
post.user가 아직 로딩 안 되어 있으면 그 순간 추가 쿼리가 나감. select_related("user")를 써두면 같은 쿼리 안에서 미리 가져오기 때문에 추가 쿼리 없이 사용 가능.
정리:
- “값만 필요”하면
user_id- “User의 다른 필드도 같이 쓸 거고, select_related도 했다”면
user.필드
5.3 user__id – ORM 룩업에서 관계 타고 조건 걸기
-
어디서?
-
ORM 쿼리 메서드의 키워드 인자 이름에서만 등장
- 예:
Post.objects.filter(user__id=1) -
의미
-
user라는 FK를 따라가서 User 테이블의 id 필드에 조건을 거는 것. -
동작
-
논리적으로는
JOIN user ON post.user_id = user.id그리고WHERE user.id = 1같은 쿼리가 되는 구조입니다. -
실제 SQL
-
단순한 경우엔 ORM이 알아서 최적화해서
user_id = 1과 비슷한 결과를 내기도 하지만, 정신 모델은 “관계를 타는 쿼리” 라고 생각하는 게 안전합니다.
6. _ vs __ 헷갈릴 수 있는 패턴 정리
6.1 필터에서 자주 보이는 조합
# 1) FK 컬럼 이름 직접 사용 (_id)
Post.objects.filter(user_id=1)
# 2) FK 필드에 PK 값 전달
Post.objects.filter(user=1)
# 3) 관계 타고 가서 조건 지정 (__)
Post.objects.filter(user__id=1)
Post.objects.filter(user__email__icontains="@example.com")
실무에서 보통의 기준은:
- 단순히 FK의 PK 값으로 필터 →
user_id=...또는user=request.user - 관련 모델의 다른 필드로 필터 →
user__email=...,user__is_active=True처럼__사용
6.2 _ 는 “이름의 일부”, __ 는 “ORM 연산자”
생긴 게 비슷해서 그렇지, 의미는 전혀 다릅니다.
-
_(underscore) -
created_at,user_id,is_active같은 그냥 이름 -
파이썬 변수, 모델 필드, DB 컬럼 어디서나 등장
-
__(double underscore) -
오직 ORM 쿼리에서만 특별한 의미
filter(),exclude(),annotate(),values()등의 키워드 인자 이름 안에서만 문법으로 인식- “필드 → 관계 → 변환 → 조건”을 이어 붙이는 구분자
7. 실무에서 기억해 두면 좋은 규칙 요약
- FK 값으로만 필터할 때
python
Post.objects.filter(user=request.user)
Post.objects.filter(user_id=request.user.id)
두 가지 모두 괜찮습니다. 팀 컨벤션에 맞추되, 개인적으로는 가독성 때문에 객체를 넘기는 쪽(user=request.user)을 선호하는 경우가 많습니다.
- 관련 모델의 다른 필드 기준으로 필터할 때
python
Post.objects.filter(user__is_active=True)
Post.objects.filter(user__email__endswith="@example.com")
이런 경우에 __ 체인이 진짜 힘을 발휘합니다.
- 템플릿/뷰에서 FK 값만 필요할 때
```python # 나쁠 수 있음 (추가 쿼리 발생) {{ post.user.id }}
# 더 가벼움 (추가 쿼리 없음) {{ post.user_id }} ```
특히 select_related("user") 를 안 썼다면, 리스트 화면에서 N+1 쿼리의 원인이 될 수 있습니다.
- User의 여러 필드를 자주 쓴다면
```python posts = Post.objects.select_related("user")
for post in posts: print(post.user.username, post.user.email) ```
이렇게 select_related + post.user.필드 조합을 쓰는 게 가장 깔끔하고, 쿼리 수도 적습니다.
__체인은 “JOIN이 늘어날 수 있다”는 걸 항상 염두에 두기
- 특히 역방향 FK(+ 여러 번의
filter().filter()체인)에서 잘못 쓰면 쿼리에 같은 테이블이 여러 번 JOIN 되면서 중복 결과가 나오기도 합니다.
8. 한 줄 정리
-
_: 그냥 이름. 특히<필드명>_id는 FK가 가진 실제 컬럼 이름. -
__: ORM 전용 연산자. 필드/관계/조건/변환을 이어 붙이는 룩업 구분자. -
.: 파이썬 속성 접근.user.id는 User 객체를 불러온 다음 그 PK를 읽는 것. -
user_id는 Post 테이블의 FK 값 -
user.id는 User 객체의 PK 값 -
user__id는 ORM 쿼리에서 “user 관계를 타고 간 User.id에 조건”
이 세 가지를 정확히 구분해 두면:
- 쿼리 의도가 더 명확해지고
- 불필요한 JOIN이나 N+1 쿼리도 줄일 수 있고
- 팀 내 코드 리뷰에서 “이건 user_id 쓰는 게 낫겠다” 같은 피드백도 훨씬 수월해집니다.

댓글이 없습니다.