[ Django ] 참조

0417taehyun/django-reference

설명과 함께 직접 코드를 통해 확인하고 싶은 분들의 경우 해당 Repository를 활용하시면 됩니다. reference.sql 이라는 파일을 통해 본 글에 나와 있는, 같은 데이터를 사용하실 수 있습니다.

참조

장고를 통해 데이터베이스를 설계하면 Foreign Key를 활용하여 서로 관계를 연결시켜야 하는 경우가 생긴다. 이러한 경우를 참조라 하는데 우선 아래 이미지를 한번 보자.

users 테이블과 posts 테이블이 참조 관계로 되어있다. 한 명의 사용자가 여러 게시글을 쓸 수 있기 때문이다. 예를 들면 현재 이 기술 블로그 또한 내가 여러 글을 게시하고 있으니 위 이미지와 같은 One to Many 관계인 것을 알 수 있다.

이때 참조하는 방향에 맞게 posts 테이블에서 users 테이블의 정보를 가져오는 경우를 정참조라 한다. 예를 들어 1번 게시글을 작성한 사용자의 정보를 가져오는 것이다. 반대로 users 테이블에서 posts 테이블의 정보를 가져오는 경우를 역참조라 한다. 예를 들어 어떤 사용자가 작성한 모든 게시글의 정보를 가져오는 것이다. 혹시 헷갈린다면 쉽게 Foreign Key가 없는 테이블에서 Foreign Key가 있는 테이블의 정보를 가져오는 걸 역참조라 생각하면 간단하다. 이제 본격적으로 Django에서 역참조를 통해 정보를 가져오는 두 가지 방법을 알아보자.

역참조

Django에서 역참조를 통해 어떤 정보를 가져올 때는 _set 또는 related_name 을 사용한다. 이때 이미지와 같이 데이터베이스에 정보가 저장되어 있는 상황이라 생각이다.

_set

_set 을 사용할 때는 보통 참조하는 대상에 아무것도 설정하지 않았을 때, 다시 말해 related_name 을 지정하지 않았을 때 사용한다. 아래 코드 예시를 한번 살펴보자. 위 ERD를 바탕으로 만든 models.py 파일의 코드다.

# models.py

from django.db import models

class User(models.Model):
    name     = models.CharField(max_length = 32, null = True, blank = True)
    email    = models.CharField(max_length = 64)
    password = models.CharField(max_length = 64)

    class Meta:
        db_table = 'users'

class Post(models.Model):
    title   = models.CharField(max_length = 32)
    content = models.CharField(max_length = 512)
    image   = models.CharField(max_length = 1024, null = True, blank = True)
    user    = models.ForeignKey('user', on_delete = models.CASCADE)

    class Meta:
        db_table = 'posts'

여기서 _set 을 통해 어떤 사용자가 작성한 게시글 정보를 가져오려면 _set 키워드 앞에 역참조 되는 테이블의 이름을 소문자로, 다시 말해 post_set 을 통해 가져올 수 있다. 결과는 아래 이미지와 같다.

이때 유의할 점은 _set 자체는 RelatedManager 를 가져온다는 것이다. 따라서 이것을 all() 과 같은 메소드를 통해 쿼리셋 정보를 얻을 수 있다. for 반복문을 통해 정상적으로 이태현 이라는 사용자가 작성한 게시글 10개의 정보를 역참조를 통해 가져온 것을 알 수 있다.

related_name 은 아래 코드 예시와 같이 해당 ForeignKey 에 원하는 이름을 적으면 된다. 그러면 _set 대신에 해당 이름을 사용하여 접근할 수 있다.

# models.py

from django.db import models

class User(models.Model):
    name     = models.CharField(max_length = 32, null = True, blank = True)
    email    = models.CharField(max_length = 64)
    password = models.CharField(max_length = 64)

    class Meta:
        db_table = 'users'

class Post(models.Model):
    title   = models.CharField(max_length = 32)
    content = models.CharField(max_length = 512)
    image   = models.CharField(max_length = 1024, null = True, blank = True)
    user    = models.ForeignKey('user', on_delete = models.CASCADE, related_name = 'writer')

    class Meta:
        db_table = 'posts'

앞서 이태현 이라는 사용자가 작성한 10개의 게시글에 접근했던 것과 마찬가지로 이제는 related_name 을 활용하여 김태현 이라는 사용자가 작성한 10개의 게시글 정보를 얻어보자. 앞서 작성한 코드와 똑같으며 _set 대신에 지정한 related_namewriter 을 사용하면 된다. 그 결과는 아래 이미지와 같다.

캐시

이제 참조 관계에서의 캐시에 관해 알아보자. 캐시는 쉽게 말해 데이터 정보를 임의로 저장하여 메모리 관리를 효율적으로 하는 것이다. 이때 실제로 Django ORM이 동작하는 과정을 보면서 어떻게 캐시가 발생하는지 알기 위해 settings.py 파일에 아래 코드를 입력하자.

# settings.py

LOGGING = {
    'disable_existing_loggers': False,
    'version': 1,
    'formatters': {
         'verbose': {
            'format': '{asctime} {levelname} {message}',
            'style': '{'
        },
    },
    'handlers': {
        'console': {
            'class'     : 'logging.StreamHandler',
            'formatter' : 'verbose',
            'level'     : 'DEBUG',
        },
        'file': {
            'level'     : 'DEBUG',
            'class'     : 'logging.FileHandler',
            'formatter' : 'verbose',
            'filename'  : 'debug.log',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers' : ['console','file'],
            'level'    : 'DEBUG',
            'propagate': False,
        },
    },
}

정참조

정참조 관계에서의 캐시는 select_related() 를 통해 이루어진다. 우선 select_related() 를 사용하지 않은 경우 아래 이미지와 같이 정보를 가져오고 싶을 때마다 데이터베이스에 접근하는 것을 알 수 있다. 아래 이미지의 코드는 posts 테이블에서 users 정보 중 name 을 가져와 리스트 컴프리헨션을 통해 lis_1 이라는 변수에 저장하는 로직인데 총 20개의 게시글이 존재하여 20개의 게시글에 대한 user 의 정보를 가져오기 위해 데이터베이스에 20번 접근하는 것이다.

이제 select_related() 를 통해 정참조 관계에서 캐시를 한 경우를 살펴보자. 그 결과는 아래 이미지와 같다. 위 이미지와 마찬가지로 리스트 컴프리헨션을 통해 name 데이터를 lis 라는 변수에 저장하는 로직인데 한 가지 차이점은 select_related() 를 사용했기 때문에 데이터베이스에 한번만 접근한 것을 알 수 있다.

settigns.py 파일에 작성한 디버깅 로직 덕분에 실제 Django ORM이 어떤 SQL Query를 사용했는지 알 수 있는데 select_related() 에서는 INNER JOIN 을 사용한 것을 확인할 수 있다.

역참조

역참조 관계에서의 캐시는 prefetch_related() 를 통해 이루어진다. 아래 이미지를 통해 prefetch_related() 를 사용하지 않은 경우와 사용한 경우의 차이점을 찾아보자.

출력이 동일하고 앞서 select_related() 처럼 데이터베이스 접근 자체의 차이가 보이지 않아 동일하게 보일 수 있다. 하지만 SQL Query를 자세히 살펴보면 prefetch_related() 를 사용한 경우 그렇지 않았을 때보다 Query가 짧은 것을 알 수 있다. 아래 코드를 통해서 자세히 보면 마지막에 WHERE 구문에서 'posts'.'user_id' = 1 이 아닌 IN (1) 을 사용한 것을 알 수 있다. Query를 줄여 더 효율적인 사용을 하는 것인데 이는 사실 지금 예시와 같은 One to Many 관계보다는 Many to Many 관계에서 더 빛난다.

// prefetch_related() 를 사용하지 않은 경우

SELECT `posts`.`id`, `posts`.`title`, `posts`.`content`, `posts`.`image`, `posts`.`user_id` FROM `posts` WHERE `posts`.`user_id` = 1 LIMIT 21;

// prefetch_related() 를 사용한 경우

SELECT `posts`.`id`, `posts`.`title`, `posts`.`content`, `posts`.`image`, `posts`.`user_id` FROM `posts` WHERE `posts`.`user_id` IN (1);

차이

그러면 이제 select_related()prefetch_related() 의 차이점이 궁금할 수 있다. 앞서 설명에서 select_related() 는 정참조 관계에서 prefetch_related() 는 역참조 관계에서 사용한다고 언급했다. 사실 prefetch_related() 는 정참조 관계에서 사용도 가능한데 위와 같은 디버깅을 통해 더 효율적인 방법을 선택하면 된다. 차이점은 바로 Query에서 JOIN 문의 사용여부다. select_related() 의 경우 앞선 예시에서 처럼 INNER JOIN 을 사용하여 Query에서 JOIN 을 하고 prefetch_related() 의 경우 데이터베이스에 나누어 접근하여 이후 이를 파이썬에 자체에서 JOIN 을 한다. 이는 아래 이미지와 같이 공식 문서에 나와 있는 차이점이다.

profile
Be Happy 😆

9개의 댓글

comment-user-thumbnail
2020년 11월 16일

공부하려고 이리저리 검색하다 들어왔는데, 이미지가 깨져요 ㅠ_ㅠ

2개의 답글
comment-user-thumbnail
2020년 11월 17일

포스팅 기다려도 될까요? : )

3개의 답글
comment-user-thumbnail
2021년 2월 4일

멋있습니다!

답글 달기