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 기본 패턴

형식은 다음 세 가지가 거의 전부입니다.

  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.iduser_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. 실무에서 기억해 두면 좋은 규칙 요약

  1. FK 값으로만 필터할 때

python Post.objects.filter(user=request.user) Post.objects.filter(user_id=request.user.id)

두 가지 모두 괜찮습니다. 팀 컨벤션에 맞추되, 개인적으로는 가독성 때문에 객체를 넘기는 쪽(user=request.user)을 선호하는 경우가 많습니다.

  1. 관련 모델의 다른 필드 기준으로 필터할 때

python Post.objects.filter(user__is_active=True) Post.objects.filter(user__email__endswith="@example.com")

이런 경우에 __ 체인이 진짜 힘을 발휘합니다.

  1. 템플릿/뷰에서 FK 값만 필요할 때

```python # 나쁠 수 있음 (추가 쿼리 발생) {{ post.user.id }}

# 더 가벼움 (추가 쿼리 없음) {{ post.user_id }} ```

특히 select_related("user") 를 안 썼다면, 리스트 화면에서 N+1 쿼리의 원인이 될 수 있습니다.

  1. User의 여러 필드를 자주 쓴다면

```python posts = Post.objects.select_related("user")

for post in posts: print(post.user.username, post.user.email) ```

이렇게 select_related + post.user.필드 조합을 쓰는 게 가장 깔끔하고, 쿼리 수도 적습니다.

  1. __ 체인은 “JOIN이 늘어날 수 있다”는 걸 항상 염두에 두기
  • 특히 역방향 FK(+ 여러 번의 filter().filter() 체인)에서 잘못 쓰면 쿼리에 같은 테이블이 여러 번 JOIN 되면서 중복 결과가 나오기도 합니다.

8. 한 줄 정리

  • _ : 그냥 이름. 특히 <필드명>_id 는 FK가 가진 실제 컬럼 이름.

  • __ : ORM 전용 연산자. 필드/관계/조건/변환을 이어 붙이는 룩업 구분자.

  • . : 파이썬 속성 접근. user.id 는 User 객체를 불러온 다음 그 PK를 읽는 것.

  • user_idPost 테이블의 FK 값

  • user.idUser 객체의 PK 값

  • user__idORM 쿼리에서 “user 관계를 타고 간 User.id에 조건”

이 세 가지를 정확히 구분해 두면:

  • 쿼리 의도가 더 명확해지고
  • 불필요한 JOIN이나 N+1 쿼리도 줄일 수 있고
  • 팀 내 코드 리뷰에서 “이건 user_id 쓰는 게 낫겠다” 같은 피드백도 훨씬 수월해집니다.

image