일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 주니어개발자역량강화
- 주니어개발자멘토링
- cp949
- not a git repository
- 파이썬 sep
- MomentumParameters
- EnvCommandError
- 파이썬 int()
- fatal:not a git repository
- 파이썬
- Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
- 코딩테스트
- Til
- 백준
- 개발자스터디
- print sep
- 파이썬 클래스
- 파이썬 map 함수
- 코딩부트캠프후기
- 10430번
- 항해플러스
- print("""
- vscode cp949
- 99클럽
- 파이썬 |
- 99클럽 #99일지 #코딩테스트 #개발자스터디 #항해 #til
- 항해99
- 개발자사이드프로젝트
- 99일지
- 항해
- Today
- Total
선발대
[스파르타] Django 심화반 2주차 (완강) 본문
수업목표
- Github Action에서 Database와 연결된 자동화 테스트를 실행할 수 있다.
- 좋아요 기능의 요구사항을 정의하고 Model 클래스를 구현한다.
- auto_now, AbstractBaseClass, unique_together를 배운다.
01. 마이그레이션
Migration
- 데이터베이스 변경할 때마다 마이그레이션이 필요함
- python manage.py migrate
- 파이썬 database 탭에서 테이블 생성을 확인함
Migration이 필요한 이유
- 데이터베이스의 형상은 마이그레이션을 사용해서 관리함.
- 버전관리시스템처럼 변경 이력을 보존함.
- 로컬 데이터베이스에서 일어난 수정사항을 그대로 실서버 데이터베이스에도 적용 가능.
- 변경이력이 남아야지 필드의 이름이 변경되었고 그 데이터가 그대로 넘어갈 수 있음.
- rollback 할때도 유리함. 바로 직전의 데이터베이스로 형상 되돌리기.
02. Github Action에서도 데이터베이스 연결...이 안 되잖아?
- 커밋과 푸쉬하면 에러난다.
- github action 환경에서는 데이터베이스 없으므로 만들어서 연결해줘야 함.
- tmate: 액션이 실행되고 있는 host system으로 바로 들어갈 수 있는 방법을 제공함.
Github Action에서의 디버깅
# tmate
name: Setup tmate session
uses: mxschmitt/action-tmate@v3
# mysql 켜기
sudo systemctl start mysql
# root 유저의 비밀번호 바꾸기
use mysql;
FLUSH PRIVILEGES;
ALTER USER 'root'@'localhost' IDENTIFIED BY '22380476';
# sparta 데이터베이스 생성하기
CREATE DATABASE sparta;
# 시간대가 정확한지 확인하기
SELECT NOW();
# 시간대 변경하기
sudo rm /etc/localtime
sudo ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime
- github action은 자신이 맡은 모든 step, job을 끝내면 자동으로 꺼짐.
- 이를 방지하기 위해 tmate가 무한루프를 돈다.
- tmate step이 실행되면 무한루프로 도는 동시에 ssh 연결이나 웹을 통해 머신 안으로
- 들어갈 수 있는 통로를 열어준다. (근데 버그 있어서 외부보다 ssh 연결이 그나마 용이)
- 디버깅 기능에서는 강력함.
03. Github Action에서 데이터베이스 연결하기
ci.yml
name: Django CI
on:
push:
jobs:
ci:
env:
DB_DATABASE: sparta
DB_USER: root
DB_PASSWORD: 22380476
runs-on: ubuntu-latest
steps:
- name: Check out the codes
uses: actions/checkout@v2
- name: Set timezone to KST
run: |
sudo rm /etc/localtime
sudo ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime
# Start Mysql
# https://ovirium.com/blog/how-to-make-mysql-work-in-your-github-actions/
- name: Start Mysql
run: |
sudo systemctl start mysql
mysql -e "use mysql; FLUSH PRIVILEGES; ALTER USER '${{ env.DB_USER }}'@'localhost' IDENTIFIED BY '${{ env.DB_PASSWORD }}';" -uroot -proot
mysql -e 'CREATE DATABASE ${{ env.DB_DATABASE }};' -u${{ env.DB_USER }} -p${{ env.DB_PASSWORD }}
- name: Setup python environment
id: setup-python
uses: actions/setup-python@v2
with:
python-version: 3.9.9
- name: Install Poetry
run: |
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
echo "${HOME}/.poetry/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
${HOME}/.poetry/bin/poetry install
- name: Test python project
run: |
poetry run python manage.py test
- github action 에서 테스트할 때 쓰는 데이터베이스에 중요한 정보 없으므로 secret 사용 안함.
- 따라서 github action에서 테스트할 때 쓰는 계정정보는 그냥 env에 담아서 전달함.
- 변경사항:
- timezone을 kst로 변경
- github action에서 사용하는 ubuntu 20 이미지에는 이미 mysql이 설치되어있음.
- 따라서 system control을 사용해서 이미 설치된 mysql을 켜주기만 하면 됨.
- root user의 비밀번호 변경함. (FLUSH PREVILEGES, ALTER USER)
04. 환경별로 다른 설정 값 쓰기
- 환경별로 다른 설정값을 쓰는 방법에는 여러 가지가 있음.
- 여기에서는 settings, local_settings를 사용할 것임.
- .gitignore에 sparta/local_settings.py 추가함.
- 실제로도 sparta/local_settings.py 만들어준다.
## sparta/local_settings.py
# For Production
# SECRET_KEY = "django-insecure-uspu)$wi(do!x3vt#quwvlba)ne=+i=(^r$axqw1r1^6n8rn%w"
# DEBUG = False
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "sparta",
"USER": "root",
"PASSWORD": "22380476",
"HOST": "localhost",
"PORT": "3306",
}
}
## sparta/settings.py
try:
from sparta.local_settings import *
except ImportError:
pass
- settings.py 끝에 추가하는 이유. 위의 값들을 덮어쓰기 위해서.
- 이렇게 분리하면 나중에 프로덕션 서버에서 값 덮어쓰기 가능해짐.
05. 좋아요 Model 구현해보기
좋아요 기능의 요구사항
- 게시물에 좋아요 누를 수 있음. 누르면 좋아요 개수 증가함.
- 한 사용자는 하나의 게시글에 하나의 좋아요만 할 수 있음.
- 이미 좋아요 했는지 여부 표시해줘야 함.
- 이미 좋아요 했다면 버튼을 다시 눌렀을 때 취소 가능하도록 해야 함.
- 게시글을 최신순으로 보여주고 좋아요 개수도 같이 보여준다.
새로운 app 생성
- 앱 생성: python manage.py startapp tabom
- settings.py > INSTALLED_APPS에 위의 앱 추가함.
- "tabom.apps.TabomConfig"
06. 모델 클래스 만들기
- tabom의 models.py 삭제
- 파일을 models.py 라는 디렉터리로 생성함.
user 모델 만들기
## user.py
from django.db import models
class User(models.Model)
name = models.CharField(max_length=50)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
article 모델 만들기
## article.py
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=255)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
like 모델 만들기
## like.py
from django.db import models
from tabom.models.article import Article
from tabom.models.user import User
class Like(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
article = models.ForeignKey(Article, on_delete=models.CASCADE)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
__init.py__
## __init.py__
from tabom.models.article import Article as Article
from tabom.models.like import like as Like
from tabom.models.user import User as User
마이그레이션
- 공식문서: https://docs.djangoproject.com/ko/4.0/intro/tutorial02/#activating-models
- 새로운 app 을 installed_apps 배열에 추가함.
- makemigration 이후에 sqlmigrate로 실제 어떤 SQL문이 실행되는지 조회하기.
- 마이그레이션: python manage.py makemigrations tabom
- 어떤 sql이 실행되는지 확인하기: python manage.py sqlmigrate tabom 0001
- python manage,py migrate
- 테이블이 잘 생성되었는지 database tab에서 확인함.
07. auto_now 사용해보기
auto_now 탐구하기
- updated_at = models.DateTimeField(auto_now=True)
- created_at = models.DateTimeField(auto_now_add=True)
- django ORM을 사용해서 model 객체를 save() 했을 때, 필드에 값을 전달하지 않아도
- 자동으로 해당 필드가 업데이트 됨.
- django를 통하지 않고 데이터베이스의 row를 직접 수정한다면 실행 안됨.
- django를 써도 raw sql 실행하면 역시 auto_now, add 실행 안됨.
08. Timezone
실제 데이터베이스에 들어가는 시간 확인하기
- timezone에 대해 짚고 넘어가기
- console을 열어서 실제 DB에 잘 들어가는지 확인하기
from tabom.models.user import User
user = User(name='test')
user.save()
- 확인해보면 DB에는 UTC 시간으로 들어간다.
TZ 설정하기
- UTC 시간으로 들어가는 이유는 TZ 설정을 하지 않았기 때문.
- settings.py 에서 UTC = Asia/Seoul, USE_TZ = False로 변경함.
- 다시 user를 insert 하면 한국 시간으로 들어감.
- db에 들어가는 시간과 django orm에서 보이는 시간이 일치하게 됨. (DB에 한국시간 넣기)
- 강의할 때는 이렇게 넣는데, 정석은 그냥 DB에 UTC로 넣고, 서버에서 각자 시간대로 변환함.
09. auto now의 동작방식 확인
save() 시 동작
- tests/test_auto_now.py 에서 updated_at, created_at에 값을 전달하지 않아도 있는 것 확인 가능.
콘솔에서 직접 row를 추가했을 때의 동작
- 장고의 test는 별도의 DB에서 이루어짐.
- mysql 클라이언트로 user를 생성하려고 하면, auto_now가 도와주지 않기 때문에
- updated_at, created_at이 null이 될 수 없다는 에러가 뜬다.
raw sql을 실행했을 때의 동작
- djanog에서 raw sql 실행할 때도 auto_now는 작동하지 않음.
- 공식문서: https://docs.djangoproject.com/ko/4.0/topics/db/sql/#executing-custom-sql-directly
def test_auto_now_field_not_set_when_raw_sql_update_executed(self) -> None:
# Given
with connection.cursor() as cursor:
cursor.execute(
"INSERT INTO tabom_user(id, name, updated_at, created_at) "
"VALUES (1, 'hihi', '1999-01-01 10:10:10', '1999-01-01 10:10:10')"
)
# When
sleep(1)
cursor.execute(
"UPDATE tabom_user SET name='changed' WHERE id=1"
)
# Then
user = User.objects.filter(id=1).get()
self.assertEqual(user.updated_at, datetime(year=1999, month=1, day=1, hour=10, minute=10, second=10))
- cursor.execute()를 사용해서 raw sql로 user을 넣음.
- 만약 auto_now가 작동했다면, sleep 하면서 지나간 시간이 updated_at 컬럼에 들어가야 함.
- 그렇지만 updated_at 컬럼은 그대로임. 작동 안한 것을 알 수 있음.
Timezone을 설정하지 않았을 때의 동작
- 위에서 timezone 설정을 했는데, 설정 안하면 에러 난다.
- 왜냐하면 한 쪽은 tzinfo가 있는 datetime이고,
- 다른 하나는 가지고 있지 않은 datetime이라서 서로 같은 게 아님.
- 관련 링크: https://spoqa.github.io/2019/02/15/python-timezone.html
- datetime: 파이썬에서 기본으로 제공하는 표준 라이브러리. 날짜, 시각 조작하기 위한 클래스 제공.
- naive datetime: 날짜, 시각만을 가짐
- aware datetime: + 시간대정보(tzinfo)
- 시간 관련 작업을 한다면 시간대를 꼭 명시해라. (not naive, but aware datetime)
- 시각을 애플리케이션 로직이나 데이터베이스에서 저장할 때는 UTC로 사용하고,
- 유저에게 표시할 때만 유저의 시간대로 변환하여 보여주도록 해라.
- 장기적으로 보존해야하는 datetime은 항상 UTC를 기준으로 저장해라. 지역 시간대는 변경 가능성.
- 강의에서만 naive한 객체 사용할 예정임.
test 주석처리
- 써드파티 모듈은 단위테스트로 테스트하지 않는 것이 맞으므로 주석처리함.
정리
- 관리상의 목적으로 updated_at, created_at은 웬만하면 모든 테이블에 생성하기
- auto_now, auto_now_add가 해당 컬럼을 set 하는 것을 도와주는데,
- 이들은 django model 객체를 save() 하는 시점에 작동함.
- raw sql 실행, db console로 직접 접근할 때는 작동하지 않음.
- datetime 에는 naive, aware 2종류가 있음. 차이점은 시간대정보의 유무임.
- db에는 UTC로 저장하고, 서버가 db에서 가져올 때 자기 시간대에 맞춰서 변환하는 것이 정석임.
10. AbstractBaseClass
공통 필드를 AbstractBaseClass로부터 상속받기
- 우리가 정의한 model 클래스들은 updated_at, created_at 이라는 필드를 공통으로 가짐.
- 공식문서: https://docs.djangoproject.com/en/4.0/topics/db/models/#abstract-base-classes
- AbstractBaseClass: 모든 model에 공통적으로 들어가는 컬럼이 있을 때 유용하게 사용.
- tabom.models.base_model.py:
## tabom.models.base_model.py
from django.db import models
class BaseModel(models.Model):
updated_at = models.DateTimeField(auto_now=True)
created_at = modles.DateTimeField(auto_now_add=True)
class Meta:
abstract = True
- 다른 모든 model 클래스에서 상속을 받고, updated_at, created_at은 지워줌.
- 그리고 ./test.sh 실행함
Migration 실행해서 확인하기
- 내부 구조는 AbstractBaseClass 상속받도록 변경되었지만, 막상 table 컬럼은 변화가 없었음.
- 따라서 migration 해도 변화 없음.
11. 좋아요 service 구현
서비스 레이어 만들기
- 원래 django에선 필수가 아니지만, spring처럼 service 레이어를 만들어봄.
- servies/like_service.py, tests/test_like_service.py
## test/test_like_service.py
from django.test import TestCase
from tabom.models.article import Article
from tabom.models.user import User
from tabom.services.like_service import do_like
Class TestLikeService(TestCase):
def test_a_user_can_like_an_article(self) -> None:
# Given
user = User.objects.create(name="test")
article = Article.objects.create(title="test_title")
# When
like = do_like(user.id, article.id)
# Then
self.assertIsNotNone(like.id)
self.assertEqual(user.id, like.user_id)
self.assertEqual(article.id, like.article_id)
- like 함수가 없으므로 에러가 발생한다. 수정해보자.
like() 함수 작성
# 초반
from tabom.models.like import Like
def do_like(user_id: int, article_id: int) -> Like:
pass
# 수정
from tabom.models.like import Like
def do_like(user_id: int, article_id: int) -> Like:
return Like.objects.create(user_id=user_id, article_id=article_id)
- 테스트 실행하면 함수가 비어있으므로 당연히 실패함.
- 이것이 TDD(Test Driven Development)임. 테스트 주도 개발로, 소스코드 작성 전에 테스트부터 작성.
- 테스트가 딱 실행이 될 정도로만 코드 짜고 실패시킨다. 이 테스트가 성공하도록 함수 내부 채워주기.
- 테스트부터 작성하는 이유는 매번 성공하는 테스트 코드만 작성하지 않기 위해서.
- 수정한 다음에 테스트 실행. ./test.sh
Service 레이어에 관하여
- 강의에서 정의하는 service는 다음의 특징을 가짐
- view 함수가 service를 호출함.
- (web과 분리되어야 하므로) request를 인자로 받지 않음.
- request로부터 들어온 변수를 전달하고 싶다면 dto를 전달하거나, int인 id 자체를 전달함.
- http exception을 터뜨리지 않음.
- service가 터뜨리는 exception은 view 함수에서 받아서 다시 http exception으로 만듦
- 위의 규칙을 지키면 service 함수는 웹과 상관없는 순수한 함수가 됨.
- 여기에서의 순수한 함수는 역동성, 참조적 투명성 등이 아니라, 그저 웹 레이어와 상관 없다는 것.
- web client를 사용하지 않고도 service를 쉽게 테스트 할 수 있다는 장점이 있음.
- 추상화 계층이 하나임. web 계층과 비즈니스 로직계층이 분리됨으로써 가독성 향상됨. (SRP 원칙)
- 만약 서비스 레이어 없이 모든 로직을 view에서 처리하게 되면, 비즈니스 로직이 복잡해지면서,
- view 함수가 점점 두꺼워지고, 가독성도 떨어짐.
- 그렇지만 service 레이어와 함꼐라면 새로운 서비스 함수를 만들어서 호출만 해도 쉽게 해결 가능.
12. 유저는 하나의 게시글에 하나의 좋아요만 할 수 있다
테스트 작성: 하나의 게시글에는 하나의 좋아요만 가능하게 하기
# 요구사항 반영해서 테스트로 작성하기
def test_a_user_can_like_an_article_only_once(self) -> None:
# Given
user = User.objects.create(name="test")
article = Article.objects.create(title="test_title")
# Expect
like1 = do_like(user.id, article.id)
with self.assertRaises(Exception):
like2 = do_like(user.id, article.id)
- 우선 가장 넓은 범위의 에러인 Exception을 캐치하는 테스트 코드를 생성함.
- context manager: with 블록과 같이 쓰는 것
- Exception: 모든 exception의 상위 클래스
- with 블록 안에서 에러가 발생해야 테스트가 성공함.
unique_constraint
- 하나의 좋아요만 보장하기 > unique_constraint 방법을 써보자
- id는 unique index가 걸려있음.
- user_id 1, article_id 1인 좋아요 추가할 때 각각 있는지 확인하고 추가하게 됨. (이래서 속도 느려진대)
- 모든 컬럼이 동일할 때 문제인거지, 하나라도 다르다면 문제는 없음.
- 과거 django에서는 unique_together이라는 옵션으로 2개 이상 컬럼에 대한 unique index 생성 가능.
- 공식문서에 따르면 이제는 unique_together 보다 unique constraints 쓰라고 나와있음.
- constraints 옵션과 함께 사용하면 됨.
- 공식문서: https://docs.djangoproject.com/en/4.0/ref/models/constraints/#uniqueconstraint
- 공식문서: https://docs.djangoproject.com/en/4.0/ref/models/options/#constraints
class Like(BaseModel):
user = models.ForeignKey(User, on_delete=models.CASCADE)
article = models.ForeignKey(Article, on_delete=models.CASCADE)
class Meta:
constraints = [
models.UniqueConstraint(fields=["user", "article"], name="unique_user_article"),
]
- python manage.py makemigrations tabom
- python manage.py sqlmigrate tabom 0002
- python manage.py migrate
- 이제 테이블에서 확인하면 실제로 unique index 들어간 것을 확인할 수 있음.
테스트 실행, Narrowing Exception에 관해
- 테스트는 끝났지만 아직 끝난 것이 아님.
- assertRaises에서 Exception 클래스 사용했는데, 정말 이 Exception이
- unique key에 의해 발생한 것인지 확인 필요함.
- 기본적으로 Exception을 except(또는 catch) 하는 건 좋은 습관이 아님.
- 항상 우리가 의도한 구체적인 Exception만 잡아내는 것이 좋음. 너무 범위가 넒음.
# 예시 (1)
# input: integer, 만약 integer로 받을 수 없는 값이면 ValueError를 캐치함.
try:
my_int = int('a')
exception ValueError:
print('error catch!')
# 예시 (2)
# ValueError가 아니라 더 넓은 Exception을 캐치할 경우.
# 아래처럼 의도하지 않은 에러까지 잡게 됨.
# 따라서 Exception이 아니라 Exception의 sub class를 except 해야 함.
try:
my_int = int(1)
pa
except Exception:
print('error catch!') # error catch! 출력
테스트 수정
- 에러 종류 확인하기 위해 테스트 코드 수정함.
## 테스트 코드
def test_a_user_can_like_an_article_only_once(self) -> None:
# Given
user = User.objects.create(name="test")
article = Article.objects.create(title="test_title")
# Except
like1 = do_like(user.id, article.id)
try:
like2 = do_like(user.id, article.id)
except Exception as e:
print(e)
## e 타입
type(e)
- 위에서처럼 수정하고 디버깅함.
- e: IntegrityError. integrity: 에러, 무결성 에러
- unique Key가 달려있는데, 중복된 key가 있을 때,
- foreign key가 걸려있는데, null이 들어갔거나 할 때 발생함.
- 에러 내용을 보면 duplicate entry라고 적혀있고, unique key 이름도 우리가 만든 그대로 있음.
- 의도한대로 에러가 터지는구나!
def test_a_user_can_like_an_article_only(self) -> None:
# Given
user = User.objects.create(name="test")
aritcle = Article.objects.create(title="test_title")
# Exception
do_like(user.id, article.id)
with self.assertRaises(IntegrityError):
do_like(user.id, article.id)
- Exception이 아니라, IntegrityError를 사용해서 에러 감지하기.
13. 숨은 요구사항: 없는 user_id나 article_id가 input
input: 없는 user_id, article_id. 에러
- 실제로 현업에서는 기상천외한 예외들이 들어온다.
- 숨은 요구사항 test:
def test_it_should_raise_exception_when_like_an_user_does_not_exist(self) -> None:
# Given
invalid_user_id = 9998
article = Article.objects.create(title="title")
# Except
with self.assertRaises(IntegrityError):
do_like(invalid_user_id, article.id)
def test_it_should_raise_exception_when_like_an_article_does_not_exist(self) -. None:
# Given
user = User.objects.create(name="test")
article = 9998
# Except
with self.assertRaises(IntegrityError):
do_like(user.id, invalid_article_id)
정리
- 실패하는 테스트 먼저 짜고, 그 다음에 테스트가 성공하도록 구현하는 TDD 맛보기 했음.
- unique constraint 생성해봤다.
- 위에서는 IntegrityError를 raise 하는데, 이것은 나중에 view 할 때 다른 에러로 변경함.
14. 숙제
- django 쓰지 않고 mysql 서버에서 datetime의 default 값을 사용하는 방법.
- 장고 없이도 auto_now_add와 비슷한 설정을 mysql 서버에서 설정할 수 있다.
15. 2주차 숙제 해설
- DATETIME DEFAULT CURRENT_TIMESTAMP
'스파르타코딩클럽 > 강의 정리' 카테고리의 다른 글
[스파르타] Django 심화반 3주차 (완강) (0) | 2022.03.08 |
---|---|
[스파르타] Django 심화반 1주차 (완강) (0) | 2022.03.08 |
[스파르타] Django 기초반 2주차 (완강) (0) | 2022.01.20 |
[스파르타] Django 기초반 1주차 (완강) (0) | 2022.01.19 |
[스파르타] 실전 머신러닝 적용 4주차 (2) | 2022.01.11 |