Skip to main content
Summary

임시 데이터에서 데이터베이스로 넘어가기

지난 장에서는 View 안에 임시 목록을 만들고 템플릿에 전달했다. 예제를 이해하기에는 충분하지만, 실제 게시판이라면 금방 한계가 드러난다. 서버를 다시 실행하면 데이터가 사라지고, 여러 사용자가 글을 작성해도 안정적으로 저장할 곳이 없다.

웹 애플리케이션에서 데이터는 보통 데이터베이스에 저장한다. Django에서는 이 데이터 구조를 Python 코드로 정의한다. 이 코드가 바로 Model이다.

Model은 데이터베이스 테이블의 설계도다. 예를 들어 게시글 모델을 만들면 Django는 그 모델을 바탕으로 게시글 테이블을 만들 준비를 한다. 모델의 필드는 테이블의 컬럼이 된다. 그리고 migration을 거쳐 실제 데이터베이스에 반영하면, 그때부터 게시글 데이터를 저장하고 조회할 수 있다.

Django는 모델을 바탕으로 SQL을 직접 작성하지 않아도 데이터를 다룰 수 있게 해 준다. 이 기능을 ORM이라고 한다.

ORM은 Object-Relational Mapping의 줄임말이다. 쉽게 말해 데이터베이스의 행을 Python 객체처럼 다루게 해 주는 도구다. 개발자는 Post.objects.create(...)처럼 Python 코드를 작성하고, Django가 내부에서 적절한 SQL로 바꿔 데이터베이스에 전달한다.

Model과 ORM으로 데이터 설계하기AI 생성 이미지

모델은 테이블의 설계도다

게시판 앱에 게시글과 분류를 저장한다고 생각해 보자. boards/models.py에 다음과 같이 모델을 작성할 수 있다.

# boards/models.py
from django.db import models


class Category(models.Model):
    name = models.CharField(max_length=50, unique=True, verbose_name='이름')

    def __str__(self):
        return self.name


class Post(models.Model):
    category = models.ForeignKey(
        Category,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='posts',
        verbose_name='분류',
    )
    title = models.CharField(max_length=100, verbose_name='제목')
    content = models.TextField(verbose_name='내용')
    is_public = models.BooleanField(default=True, verbose_name='공개 여부')
    created_at = models.DateTimeField(auto_now_add=True, verbose_name='작성일')
    updated_at = models.DateTimeField(auto_now=True, verbose_name='수정일')

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = '게시글'
        verbose_name_plural = '게시글 목록'
        ordering = ['-created_at']

Category와 Post는 모두 models.Model을 상속한다. Django는 이 클래스를 보고 데이터베이스 테이블을 만들 준비를 한다. 기본적으로 각 모델에는 id라는 기본 키가 자동으로 추가된다. 기본 키는 각 데이터를 구분하는 고유한 값이다.

Post 모델에는 제목, 내용, 공개 여부, 작성일, 수정일, 분류가 있다. 이 필드 하나하나가 데이터베이스 테이블의 컬럼이 된다.

자주 쓰는 필드 이해하기

모델을 설계할 때 가장 먼저 정해야 할 것은 어떤 값을 어떤 형태로 저장할지다. Django는 자주 쓰는 데이터 타입을 필드 클래스로 제공한다.

CharField는 짧은 문자열을 저장할 때 사용한다. 제목, 이름, 전화번호처럼 길이 제한이 필요한 값에 적합하다. CharField에는 보통 max_length를 지정한다. 이 값은 저장할 수 있는 최대 글자 수를 의미한다.

TextField는 긴 문자열을 저장할 때 사용한다. 게시글 본문, 상품 설명, 댓글 내용처럼 길이가 길어질 수 있는 값에 어울린다. 제목은 CharField, 본문은 TextField로 나누어 생각하면 쉽다.

DateTimeField는 날짜와 시간을 함께 저장한다. auto_now_add=True를 지정하면 객체가 처음 만들어질 때 현재 시간이 저장된다. 작성일에 자주 쓴다. auto_now=True를 지정하면 객체가 저장될 때마다 현재 시간으로 갱신된다. 수정일에 적합하다.

BooleanField는 참 또는 거짓 값을 저장한다. 공개 여부, 활성 상태, 삭제 여부처럼 두 가지 상태 중 하나를 표현할 때 사용한다. default=True처럼 기본값을 지정해 두면 새 데이터를 만들 때 값을 생략해도 기본값이 들어간다.

ForeignKey는 다른 모델과의 관계를 나타낸다. 위 예제에서 게시글은 하나의 분류에 속할 수 있다. 그래서 Post에서 Category를 가리키는 ForeignKey를 사용했다. 반대로 하나의 분류에는 여러 게시글이 연결될 수 있다.

ForeignKey에서 중요한 옵션은 on_delete다. 연결된 분류가 삭제될 때 게시글을 어떻게 처리할지 정한다. models.CASCADE는 분류가 삭제되면 연결된 게시글도 함께 삭제한다. models.SET_NULL은 연결만 비우고 게시글은 남긴다. 이 옵션을 쓰려면 해당 필드에 null=True가 필요하다. models.PROTECT는 연결된 게시글이 있으면 분류 삭제를 막는다.

예제에 있는 related_name='posts'는 반대 방향으로 조회할 때 사용할 이름이다. 예를 들어 어떤 분류 객체가 notice라면, notice.posts.all()처럼 그 분류에 연결된 게시글들을 가져올 수 있다. 지정하지 않아도 Django가 기본 이름을 만들어 주지만, 직접 정해 두면 코드를 읽기가 더 쉽다.

null=True와 blank=True도 자주 헷갈린다. null=True는 데이터베이스에 빈 값을 NULL로 저장할 수 있다는 뜻이다. blank=True는 폼이나 관리자 화면에서 값을 비워 둘 수 있다는 뜻이다. 데이터베이스 저장 규칙과 입력 검증 규칙이 다르기 때문에 둘을 구분해서 생각해야 한다.

verbose_name은 사람이 읽기 쉬운 필드 이름이다. 지금은 효과가 크게 보이지 않을 수 있지만, 다음 장에서 관리자 페이지를 사용할 때 이 이름이 화면에 표시된다.

모델 변경을 테이블에 반영하기

모델 클래스를 작성했다고 해서 데이터베이스 테이블이 바로 생기지는 않는다. Django는 모델 코드와 데이터베이스 사이에 migration이라는 변경 이력 파일을 둔다.

흐름은 세 단계로 나눌 수 있다.

첫째, models.py에 모델을 작성하거나 수정한다.

둘째, makemigrations 명령으로 변경 이력 파일을 만든다.

python manage.py makemigrations

이 명령은 현재 모델 상태를 확인하고, 이전 migration과 비교해 무엇이 달라졌는지 기록한다. 새 모델을 만들었다면 테이블 생성 기록이 만들어지고, 필드를 추가했다면 컬럼 추가 기록이 만들어진다. 생성된 파일은 보통 앱의 migrations 폴더 안에 저장된다.

셋째, migrate 명령으로 변경 이력을 실제 데이터베이스에 적용한다.

python manage.py migrate

makemigrations는 설계 변경 내용을 파일로 만드는 단계이고, migrate는 그 변경 내용을 데이터베이스에 실행하는 단계다. 둘은 역할이 다르다. 모델을 바꾸고 makemigrations만 실행하면 아직 데이터베이스는 바뀌지 않는다. 반대로 migration 파일이 없으면 migrate가 적용할 모델 변경 내용도 없다.

실무에서는 migration 파일도 코드와 함께 관리한다. 다른 개발자나 배포 서버가 같은 데이터베이스 구조를 만들 수 있어야 하기 때문이다.

주의할 점도 있다. 이미 데이터가 들어 있는 테이블에 필수 필드를 새로 추가하면 기존 데이터에 어떤 값을 넣을지 정해야 한다. 이때는 default를 지정하거나, 처음에는 null=True로 추가한 뒤 데이터를 채우고 다시 제약을 강화하는 방식으로 진행할 수 있다.

ORM으로 데이터 만들고 조회하기

이제 모델을 데이터베이스에 반영했다면 ORM으로 데이터를 다룰 수 있다. Django shell을 열어 직접 확인해 보자.

python manage.py shell

먼저 모델을 가져온다.

from boards.models import Category, Post

데이터를 만들 때는 objects.create()를 사용할 수 있다.

notice = Category.objects.create(name='공지')

post = Post.objects.create(
    category=notice,
    title='첫 번째 게시글',
    content='Django ORM으로 저장한 글입니다.',
    is_public=True,
)

여기서 objects는 모델의 기본 매니저다. 매니저는 데이터베이스 조회와 생성을 시작하는 입구라고 보면 된다.

전체 게시글을 가져올 때는 all()을 사용한다.

posts = Post.objects.all()

posts는 단순한 리스트가 아니라 QuerySet이다. QuerySet은 데이터베이스 조회를 표현하는 객체다. 반복문으로 사용하거나 실제 값이 필요해지는 시점에 데이터베이스 조회가 실행된다. 그래서 조건을 덧붙이거나 정렬을 이어 붙이기 좋다.

조건을 걸어 조회할 때는 filter()를 사용한다.

public_posts = Post.objects.filter(is_public=True)
notice_posts = Post.objects.filter(category=notice)

filter()는 조건에 맞는 데이터가 없어도 오류를 내지 않고 빈 QuerySet을 반환한다. 목록 화면을 만들 때 자주 사용한다.

하나의 객체만 가져올 때는 get()을 사용한다.

post = Post.objects.get(id=1)

get()은 정확히 하나의 결과를 기대한다. 결과가 없으면 DoesNotExist 예외가 발생하고, 둘 이상이면 MultipleObjectsReturned 예외가 발생한다. 그래서 상세 페이지처럼 기본 키로 하나의 데이터를 찾을 때 적합하다.

정렬은 order_by()로 지정한다.

recent_posts = Post.objects.order_by('-created_at')
old_posts = Post.objects.order_by('created_at')

필드 이름 앞에 -를 붙이면 내림차순이다. 최신 글을 먼저 보여 주려면 -created_at을 사용한다.

수정과 삭제도 객체처럼 처리한다

ORM의 장점은 조회한 데이터를 Python 객체처럼 다룰 수 있다는 점이다. 게시글 제목을 수정하려면 객체를 가져와 값을 바꾸고 save()를 호출한다.

post = Post.objects.get(id=1)
post.title = '수정된 제목'
post.save()

삭제도 비슷하다.

post = Post.objects.get(id=1)
post.delete()

여러 데이터를 한 번에 수정할 수도 있다.

Post.objects.filter(is_public=False).update(is_public=True)

이 방식은 조건에 맞는 데이터를 데이터베이스에서 바로 수정한다. 객체를 하나씩 가져와 save()를 호출하는 방식과 다르게, 각 객체의 save() 메서드는 실행되지 않는다. 그래서 개별 객체마다 별도의 처리 로직이 필요하다면 update()를 사용할 때 주의해야 한다.

모델의 표현을 다듬기

모델은 데이터 구조만 정의하는 곳이 아니다. 그 데이터를 어떻게 표현할지도 함께 정리할 수 있다.

__str__() 메서드는 객체를 문자열로 표현할 때 사용된다. 예를 들어 Post 객체를 출력했을 때 Post object (1)처럼 보이면 어떤 글인지 알기 어렵다. return self.title을 작성해 두면 게시글 제목이 표시된다.

def __str__(self):
    return self.title

이 작은 메서드는 관리자 페이지, shell, 디버깅 과정에서 큰 차이를 만든다. 모델을 만들 때는 가능하면 사람이 알아볼 수 있는 대표 값을 __str__()로 반환하도록 작성하는 것이 좋다.

Meta 클래스는 모델 전체의 동작을 조정한다. 위 예제에서는 다음 옵션을 사용했다.

class Meta:
    verbose_name = '게시글'
    verbose_name_plural = '게시글 목록'
    ordering = ['-created_at']

verbose_name은 모델 하나를 부르는 이름이고, verbose_name_plural은 여러 개를 부르는 이름이다. 관리자 페이지에서 더 자연스러운 이름으로 표시된다.

ordering은 기본 정렬 기준이다. Post.objects.all()처럼 별도 정렬을 지정하지 않아도 최신 글이 먼저 나오게 만들 수 있다. 다만 특정 화면에서 다른 정렬이 필요하면 order_by()를 사용해 덮어쓸 수 있다.

View에서 ORM 사용하기

모델과 ORM은 View에서 본격적으로 사용된다. 이제 임시 목록 대신 데이터베이스에 저장된 게시글을 가져와 템플릿에 전달할 수 있다.

# boards/views.py
from django.shortcuts import render
from .models import Post


def post_list(request):
    posts = Post.objects.filter(is_public=True).order_by('-created_at')
    return render(request, 'boards/index.html', {'posts': posts})

이 View는 공개된 게시글만 조회하고, 최신 글이 먼저 오도록 정렬한 뒤 템플릿에 전달한다. 템플릿 입장에서는 지난 장과 크게 달라지지 않는다. 여전히 posts를 반복하면서 제목과 내용을 출력하면 된다. 달라진 점은 데이터의 출처다. 이제 데이터는 View 안의 임시 리스트가 아니라 데이터베이스에서 온다.

이 분리가 중요하다. Model은 데이터 구조와 데이터베이스와의 연결을 맡는다. View는 요청에 맞는 데이터를 고르고 응답을 준비한다. Template은 전달받은 데이터를 화면에 배치한다. 각 구성요소가 자기 역할을 지킬수록 코드가 커져도 흐름을 따라가기 쉬워진다.

정리

Model은 데이터베이스 테이블을 Python 코드로 표현한 설계도다. 필드는 컬럼이 되고, 모델 객체는 데이터베이스의 한 행과 연결된다. makemigrations는 모델 변경 이력을 만들고, migrate는 그 변경을 실제 데이터베이스에 적용한다.

ORM을 사용하면 create, all, get, filter, order_by 같은 메서드로 데이터를 만들고 조회할 수 있다. 수정은 값을 바꾼 뒤 save(), 삭제는 delete()로 처리한다. 여러 데이터를 한꺼번에 바꿀 때는 update()도 사용할 수 있다.

__str__()와 Meta는 모델을 더 다루기 쉽게 만든다. 사람이 읽기 쉬운 이름과 기본 정렬을 정해 두면 관리자 페이지와 조회 코드에서 같은 모델을 더 편하게 사용할 수 있다.

이제 게시판의 데이터 구조가 준비되었다. 다음 장에서는 이 모델을 Django 관리자 페이지에 등록하고, 별도의 화면을 만들기 전에 브라우저에서 데이터를 직접 관리하는 방법을 살펴본다.

책 목록

Django 활용법: 웹 개발 기본기부터 배포와 운영까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기