이전 버젼까지는 mysql.user테이블의 레코드를 수정해서 바로 password를 변경할수가 있었는데요 8버젼부터는 그게 허용이 안됩니다. 아마도 password string을 좀더 복잡하게 하고 비밀번호 암호화 알고리즘을 보강해서 해킹에 대한 보안을 강화하기 위한 대책인것으로 보입니다.
일단 root로 mysql에 접속해서 root password를 비워줍니다.
UPDATE mysql.user SET authentication_string=null WHERE User='root';
이 상태에서 변경된 내용을 적용하고 mysql을 종료합니다.
FLUSH PRIVILEGES;
exit
다시 mysql로 들어갈때는 password를 입력하지 않아도됩니다.
mysql -u root -p
이제 새로운 password를 입력할 차례입니다. 아래 명령어를 이용해서 password를 입력해주세요.
ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY '비밀번호';
여기서 비밀번호에 root라던가 pass같이 쉬운걸 넣으면 Your password does not satisfy the current policy requirements라면서 거절해요. 대문자몇개 소문자 몇개 숫자랑 특수기호등 물론 길이도 보겠죠 password답게 만들어서 넣어야 변경해줍니다. 명령어가 문제 없이 실행이 되면 나갔다가 해당 password를 치고 다시 들어와 보는것도 잊지마세요. 그럼 다음에 더 유익한 정보로 또 뵐게요. 안녕.
이번시간에는 django라는 full-stack framework에 대해 간단하게 알아보겠습니다. 본 article은 reader의 컴퓨터에 python과 mysql이 정상적으로 연동되어 운영되고 있는 환경의 가정하고 씌여졌습니다.
Install
우선 django를 쓰려면 설치가 우선적으로 되어야하겠죠?
pip install Django
설치가 되었으면 버젼을 확인합니다. command not found에러가 나면 /usr/local/bin에 pip이 없는것이니 ln -s /usr/local/bin/pip3 /usr/local/bin/pip을 실행하여 링크를 만들어준다.
$ python -m django --version
3.2.15
이제 django-admin의 startproject옵션을 통해서 새로운 프로젝트를 하나 생성해볼게요. django-admin은 django framework을 손쉽게 관리할수 있도록 제공된 툴입니다.
startproject
django-admin startproject tutorial
위의 명령어를 실행하면 tutorial라는 폴더가 생성되고 그 안에 아래와 같은 폴더 구조가 생겼을 거에요.
혹시 위의 명령을 실행하는데 command not found에러가 났다면, /usr/local/bin폴더에 django-admin이 없는 것이니, ln -s /Library/Frameworks/Python.framework/Versions/3.7/bin/django-admin /usr/local/bin/django-admin를 실행하여 심볼릭 링크를 만들어 준 후에 다시 실행해보세요.
__init__.py는 해당 폴더를 package에 추가하기위해서 필수적으로 있어야하는 파일입니다. 이건 django 뿐만 아니라 python에서 정한 규칙이라서 해당 폴더안의 파일을 다른 파일에서 불러 쓰려면 꼭 있어야해요.
settings.py은 각종 설정값들을 저장하는 용도이고, urls.py은 서비스의 모든 route를 저장하고 있는 파일입니다. 그리고 asgi.py는 ASGI를 사용하기위한 entry-point이고, wsgi.py는 WSGI를 사용하기 위한 entry-point입니다. 얘네들은 나중에 접속자가 많아졌을때 성능을 좋게 해줄수 있는 기능을 제공하는 서비스들인데 설정이 조금 복잡하기도 하고해서 지금은 일단 넘어가고 나중에 깊이있게 다루도록 하겠습니다.
이제 서비스를 한번 띄워볼까요?
$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
January 10, 2022 - 03:37:06
Django version 3.2.11, using settings 'print.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
위의 명령을 실행하면 서비스가 뜨고, 프로그램에서 남기는 로그들을 보여줍니다. 서비스는 Ctrl+C를 쳐서 끝낼수 있습니다.
서비스가 잘 떴는지 http://127.0.0.1:8000/를 브라우저에 치고 들어가서 확인해볼까요? 성공적으로 실행이 되면 아래와 같은 페이지가 뜹니다.
아직 프로젝트 폴더에 디자인등 보여줄게 아무것도 없어도 django framework자체에서 이렇게 예쁜 화면을 보여준답니다.
startapp
이렇게 만들어진 framework에 투표를 할수 있는 poll 서비스를 만들어 붙여볼게요.
views.py를 열어서 아래와 같이 화면에 보여줄 메세지를 넣어주세요. 이 파일은 손님들이 poll에 접속했을때 보여줄 화면을 정하는 곳이랍니다. poll이라는 app에 접속하면 index는 보통 폴더path를 쳤을때 기본적으로 보여주는 파일명이자나요. 그런 의미에서 index라고 함수명을 적어주고 거기에서 “Hello, world. You’re at the polls index.”를 출력해서 보여줄게요. django에서 제공하는 HttpResponse라는 함수를 이용해서 rendering을 할거에요. HttpResponse를 아래와 같이 import한 후에 함수를 호출해주세요.
from django.http import HttpResponse
def index(request):
return HttpResponse("Hello, world. You're at the polls index.")
view파일에 index함수를 만들었다고 django가 자동으로 folder path를 쳤을때 위의 메세지를 보여주는건 아니구요. 아까 project를 생성할때 설명드렸던 route를 관리하는 urls.py파일 기억하시죠? 그 파일에 poll폴더의 path를 등록해야 해당 route가 서비스로서 기능을 해요. urls.py를 열어서 원래 생성되었던 admin이라는 route위에 polls라는 route를 추가할게요.
from django.contrib import admin
from django.urls import usls, path
urlpatterns = [
path('polls/', include('polls.urls')),
path('admin/', admin.site.urls),
]
여기서 제가 include라는 함수를 썼는데요. polls라는 app안에 route를 관리하는 또다른 urls.py를 두고 싶을때 그걸 여기에서 불러다가 추가시키는 거에요. 여기서 사용한 polls.urls는 사실 폴더를 package구조로 생각해서 변형한거라서 실은 polls/urls.py파일을 include시킨거랍니다. urls.py를 root에서 죄다 관리하는게 아니라 app별로 쪼개서 관리하면 훨씬 심플하고 읽기도 편리하겠죠? 근데 여기서 왜 admin.site.urls은 include함수 없이 그냥 갖다 쓴거죠? 하고 궁금하실텐데요. 그건 admin.site.urls는 django에서 얘만 유일하게 예외적으로 그냥 갖다 쓸수 있게 만들었어요. 특별대우인데 왜 특별대우를 받는지는 앞으로 차차 설명해드릴게요. 혹시 원래 코드와 차이점을 보셨는지 모르겠는데 include함수를 쓰기 위해서 from django.urls import include가 추가되었습니다. 다시 말씀드리지만 어떤 함수든 Python기본 함수가 아니면 패키지에서 import를 해주어야합니다.
polls/urls.py를 include시키려면 해당 파일을 만들어서 정한 곳에 갖다놔야겠죠? polls폴더 안에 urls.py를 생성하고 아래와 같이 urlpatterns이라는 이름의 배열을 하나 정의해주세요. 그리고 그 안에 views.index를 보여주도록 합니다.
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]
urlpatterns이라는 이름은 route를 나열해놓는 배열의 이름인데 이건 django에서 정한 예약어입니다. urls.py에 이 urlpatterns라는 배열이 정의되어있지 않으면 나중에 실행할때 형식에 맞지 않는다고 에러가나요. 이게 바로 framework을 사용하는 이유겠죠? 모든 개발자가 같은 변수명과 같은 규칙을 따르게 해서 나중에 어떤 개발자가 코드를 읽더라도 손쉽게 이해할수 있게 말이죠.
일단 route에 polls를 추가했으니까 view에서 정한대로 메세지가 나오는지 한번 서비스를 실행해볼까요?
python manage.py runserver
서비스가 문제없이 떴다면 http://localhost:8000/polls/를 열어보세요.
site domain에 접속하면 우선 root folder의 urls.py를 찾게 되고 거기서 include한 polls/urls.py을 호출하게 됩니다. polls/urls.py에서는 urlpatterns라는 배열에 path라는 함수를 이용해서 하나의 route을 추가했어요.
polls/urls.py에서 사용한 path라는 함수는 3개의 인자를 받습니다. 그 첫번째가 route인데요, 여기서는 empty string이죠? 그 말은 해당 폴더의 root를 의미합니다. 그리고 두번째 인자는 화면에 보여줄 view인데 값이 views.index라고 되어있는데, views.py파일안의 index함수를 호출하라는 의미입니다. 마지막으로 name이라는 인자가 있는데 이는 해당 route에 별칭을 두어 django framework어디에서나 해당 route를 이 별칭으로 불러다 쓸수 있는 유용한 기능이에요. route에 할당한 이름은 template에서 링크를 걸때 매우 편리하게 쓸수 있어요.
자 이제 Database를 연동해볼게요. 프로젝트생성시 만들어졌던 settings.py를 열어보세요. 그 안에 보시면 DATABASE라는 변수가 선언되어 있는데요.
django는 기본적으로 SQLite를 database로 사용합니다. 다른 database를 사용하고자 할때는 ENGINE의 값을 바꿔주시는데요. 현재는 ‘django.db.backends.sqlite3’로 되어있지만 ‘django.db.backends.postgresql’, ‘django.db.backends.mysql’이나 ‘django.db.backends.oracle’등 다른 엔진도 지원합니다. 그밖에 다른 engin은 여기서 확인하세요. 그리고 NAME에는 database의 이름을 적으면 되는데요. SQLite은 파일의 형태로 저장되기때문에 파일의 절대경로와 확장자를 포함한 파일명을 적어주었어요. MySQL을 사용한다면 DB명 이외에 접속에 필요한 USER나 PASSWORD등을 정의해주어야해요.
기왕에 settings.py를 열었으니까 잠시 서비스 timezone을 맞추고 갈게요. 현재는 UTC로 설정이 되어있는데 여러분이 서비스할 지역의 시간으로 해야 읽기 편리하겠죠. 한국은 KST로 하면 될거 같아요. Korea Standard Time. 저는 뉴욕시간으로 정보를 저장하도록 설정하겠습니다.
TIME_ZONE = 'EST'
그리고 settings.py파일 맨 위에 보시면 INSTALLED_APPS라는 변수가 있어요. 이게 현재 프로젝트에 사용될 django의 application들을 나열해 놓은건데요. 다른 application을 추가할수도 있고 사용하지 않는건 제거하셔도됩니다. 하지만 기본적으로 추가된 application들이 유용하게 쓰일수 있으니까 일단 이대로 갈게요.
위의 유틸중에 어떤 애들은 database table사용해서 관리하는 경우가 있어요. 그래서 위의 유틸들을 정의한 뒤에 database migration을 반드시 해주어야합니다. 그래야 정의된 유틸에 따라 필요한 table을 database에 생성을 하거든요.
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK
위의 migrate명령어를 실행하면 다음과 같이 유틸사용에 필요한 table들을 자동으로 생성합니다.
다시 말씀드리지만 해당 table들은 INSTALLED_APPS에 나열된 유틸기반으로 생성되기때문에 추가 table을 원하지 않으시는 경우에는 유틸사용을 하지 않으시면 됩니다.
migrate명령어를 실행했을때 table들이 잘 생성되었다면 데이타베이스 접속은 원활하게 잘되고 있다는 뜻입니다. 이제 데이타베이스에 데이타를 연동할 model을 만들어 볼게요. polls/model.py를 열어서 아래와 같이 코딩해주세요.
from django.db import models
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField('date published')
class Choice(models.Model):
question = models.ForeignKey(Question, on_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
위의 코드를 설명하자면요. 일단 django.db에서 제공하는 models를 import합니다. 왜냐면 각 테이블에 매칭되는 데이타들을 models.Model을 기반으로 만들거거든요. 무슨말이냐면 테이블의 칼럼들은 테이블마다 다양하지만 models.Model클래스를 엄마 클래스로 사용하여 models.Model가 제공하는 모든 기능을 사용하겠다는 거에요.
위의 코드에서 정의한 Question클래스를 보세요 옆에 괄호안에 models.Model를 넣어서 엄마클래스로 지정했지요. 그리고 추가로 2개의 클래스변수를 선언했습니다. 하나는 Char로 하나는 Datetime으로 정의했네요.
그리고 또 하나의 클래스 Choice도 보시면 마찬가지로 models.Model를 확장해서 만들겠다고 선언했어요. 그리고 3개의 멤버변수를 추가했습니다. Question과의 관계를 정의하는 ForeignKey하나와, 답변문장을 저장할 Char필드, 그리고 몇명이 선택했는지 저장할 votes라는 변수를 Integer로 선언했네요.
이제 이렇게 선언한 데이타베이스정보를 실제 데이타베이스에 적용할 차례에요. 방금전에 INSTALLED_APPS에 나열된 앱들을 사용하기 위해 migrate을 했는데 테이블들이 자동으로 생성되었자나요. 지금 우리가 만드는 polls라는 앱도 똑같이 취급하면 됩니다. INSTALLED_APPS에 앱을 등록하고 migrate을 실행하면 우리가 정의한 테이블이 자동으로 생성이 되는거에요. 그런데 여기서 우리가 만든 polls라는 앱을 application으로 인식하게 해주는 것이 바로 apps.py안에 정의된 PollsConfig클래스 입니다. 이 파일은 아까 우리가 startapp을 실행할때 자동으로 생성된건데 안에 보면 해당 앱을 django에서 applicatioin인식하도록 코딩이 이미 되어있어요.
from django.apps import AppConfig
class PollsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'polls'
이렇게 정의된 application을 INSTALLED_APPS에 아래와 같이 등록해주시면 됩니다. settings.py를 여시고 INSTALLED_APPS맨위에 PollsConfig를 full path와 함께 넣어줍니다.
이제 django가 polls라는 앱이 서비스에 추가되었다는 걸 알게 되었어요. 이제 테이블 migration을 해야겠죠? table을 생성하기에 앞서 model을 만들거에요. django가 작업하기 쉬운 형태로 table을 정의하는거죠.
$ python manage.py makemigrations polls
Migrations for 'polls':
polls/migrations/0001_initial.py
- Create model Question
- Create model Choice
위와 같이 명령어를 실행하면 polls/migrations/0001_initial.py파일이 생성되고 그 안에 CreateModel로 두개의 model이 정의되어 있습니다. 실제 database에 migration을 하기에 앞서 위에 생성된 0001파일을 이용해서 Create table 스크립트를 만들어서 한번 볼게요.
$ python manage.py sqlmigrate polls 0001
BEGIN;
--
-- Create model Question
--
CREATE TABLE "polls_question" (
"id" serial NOT NULL PRIMARY KEY,
"question_text" varchar(200) NOT NULL,
"pub_date" timestamp with time zone NOT NULL
);
--
-- Create model Choice
--
CREATE TABLE "polls_choice" (
"id" serial NOT NULL PRIMARY KEY,
"choice_text" varchar(200) NOT NULL,
"votes" integer NOT NULL,
"question_id" integer NOT NULL
);
ALTER TABLE "polls_choice"
ADD CONSTRAINT "polls_choice_question_id_c5b4b260_fk_polls_question_id"
FOREIGN KEY ("question_id")
REFERENCES "polls_question" ("id")
DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX "polls_choice_question_id_c5b4b260" ON "polls_choice" ("question_id");
COMMIT;
위와 같이 sqlmigrate을 이용하면 실제 table을 생성하지 않은 상태에서 create스크립트를 볼수 있어요. Create table스크립트를 확인하셨고 문제가 발견되지 않았다면 실제 database에 테이블을 생성할 차례입니다.
$ python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, polls, sessions
Running migrations:
Applying polls.0001_initial... OK
database에 들어가서 확인해보니 polls_choice와 polls_question이라는 테이블이 만들어 졌어요. migrate명령어는 실행할때마다 그동안 정의한 모든 스크립트를 다시 실행하는게 아니고요. 새로 만들어서 아직 실행되지 않은 파일만 실행해줍니다. 그러니까 테이블을 수정해야할 일이 있으면 그 전에 만들었던 Create table파일을 수정하시면 안되고요. 새롭게 Alter table스크립트를 만들어서 적용하셔야해요. 물론 Alter table스크립트도 model변경시 자동으로 만들어 지기때문에 편리하게 테이블을 관리할수 있어요.
이쯤에서 우리가 만든 model을 django API를 이용해서 살펴볼까요? django에서 제공하는 shell을 아래 명령어를 실행하여 열면 코드를 입력할수 있는 프롬프트가 뜹니다.
API shell
$ python manage.py shell
Python 3.7.4 (v3.7.4:e09359112e, Jul 8 2019, 14:54:52)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>>
여기에 우리가 방금 만든 model들을 import해볼게요
>>> from polls.models import Choice, Question
그리고 Question에 있는 모든 데이타를 가져옵니다
>>> Question.objects.all()
<QuerySet []>
아직 테이블 안에 아무것도 없으니까 비어있겠죠. 데이타를 넣어봅시다. 우리가 아까 선언한 Question model을 잠시 다시 살펴보면 pub_date라는 칼럼이 있어요. 여기에 시간데이타를 넣으려면 우리가 시간을 알아야겠죠. 현재 시간을 알아올수 있는 함수는 timezone에서 제공합니다. 일단 timezone을 import할게요.
>>> from django.utils import timezone
이 다음에는 Questions model클래스를 객체화해서 정의한 변수에 실제 값들을 넣습니다. 질문내용과 출간시간을 아래와 같이 넣을게요.
처음에는 비어있던 테이블이 이제는 Question이라는 model이 배열방에 들어가있네요. 근데 우리가 입력한 데이타는 안보여주고 Question: Question object (1)라고 보여주네요. 이렇게 부르면 우리가 입력했던 질문을 보여주면 좋겠죠? model.py를 열어서 Question과 Choice클래스에 __str__이라는 함수를 아래와 같이 추가해주세요. 기존에 변수는 그대로 놔주고 밑에 함수만 추가하는거에요.
from django.db import models
class Question(models.Model):
...
def __str__(self):
return self.question_text
class Choice(models.Model):
...
def __str__(self):
return self.choice_text
이렇게 하면 Object를 보여줄때 내용을 보여주게 되거든요. Shell을 다시 실행하셔야 변경된 내용이 적용되어 보여집니다.
결과 set은 당연히 비어있겠죠. 우리가 아무것도 할당하지 않았잖아요. 그럼 이제 질문객체의 choice_set에 선택지를 하나 생성해볼게요.
>>> q.choice_set.create(choice_text='Not much', votes=0)
<Choice: Not much>
생성후에는 결과로 객체를 보여줍니다. 아까 우리가 __str__에 정의한대로 선택지를 문자열로 보여주네요.
선택지가 하나만 있으면 안되니까 몇개 더 만들어 볼까요? 만들면서 생성된 Choice객체를 받아서 한번 살펴볼게요.
>>> q.choice_set.create(choice_text='The sky', votes=0)
<Choice: The sky>
>>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)
Choice model에 question이라는 멤버변수는 Question model을 ForeignKey로 가지고 있습니다.
>>> c.question
<Question: What's up?>
자, 그러면 지금까지 생성한 Choice들이 잘 들어갔는지 1번 Question객체의 choice_set을 한번 들여다볼까요?
>>> q.choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
Choice도 마찬가지로 filter함수를 이용하여 검색이 가능합니다. 아래는 올해 출간한 Choice들입니다.
>>> Choice.objects.filter(question__pub_date__year=current_year)
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
이번에는 Choice 문자열로 검색을 해볼까요?
>>> c = q.choice_set.filter(choice_text__startswith='Just hacking')
>>> <QuerySet [<Choice: Just hacking again>]>
가져온 Choice를 삭제해볼게요
>>> c.delete()
(1, {'polls.Choice': 1})
>>> q.choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>]>
삭제한 뒤에 Question객체의 choice_set을 보니까 두개밖에 남지 않았습니다.
createsuperuser
django shell을 exit()로 종료하시고, 이번에는 어드민계정을 생성해볼게요.
$ python manage.py createsuperuser
Username (leave blank to use 'slim'): admin
Email address: myemail@gmail.com
Password:
Password (again):
Superuser created successfully.
계정을 생성했으면 서버를 띄워볼게요.
$ python manage.py runserver
브라우저를 열고 http://127.0.0.1:8000/admin/를 열어보세요. 로그인 화면이 나오면 방금 입력한 id와 비번을 치고 들어가세요.
로그인 하시면 아래와 같은 관리자페이지가 보이실거에요. 안전하게 로그인 할수 있는 사용자를 등록할수 있고, 사용자에게 특정권한을 부여할수도 있습니다.
이제 지금까지 우리가 만든 Poll서비스를 여기 관리자페이지에서 관리할수 있도록 만들거에요. 그동안 Shell로 등록해야해서 불편했는데 GUI로 관리할수 있으니 너무 좋겠어요. 우선 admin에 서비스를 등록하려면 polls/admin.py파일을 생성해야해요. 파일에 django가 제공하는 admin클래스를 import해서 site에 Question을 아래와 같이 등록해주면 됩니다.
from django.contrib import admin
from .models import Question
admin.site.register(Question)
이 상태에서 http://127.0.0.1:8000/admin을 열어보세요.
Questions을 클릭하시면 아래와 같이 질문목록이 보이고 여기서 새 질문을 생성하기도하고 삭제도 하고 하면서 편리하게 관리하실수 있어요.
일단 여기서는 간단하게 admin 페이지에서 기본적인 관리가 된다는것만 보고 실제 view를 만들어 보도록할게요. MVC는 뭔지 다 아시죠? 간단하게 설명하자면 Model View Controller의 줄임말인데요. Model은 데이타베이스를 반영한 클래스이고 클래스안에 데이타 관련한 함수가 들어가기도 해요. 대부분 Model은 데이타베이스의 테이블과 똑같은 항목을 가지는데요 그게 항상 그렇지도 않아요. 운영하다가 DB 테이블은 안바꾸고 Model만 바꿔서 쓰기도 하거든요. 하지만 새로 구성하는 서비스라면 Model이랑 테이블이 같으면 덜 헷갈리고 좋죠. Model이 순수하게 저장되는 데이타를 표현한 것이라면, View는 화면에 보여주는 역할을 담당해요. 보통 HTML템플릿을 별도에 파일에 만들어서 View클래스에서 불러다가 가져온 데이타를 rendering하는 거죠. 그리고 얘네둘 중간에 Controller가 있습니다. Controller는 Model을 가져다가 데이타를 불러 담고 비지니스 로직에 맞춰 데이타를 지지고 볶고 하는 애에요. Framework에 따라서 View를 불러주기도 해요.
현재 우리가 만드는 poll서비스의 view파일에는 index함수 하나만있어요. urls.py에 정의된 대로 /polls/가 호출되었을때 보여주는 화면이죠. View파일에 아래 3개의 함수를 추가로 넣어볼게요.
def detail(request, question_id):
return HttpResponse("You're looking at question %s." % question_id)
def results(request, question_id):
response = "You're looking at the results of question %s."
return HttpResponse(response % question_id)
def vote(request, question_id):
return HttpResponse("You're voting on question %s." % question_id)
View가 추가되면 해당 뷰를 호출해줄 route를 polls/urls.py에 명시해주어야합니다.
코드위에 주석에서 보시다시피 브라우저에서 /polls/1을 호출하면 1번 Question을 화면에 보여주어야겠죠? 현재는 View에 정의된 대로 Question번호를 보여주고 있습니다. 이제 poll서비스를 열면 데이타를 가져와서 Question목록을 보여주고 그중 하나를 치고 들어가면 상세하게 보여주도록 한번 해볼게요.
우선 index에 들어간 메세지대신 질문 목록을 보여주도록 하겠습니다. 목록을 보여주기 위해서는 일단 목록디자인을 정의한 템플릿파일이 있어야되요. /polls/templates/polls/index.html이라는 파일을 생성해서 그 안에 템플릿을 아래와 같이 저장해 주세요.
{% if latest_question_list %}
<ul>
{% for question in latest_question_list %}
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}
위의 파일은 latest_question_list라는 배열변수를 받아서 목록으로 보여주고, 해당 아이템은 상세페이지로 가는 링크를 걸어줍니다. 만약 latest_question_list라는 배열이 비었다면 No polls are available.이라는 메세지를 보여주는 템플릿입니다.
그리고 위의 템플릿에서 사용할 변수값, latest_question_list을 views.py에서 정의해주어야합니다. polls/views.py에 있는 index함수를 아래와 같이 수정해주세요. 변경에 따른 라이브러리 import도 다 수정해주셔야합니다.
from django.http import HttpResponse
from django.template import loader
from .models import Question
def index(request):
latest_question_list = Question.objects.order_by('-pub_date')[:5]
template = loader.get_template('polls/index.html')
context = {
'latest_question_list': latest_question_list,
}
return HttpResponse(template.render(context, request))
이 파일에서는 polls의 index를 열었을때 Question데이타를 pub_date순으로 가져와서 우리가 위에서 생성한 템플릿 파일, index.html을 가져와서 rendering을 합니다. rendering을 할때 context라는 dict를 넘겨주게 되는데 여기서 key/value로 저장되는 값들이 템플릿에서는 변수명과 그 값으로 변환됩니다.
파일들을 저장 하셨다면 이제 브라우저를 열어서 http://127.0.0.1:8000/polls/를 확인해보세요.
우리가 view의 index에 명시한대로 모든 Question을 가져와서 템플릿에 디자인한 대로 ul/li태그에 보여줍니다.
404 Error
위의 화면에서 Question의 link를 클릭하면 상세화면으로 가는데요. 이때 해당 Question이 없다면 우리는 404 Error를 보여주어야 하겠습니다. 아래와 같이 views.py파일의 detail함수를 수정해 주세요.
from django.http import Http404, HttpResponse
from django.shortcuts import render
from django.template import loader
from .models import Question
...
def detail(request, question_id):
try:
question = Question.objects.get(pk=question_id)
except Question.DoesNotExist:
raise Http404("Question does not exist")
return render(request, 'polls/detail.html', {'question': question})
주어진 question_id로 Question을 조회해와서 detail.html라는 템플릿을 통해서 화면에 출력하고 있습니다. 특히 여기서는 render함수에 템플릿 경로와 사용될 데이타를 넘겨줌으로써 간편하게 rendering을 하고 있습니다. 이제 그러면 detail.html도 정의를 해야겠네요. /polls/templates/polls/detail.html이라는 파일을 생성하고 심플하게 그 안에서 해당 Question을 출력해줘볼게요.
{{ question }}
http://127.0.0.1:8000/polls/1/을 열어보면 이제 아래와 같이 Question의 text를 보여줍니다.
그리고 없는 Question을 열어보면, http://127.0.0.1:8000/polls/4/ 아래와 같이 404에러를 발생시킵니다.
위의 화면에서 보듯이 project root의 settings.py라는 파일안에 DEBUG변수의 설정값이 True로 되어있어서 개발자 모드로 에러에 대해 더욱 자세히 보여주고 있습니다.
get_object_or_404()
django에서는 404를 더욱 간단하게 보여주도록 제공하는 함수가 또 있습니다. views.py의 detail함수를 아래와 같이 수정해 주세요.
polls/templates/polls/index.html파일에 보면 각 Question들의 링크의 href이 /polls/{{ question.id }}/로 경로가 hardcoding되어있습니다. 이 코드는 {% url 'detail' question.id %}로 표현될수 있습니다. 이렇게 바꾸면 링크의 경로가 바뀌어도 자동으로 반영이 되어 보다 편리하게 이용하실수 있습니다.
위의 경로가 자동으로 반영되는지 테스트삼아 polls/urls.py에서 urlpatterns에 기존에 path('/', views.detail, name='detail'),라고 되어있던 부분을 path('specifics/<int:question_id>/', views.detail, name='detail'),로 잠시 변경해 볼게요. 그러면 detail페이지의 링크가 http://127.0.0.1:8000/polls/specifics/1/로 바뀌게 되는데 http://127.0.0.1:8000/polls/의 링크가 자동으로 변경되어 있는것을 보실수 있으실 겁니다.
Namespacing URL names
index.html에서 {% url 'detail' question.id %}부분을 보면 url이 detail이라고 되어 있는데, 하나의 프로젝트안에 여러개의 앱이 존재하는 경우 detail이라는 url은 여러개 존재할수 있습니다. 이때 각 app별로 영역을 구분짓는것이 바로 namespacing입니다. polls/urls.py파일에 app_name = 'polls'을 아래와 같이 추가해주세요.
이 함수에서는 해당 Question을 get_object_or_404함수로 가져와서 관련된 choice들을 get으로 가져와서 선택한 보기가 존재하는지 확인하고 만약 선택지가 없는 경우 에러메세지를 보여줍니다. 투표를 하고자하는 Question과 선택지가 존재한다면 기존 투표수에 1을 더해서 저장한뒤 “polls:results”화면으로 redirect시킵니다.
여기서 자세히 보시면 request.POST[‘choice’]라는 코드가 있는데 짐작하시겠지만 django는 request의 POST에 form정보를 저장해서 vote함수를 호출해줍니다. 마찬가지로 form이나 URL에 GET으로 받은 데이타는 request.GET에 저장이 됩니다.
그리고 HttpResponseRedirect라는 함수를 가져다 썼는데요, 이 함수는 특정경로로 redirection시켜주는 기능을 합니다. HttpResponseRedirect안에 보면 reverse라는 함수를 함께 사용했는데요, 이 함수는 논리적 경로를 실제 경로(/polls/3/results/)로 변경해주는 역할을 합니다. 위에서도 말씀드렸다시피 실제 경로를 사용하게 되면 나중에 경로가 변경되었을때 경로를 참조한 모든 곳을 수정해주어야하기 때문에 가급적 하드코딩대신 이렇게 namespace와 view의 함수명으로 명시해줌으로써 좀더 flexible한 코딩이되는거에요.
그러면 이제 redirect시키는 polls:results로 이동해볼까요? 이 경로는 urls에 아래와 같이 정의되어있습니다.
이제 결과를 확인해볼까요? http://127.0.0.1:8000/polls/에는 설문지 목록이 보입니다. 그중 하나를 클릭하고 들어가면 http://127.0.0.1:8000/polls/1/ 선택할수 있는 폼이 보입니다. 선택을 하고 Vote버튼을 누르면 http://127.0.0.1:8000/polls/1/results/ 결과가 보입니다. Vote again?링크를 눌러서 다시 시도해봅니다. 이번에는 아무것도 선택하지 않고 Vote버튼을 눌러볼까요? You didn't select a choice.라는 메세지가 잘 뜨네요.
그런데 여기서 한가지 문제가 있습니다. 사용자가 투표를 하면 기존 투표수를 가져와서 +1을 하는 방식은 자칫 잘못된 결과를 초래할수 있습니다. 두명의 사용자가 동시에 투표를 한경우 기존 투표수가 동일해서 둘다 +1을 눌렀지만 기존 투표수 + 1은 같은 값을 가지게되는거죠. 이런 상황을 race condition이라고 하는데 이런 상황을 처리하는 방법을 여기를 눌러서 확인해주세요.
Use generic views: Less code is better
코드는 언제나 짧으면 짧을수록 좋습니다. 우리는 지금까지 polls앱에 대한 기능을 장황하게 코딩을 했습니다. 그런데 사실 지금 우리가 만든 기능은 거의 대부분의 앱에서 필요로 하는 기본적인 기능들이었습니다. Model에서 각 Model들간의 정보와 그들간의 관계만 정확하게 설정해준다면 이하 모든 기능들은 자동으로 처리해주는 django의 generic views를 살펴보도록하시죠.
자동으로 처리해주는 generic view를 사용하기 위해서는 우선 urls.py를 아래와 같이 수정합니다. index와 detail 그리고 results를 generic view로 하겠다고 변경하는 겁니다. 여기서 주의하실점은 기존에 상세페이지로 넘겨줄때 사용했던 변수 question_id는 pk로 변경해 주셔야합니다.
그리고 위의 urls.py에서 불러주었던 views.py에 정의했던 3개의 함수를 Django에서 제공하는 View Class로 변경하여 선언합니다. 우선 generic모듈을 import하고, generic.ListView를 확장하여 urls.py에 명시한대로 클래스를 아래와 같이 선언합니다. 선언한 뒤에 model에 Question이라고 값을 주고, template_name에 각 템플릿 경로를 저장합니다.
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.template import loader
from django.views import generic
from .models import Choice, Question
class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'
def get_queryset(self):
"""Return the last five published questions."""
return Question.objects.order_by('-pub_date')[:5]
class DetailView(generic.DetailView):
model = Question
template_name = 'polls/detail.html'
class ResultsView(generic.DetailView):
model = Question
template_name = 'polls/results.html'
def vote(request, question_id):
...
// 이하 코드는 원래 있던대로 놔주세요
만든 코드 Test하기
polls/tests.py파일을 아래와 같이 수정합니다.
import datetime
from django.test import TestCase
from django.utils import timezone
from .models import Question
class QuestionModelTests(TestCase):
def test_was_published_recently_with_future_question(self):
"""
was_published_recently() returns False for questions whose pub_date
is in the future.
"""
time = timezone.now() + datetime.timedelta(days=30)
future_question = Question(pub_date=time)
self.assertIs(future_question.was_published_recently(), False)
위의 테스트파일은 TestCase를 확장하여 정의한 테스트클래스에 정의된 함수는 질문을 생성할때 만든날짜를 미래의 날짜로 생성하려고 할때 어떻게 되는지 보는 함수입니다.
Python프로그램을 하다보면 프로젝트마다 각기 다른 환경을 구축해야할 때가 있습니다. 이때 유용한 툴이 바로 venv, virtual environment입니다. Python은 기본적으로 virtual environment를 제공합니다. virtual environment를 생성하기 위해서 아래와 같은 명령을 실행해주세요.
python2 -m venv ./venv
python3 -m venv ./venv
virtual환경의 기본 python을 version 2로 만들고 싶다면 첫번째 줄에서 보시는 것처럼 python2로 venv를 생성해주시고, python3를 기본으로 만들고 싶다면 두번째 줄에서 보시는것처럼 python3로 명령을 실행해주세요. 명령어 맨 마지막에 ./venv는 virtual환경을 저장하고자 하는 폴더위치입니다. 저는 현재 폴더에 venv라는 폴더를 생성하고 그곳에 virtual환경관련데이타를 저장하도록 했어요.
그럼 이제 생성한 virtual환경 속으로 들어가볼까요?
$ source ./venv/bin/activate
(venv) $
위의 명령어를 치면 두번째 줄에서 보시듯이 (venv)라는 폴더명이 프롬프트에 뜹니다. 이것으로 내가 현재 virtual환경에 들어와 있는지 아닌지를 알수가 있지요.
그러면 이번에는 작업을 다 마치고 virtual환경을 나가는 명령어를 실행해 볼까요?
(venv) $ deactivate
$
deactivate이라는 명령어를 치면 virtual환경을 빠져나오고, 프롬프트에도 더이상 (venv)라는 표시가 나타나지 않게 됩니다. 각종 라이브러리를 설치할때도 프로젝트별로 venv를 만들어서 작업을 하면 버젼에 대한 충돌없이 관리가 잘되겠죠? 도움이 되셨길 바랍니다. 그럼 오늘도 좋은 하루 되세요.
MySQL server를 띄운상태에서 mysql_secure_installation라는 명령어를 실행할건데요. 이 명령어는 MySQL의 보안을 강화하는 명령어로써 다양한 옵션을 제공합니다. 여기서는 기본옵션으로 간단하게 실행만 할거에요. mysql_secure_installation에 대한 자세한 사항은 여기를 클릭해서 알아볼수 있습니다.
$ mysql_secure_installation
Securing the MySQL server deployment.
Enter password for user root:
Connecting to MySQL using a blank password.
VALIDATE PASSWORD COMPONENT can be used to test passwords
and improve security. It checks the strength of password
and allows the users to set only those passwords which are
secure enough. Would you like to setup VALIDATE PASSWORD component?
Press y|Y for Yes, any other key for No: y
There are three levels of password validation policy:
LOW Length >= 8
MEDIUM Length >= 8, numeric, mixed case, and special characters
STRONG Length >= 8, numeric, mixed case, special characters and dictionary file
Please enter 0 = LOW, 1 = MEDIUM and 2 = STRONG: 0
Please set the password for root here.
New password:
Re-enter new password:
Estimated strength of the password: 50
Do you wish to continue with the password provided?(Press y|Y for Yes, any other key for No) : y
By default, a MySQL installation has an anonymous user,
allowing anyone to log into MySQL without having to have
a user account created for them. This is intended only for
testing, and to make the installation go a bit smoother.
You should remove them before moving into a production
environment.
Remove anonymous users? (Press y|Y for Yes, any other key for No) : y
Success.
Normally, root should only be allowed to connect from
'localhost'. This ensures that someone cannot guess at
the root password from the network.
Disallow root login remotely? (Press y|Y for Yes, any other key for No) : y
Success.
By default, MySQL comes with a database named 'test' that
anyone can access. This is also intended only for testing,
and should be removed before moving into a production
environment.
Remove test database and access to it? (Press y|Y for Yes, any other key for No) : y
- Dropping test database...
Success.
- Removing privileges on test database...
Success.
Reloading the privilege tables will ensure that all changes
made so far will take effect immediately.
Reload privilege tables now? (Press y|Y for Yes, any other key for No) : y
Success.
All done!
보안을 강화한 설정을 서버에 적용하기 위해서는 서버를 다시 실행해야합니다. 일단 brew service를 이용해서 MySQL을 종료합니다.
$ brew services stop mysql
Stopping `mysql`... (might take a while)
==> Successfully stopped `mysql` (label: homebrew.mxcl.mysql)
brew services를 이용해서 서버를 띄우면 MySQL서버가 daemon mode로 뜨게 되는데요. 프로그램이 daemon mode로 떠있으면 컴퓨터가 재시작할때마다 자동으로 background에 MySQL을 띄워주는 거에요. 그런데 저는 컴퓨터가 꺼지면 MySQL도 꺼지고 제가 수동으로 띄우기 전까지는 자동으로 뜨지 않았으면 좋겠어요. 그래서 이번에는 mysql.server를 이용해서 띄워볼게요.
$ mysql.server start
Starting MySQL
. SUCCESS!
mysql.server를 이용해서 띄우면 컴퓨터가 꺼지거나 mysql.server stop을 실행할때까지 MySQL서버가 계속 실행되게 됩니다.
$ mysql.server stop
Shutting down MySQL
. SUCCESS!
다시 mysql.server start를 실행한뒤, 이번에는 MySQL에 접속을 해볼게요.
$ mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.27 Homebrew
Copyright (c) 2000, 2021, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
이번시간에는 이렇게 Local환경에 MySQL서버를 띄우는 방법을 배워보았습니다. 도움이 되셨기를 바래요. 바이!
$ brew -v
Homebrew 3.3.9
Homebrew/homebrew-core (git revision 8eab795ad1d; last commit 2022-01-06)
Homebrew/homebrew-cask (git revision 1a7ec319ea; last commit 2022-01-06)
최신버젼이 아닌경우에는 brew update 명령어를 이용하여 최신버젼으로 업데이트를 해주세요.
$ brew update
Updated 2 taps (homebrew/core and homebrew/cask).
==> Updated Formulae
...
You have 5 outdated formulae installed.
You can upgrade them with brew upgrade
or list them with brew outdated.
특정 툴을 설치하시고 싶을 때는 brew install 명령어를 이용하시면 됩니다. 예를 들어 wget를 설치하시고 싶을때는 아래와 같이 brew install wget이라고 치시면 설치가 됩니다.
$ brew install wget
==> Downloading https://ghcr.io/v2/homebrew/core/gettext/manifests/0.21
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/gettext/blobs/sha256:0e93b5264879cd5ece6efb644fd6320b0b96cce36de3901c1926e5
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:0e93b5264879cd5ece6efb644fd6320b0b96cce
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libunistring/manifests/1.0
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libunistring/blobs/sha256:18a1691229db1dbc9c716236df52f447aa9949121c36ae65b
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:18a1691229db1dbc9c716236df52f447aa99491
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libidn2/manifests/2.3.2
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/libidn2/blobs/sha256:29b1ea810ddad662b0c766429c2384495d643baa253dc31eed0300
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:29b1ea810ddad662b0c766429c2384495d643ba
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/wget/manifests/1.21.2
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/wget/blobs/sha256:b6d6f422e3c4db0607caf5fc91dba4fb19b3c52883d7a012c9fc11b87
==> Downloading from https://pkg-containers.githubusercontent.com/ghcr1/blobs/sha256:b6d6f422e3c4db0607caf5fc91dba4fb19b3c52
######################################################################## 100.0%
==> Installing dependencies for wget: gettext, libunistring and libidn2
==> Installing wget dependency: gettext
==> Pouring gettext--0.21.monterey.bottle.tar.gz
🍺 /usr/local/Cellar/gettext/0.21: 1,953 files, 20.2MB
==> Installing wget dependency: libunistring
==> Pouring libunistring--1.0.monterey.bottle.tar.gz
🍺 /usr/local/Cellar/libunistring/1.0: 56 files, 5MB
==> Installing wget dependency: libidn2
==> Pouring libidn2--2.3.2.monterey.bottle.tar.gz
🍺 /usr/local/Cellar/libidn2/2.3.2: 77 files, 846.7KB
==> Installing wget
==> Pouring wget--1.21.2.monterey.bottle.tar.gz
🍺 /usr/local/Cellar/wget/1.21.2: 89 files, 4.2MB
==> `brew cleanup` has not been run in the last 30 days, running now...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
Removing: /usr/local/Cellar/gettext/0.20.1... (1,899 files, 18.5MB)
Error: Permission denied @ apply2files - /usr/local/lib/docker/cli-plugins
설치된 툴들을 보고 싶을때는 brew list를 이용합니다.
$ brew list
==> Formulae
ca-certificates inetutils libunistring openssl@1.1 wget
gettext libevent lz4 pcre2 zstd
git libidn mysql protobuf
icu4c libidn2 mysql-search-replace six
설치한 툴을 삭제하고 싶으면 brew uninstall을 이용합니다.
$ brew uninstall wget
Uninstalling /usr/local/Cellar/wget/1.21.2... (89 files, 4.2MB)
Warning: The following may be wget configuration files and have not been removed!
If desired, remove them manually with `rm -rf`:
/usr/local/etc/wgetrc
안녕하세요 오늘은 jsonschema를 통해서 사용자가 입력한 JSON데이타에 어떤 오류가 있는지를 확인하는 방법을 알려드릴게요.
초간단 validation
일단 우리가 사용할 모듈은 jsonschema입니다. import하시구요
import jsonschema
그 다음에 하셔야하는 일이 바로 schema를 정의하는 건데요. 여기서는 군더더기 다 빼고 그냥 기능만 아주 간단하게 알려드릴게요. 우선 여러분들이 입력받고자하는 사용자 입력데이타가 어떤건지 정의를 합니다. 저는 여기에서 우선 정수값의 ID를 받을 거구요, 이름은 문자열로 받을거에요.
이렇게 schema를 정의하셨으면 사용자 데이타를 가지고 한번 validate을 해볼게요. 이 사용자는 개발자가 시키는대로 id는 정수, 이름은 문자열로 잘 입력을 해주었어요.
data = {
"id": 1,
"name": "ellie"
}
그러면 이 사용자가 입력한 data가 위에 정의한 schema를 통과하는지 실행해봅니다.
jsonschema.validate(instance=data, schema=schema)
아무 문제가 없으면 이 함수는 아무것도 안하고 그냥 넘어갑니다. 그런데 만약에 어떤 사용자가 엉뚱하게도 id에 문자열을 넣고 name에 정수값을 넣었다고 해봅시다.
data = {
"id": "ellie",
"name": 1
}
jsonschema.validate(instance=data, schema=schema)
그러면 이때는 jsonschema.validate함수가 ValidationError 예외를 throw합니다.
Traceback (most recent call last):
File "/tmp/main.py", line 27, in <module>
jsonschema.validate(instance=data, schema=schema)
File "/usr/local/lib/python3.9/site-packages/jsonschema/validators.py", line 934, in validate
raise error
jsonschema.exceptions.ValidationError: 'ellie' is not of type 'integer'
Failed validating 'type' in schema['properties']['id']:
{'type': 'integer'}
On instance['id']:
'ellie'
그러면 사용자가 어떤 데이타를 넣을지 모르니까 jsonschema.validate() 를 호출할때는 반드시 try except로 묶어서 ValidationError exception이 발생했는지를 점검해 주어야겠죠?
Validation Error 모으기
위의 jsonschema.validate() 함수는 schema의 정의를 하나씩 살피면서 문제가 되는 데이터값을 발견했을때 바로 ValidationError를 throw합니다. 그런데 때로는 그때그때 하나씩 error를 고치기보다는 해당 입력 데이타에 어떤 어떤 에러가 있는지 한번에 확인하고 한번에 사용자에게 알려주거나 아니면 모아다가 다른 식으로 에러를 처리하고 싶을때가 있자나요. 그럴때는 jsonschema에서 제공하는 Draft7Validator라는 함수를 사용합니다.
예를 들어 저는 사용자입력 데이타를 싹 한번 훑은 다음에 문제가 되는 항목의 field명만 모아서 배열에 담고 싶어요. 그러면 아래와 같이 Draft7Validator를 호출해서 받은 validator로 data를 iter_errors의 인자로 넘겨주면 error를 전부다 한번에 받아올수가 있어요.
data = {
"id": "ellie",
"name": 1
}
validator = jsonschema.Draft7Validator(schema)
errors = validator.iter_errors(data)
invalid_fields = []
for error in errors:
invalid_fields.append(error.path[0])
errors에는 그러면 ValidationError가 배열로 담여있을거구요. 그 안에서 저는 field명만 빼서 invalid_fields에 담아줍니다. 그리고 나서 결과를 출력해보면 id와 name이 배열안에 담김니다.
print(invalid_fields)
['id', 'name']
invalid field 제거하기
이번에는 데이타를 받아서 확인하고 valid하지 않은 field들은 원본데이타에서 삭제해 볼게요. 마찬가지로 Draft7Validator를 이용할거에요.
def validate_schema(schema, data):
validator = jsonschema.Draft7Validator(schema)
errors = validator.iter_errors(data)
for error in errors:
remove_field(list(error.path), data)
아까 처럼 iter_errors를 호출해서 errors를 받아오는 부분까지는 동일해요. 그런데 이번에는 해당 path의 field를 data에서 제거하도록 하는 함수를 만들어서 아예 조건에 맞지 않는 데이타는 안받은척 하는거죠. remove_field에서는 path를 받아서 data와 함께 재귀적으로 호출을 하는데요. 이때 path의 가장 끝에 field명까지 갔다가 삭제하고 돌아오는 로직을 아래와 같이 구현합니다. 그래야 data의 depth가 여러개 일때 끝까지 찾아들어가서 지우고 오겠죠?
def remove_field(path, data):
field = path.pop(0)
if len(path) > 0:
remove_field(path, data[field])
if not data[field]:
data.pop(field)
else:
data.pop(field)
그럼 실행을 한번 해볼까요? schema를 multiple depth로 테스트 하기 위해서 name을 first와 last두개로 나눠서 정의할게요
결과가 잘 나오는거 같네요. 여러분들도 한번 여러분들만의 코드를 만들어서 한번 실행해보세요. 이런것들이 알고나면 참 간단한데 배우기 전에는 헷갈리는 것들이죠. 배우지 않고도 자유롭게 코딩을 할수 있는 그 날이 올때까지 다같이 열심히 배워봅시다. 오늘도 여기까지 따라오느라 수고하셨습니다. 좋은 하루 되세요.
[Additional]
Validate Array
The validation result is a bit different when you have invalid data in an array.
와우!! 엑셀에서 이런게 되는지 몰랐어요. 기능이 좀 억지스럽긴 하지만 프로그래밍을 못하는 엑셀사용자에게는 어쩌면 이런 기능이 구세주가 되는 상황이 있을수도 있을거 같다는 생각이 들어서 여러분들께 소개해드리려고 가지고 와봤어요.
제가 오늘 소개해드리고자 하는 기능은 Array Formular라는 엑셀의 기능인데요, 랩탑에 엑셀이 없으신 분들이 많이 계실것 같아서 설명은 Google sheet으로 해드릴게요. 대신 엑셀에서는 연산을 하고자하는 해당셀에 대고 Control + Shift + Enter를 눌러서 Array formular를 하겠다고 명시하지만, Google sheet에서는 ARRAYFORMULA 라는 함수로 전체 formular를 한번 감싸므로써 해당 연산이 Array formular라는것을 명시합니다. 자세한것은 밑에서 설명드릴게요.
우선 Array Formular가 뭔지 모르시는 분들을 위해서 간단하게 설명을 드릴게요. 엑셀에서 함수를 호출하면 보통 반환되는 값이 하나의 정수이거나 아니면 Boolean값, 또는 문자열이 되는데요. Array Formular로 함수를 호출하면 함수가 단답형의 결과가 아닌 배열의 형태로 결과를 반환하게 되는데요, 그걸 응용해서 다양한 기능을 구현 할수가 있는거죠.
무작정 따라하기
예를 들어 아래와 같이 두개의 칼럼으로 구성된 원본 데이타가 있다고 합시다.
하나의 칼럼에는 그룹명이 적혀있고, 그룹 옆에는 사람이름이 나열되어있어요. 이걸 그룹별로 사람들을 묶어서 보여주고자 할때 Array Formular를 사용하는데요. 방법은 다음과 같습니다.
일단 위의 데이타를 Google sheet에 입력해주세요. 그리고 데이타구간을 변수로 선언하는데요. C5부터 C11는 names라는 변수로, B5부터 B11까지는 groups라는 변수로 선언해주세요. Google sheet에서 변수를 선언하는 방법은 상단메뉴 Data > Named ranges를 클릭하셔서 변수명과 해당 변수명에 할당할 data range를 아래와 같이 지정해주세요.
이 formular에서 가장 눈여겨 보셔야할 포인트는 바로 SMALL함수 입니다. SMALL함수는요 원래 주어진 데이타 range안에서 가장 작은 값부터 순서를 매겨서 지정한 순위의 값을 가져오도록 하는 함수인데요. 그래서 인자로는 data range와 작은순서대로 몇번째 데이타를 가져올지 nth라는 변수를 인자로 받습니다. 그런데 여기서는 값이 아니라 라인번호를 넘겨주고 있기때문에 값을 가져올 행번호를 리턴하게 되는데 그걸 INDEX함수가 받아서 열은 고정이니까 행번호만 알면 INDEX가 데이타를 딱 가져올수 있겠죠.
여기서 헷갈리는 부분이 바로 SMALL이 배열을 반환한다는 사실인데요. 그 배열은 IF문에 의해서 동적으로 생성이 됩니다.
IF(groups=E$4,ROW(names)-MIN(ROW(names))+1)
위의 연산자는 groups이라는 데이타 range에서 값이 E4와 같으면 (E4는 Fox로 고정된 값이죠), E4와 같으면, 그 앞의 연산자(아래)를 통해서 생성된 데이타 range에서 행번호를 가져오게 됩니다.
ROW(names)-MIN(ROW(names))+1
IF문 안에서 가장 먼저 나오는 ROW(names)는 행번호를 배열로 반환합니다.
{5, 6, 7, 8, 9, 10, 11}
이거 우리가 formular 맨 처음에 ARRAYFORMULA로 처리하겠다고 명시했기때문에 이렇게 배열을 반환하는 거지 만약에 그냥 ROW(names)만 넣으셨으면 정수 5를 반환합니다.
그리고 MIN(ROW(names))는 그 안에서 가장 작은 방번호 5를 반환합니다.
그러면 ROW(names)-MIN(ROW(names))를 연산하면 아래와 같이 각 방에서 5를 뺀값이 됩니다. 이것도 마찬가지로 ARRAYFORMULA를 명시하지 않았다면 결과가 정수 0이었을거에요. 하지만 ARRAYFORMULA로 처리하라고 명시 했기때문에 배열방의 값들에서 5씩 뺀 결과 배열을 반환 받습니다.
{0, 1, 2, 3, 4, 5, 6}
그런데 지금 우리가 INDEX에서 접근할때 인자로 range랑 행번호, 열번호를 넘길때 첫번째 방번호는 1이되야한단 말이에요. 그래서 나중에 계산하기 쉽게 방번호에 1을 더해줍니다.
ROW(names)-MIN(ROW(names))+1
그러면 배열에 1이 각각 더해져서 아래와 같은 배열이 되요
{1, 2, 3, 4, 5, 6, 7}
위의 배열을 가지고 나오면 바로 다음 IF문이 기다리고 있죠? IF문은 배열에 조건을 겁니다. 보통 IF문이라면 조건, TRUE일때 값, FALSE일때 값 이런식으로 움직이겠지만 지금 우리는 이 formular를 Array formular로 한다고 명시했기때문에 IF문도 다르게 동작합니다. 이거 맨 마지막에 ARRAYFORMULA함수 없으면 오류나요. 왜냐면 보통 IF문은 함수의 인자를 3개 넘겨줘야하거든요. 함수의 인자를 2개 넘겨주는건 오직 ARRAYFORMULA일때만 가능합니다.
IF(groups=E$4,ROW(names)-MIN(ROW(names))+1)
첫번째 인자로 groups데이타와 비교할 값 E4가 조건문으로 들어갔어요. E4의 값은 Fox죠? groups데이타를 잠깐 보고 갈까요?
{Fox, Bear, Bear, Bear, Moose, Fox, Moose}
IF문이 위의 배열에서 “Fox인것만 True로 반환해라”라고 한거에요 그러면 조건문을 지나면 아래와 같은 결과가 나올거에요.
{True, False, False, False, False, True, False}
위의 조건을 방금전에 ROW(names)-MIN(ROW(names))+1을 통해서 만들어낸 배열과 연산해볼게요
ROWS($E$5:E5)를 잠시 보시면요, range의 앞쪽은 고정이고, 복사를 해서 늘릴수록 뒷쪽은 해당 셀로 변경이 된다는 거죠? 다시말해서 현재 셀이 E5인데 이걸 복사해서 바로 아래칸 E6에 붙여넣게 되면 ROWS($E$5:E6) 시작점은 여전히 E5이지만 끝나는 점은 E6인 range가 되는거죠? 그리고 만약에 이번에는 ROWS($E$5:E5)를 오른쪽에 갖다 붙였다고 쳐보세요. 그러면 이번에는 ROWS($E$5:F5)와 같이 시작점은 그대로고, 행번호도 그대로 이지만 열이 하나 늘어나게 되는거죠? 붙여 넣을때마다 해당 셀로 range가 조정이 되는거에요. 그러면 range가 조정됨에 따라서 ROWS가 반환하는 값은 어떻게 바뀔까요? 처음에는 range안에 cell하나였는데, 아래로 한칸 늘어날때마다 행번호가 늘어나겠죠? 이해를 돕기위해서 제가 아래와 같이 ROWS($E$5:E5)만 따로 떼서 아래로, 그래고 옆으로 복사해봤어요. 결과가 왜 이렇게 나오는지 이해가 가시나요? ROWS는 인자로 넘겨준 range가 몇줄짜리 데이타인지를 반환하기 때문에 아래와 같은 결과가 나옵니다.
INDEX함수의 첫번째 인자는 데이타를 가져올 range이고요, 두번째는 행번호, 그리고 세번째 인자는 열번호 입니다. 보통 data range의 범위가 넓을때 아래와 같이 행과 열을 지정해서 값을 가져옵니다. 이때 행, 열은 절대값이 아니고, 해당 range에서의 상대적인 행,열번호를 의미합니다.
INDEX(B5:E13, 5, 3)
row 5, column 3
그런데 우리는 현재 range의 열이 1개밖에 없기때문에 행번호만 지정해주면 됩니다. SMALL에서 받아온 값이 첫번째 셀에서는 1, 두번째 셀에서는 6이었으니까 한번 대체해 볼게요.
INDEX(names,1)
INDEX(names,6)
그러면 첫번째 행에는 names의 첫번째 이름인 Doug, 그리고 두번째 행에는 names의 6번째 이름인 Cindy가 들어가겠죠. 그러면 이렇게 결과가 나오죠.
여기까지만 하고 나가면 에러가 난 셀들이 안이뿌니까 에러가 났다면 그냥 공백처리를 하도록 IFERROR로 감싸줄게요.
에러가 안보이니까 한결 좋으네요. 오늘배운 Fomular를 정리해서 보면 아래와 같습니다.
Python에서 Pickling을 한다는건 어떤 Object를 serializing해서 어딘가에 저장하고, 그걸 다시 꺼내서 de-serializing하는 것을 pickling이라고 합니다. 어떤 오브젝트를 시리얼라이징함으로 인해서 파일에 저장할수 있게 하는거죠. 파이썬에 있는 어떤 Object든지 피클링을할수 있어요. 다시말해 피클링은 파이썬 오브젝트들 예를 들면 list, dict같은 애들을 문자열로 만들어 주는 작업을 말합니다. 이 작업은 문자열로 만들었던 정보가 다시 오브젝트로 재건되는데 필요한 모든 정보를 해당 문자열 안에 가지고 있어야합니다. 아래는 어떤 오브젝트를 피클링해서 저장하고, 다시 가져와서 오브젝트로 만들어 주는 스크립트입니다.
import pickle
def storeData():
# initializing data to be stored in db
Omkar = {'key' : 'Omkar', 'name' : 'Omkar Pathak',
'age' : 21, 'pay' : 40000}
Jagdish = {'key' : 'Jagdish', 'name' : 'Jagdish Pathak',
'age' : 50, 'pay' : 50000}
# database
db = {}
db['Omkar'] = Omkar
db['Jagdish'] = Jagdish
# Its important to use binary mode
dbfile = open('examplePickle', 'ab')
# source, destination
# print(pickle.dumps(db))
pickle.dump(db, dbfile)
dbfile.close()
def loadData():
# for reading also binary mode is important
dbfile = open('examplePickle', 'rb')
# print(pickle.loads(dbfile))
db = pickle.load(dbfile)
for keys in db:
print(keys, '=>', db[keys])
dbfile.close()
if __name__ == '__main__':
storeData()
loadData()
위의 코드를 실행하면 아래와 같이 문자열로 저장되어있던 내용이 다시 오브젝트로 로딩이 되어서 보여집니다.