Change root password on MySQL 8

이전 버젼까지는 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 framework 시작하기

이번시간에는 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-adminstartproject옵션을 통해서 새로운 프로젝트를 하나 생성해볼게요. 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를 실행하여 심볼릭 링크를 만들어 준 후에 다시 실행해보세요.

tutorial/
    manage.py
    print/
        __init__.py
        settings.py
        urls.py
        asgi.py
        wsgi.py

root폴더명은 맘대로 바꾸셔도 되요.

manage.py는 django-admin랑 basically같은 건데요, django-admin가 system path가 추가되어있지 않은경우에 직접 호출해서 사용할수 있습니다. 아래의 3개 명령은 모두 같은 명령입니다.

$ django-admin <command> [options]
$ manage.py <command> [options]
$ python -m django <command> [options]

__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 서비스를 만들어 붙여볼게요.

$ cd tutorial
$ python manage.py startapp polls

위의 명령어를 실행하면 아래와 같은 폴더와 파일들이 생성됩니다.

polls/
    __init__.py
    admin.py
    apps.py
    migrations/
        __init__.py
    models.py
    tests.py
    views.py

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라는 변수가 선언되어 있는데요.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

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등을 정의해주어야해요.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'tutorial',
        'USER': 'tutorial',
        'PASSWORD': 'PassWord123!!!',
        'HOST': '127.0.0.1',
        'PORT': '3306',
    }
}

ENGINE에 대한 자세한 사항은 여기를 참고해주세요.

기왕에 settings.py를 열었으니까 잠시 서비스 timezone을 맞추고 갈게요. 현재는 UTC로 설정이 되어있는데 여러분이 서비스할 지역의 시간으로 해야 읽기 편리하겠죠. 한국은 KST로 하면 될거 같아요. Korea Standard Time. 저는 뉴욕시간으로 정보를 저장하도록 설정하겠습니다.

TIME_ZONE = 'EST'

그리고 settings.py파일 맨 위에 보시면 INSTALLED_APPS라는 변수가 있어요. 이게 현재 프로젝트에 사용될 django의 application들을 나열해 놓은건데요. 다른 application을 추가할수도 있고 사용하지 않는건 제거하셔도됩니다. 하지만 기본적으로 추가된 application들이 유용하게 쓰일수 있으니까 일단 이대로 갈게요.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

위의 유틸중에 어떤 애들은 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와 함께 넣어줍니다.

INSTALLED_APPS = [
    'polls.apps.PollsConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

이제 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클래스를 객체화해서 정의한 변수에 실제 값들을 넣습니다. 질문내용과 출간시간을 아래와 같이 넣을게요.

>>> q = Question(question_text="What's new?", pub_date=timezone.now())

그리고 나서 이 model을 실제 database에 적용합니다.

>>> q.save()

적용하고 나면 model에 원래는 없었던 id가 생깁니다. database에서 q를 저장한뒤 자체적으로 생성한 id를 q.id에 저장해줌으로써 저장결과도 알려주고 필요한데다 갖다 쓰라는거죠.

>>> q.id
1

우리가 넣은 데이타가 잘 들어갔는지 살펴볼까요?

>>> q.question_text
"What's new?"
>>> q.pub_date
datetime.datetime(2022, 1, 10, 13, 27, 23, 742673, tzinfo=<UTC>)

아주 잘 들어갔네요. 이번에는 내용을 수정해볼까요? 해당객체의 question_text변수의 값을 바꾸고 저장을 하면 이번에는 q.id에 값이 들어가 있기때문에 새로운 record를 생성하는게 아니라 기존에 record를 수정하게 됩니다.

>>> q.question_text = "What's up?"
>>> q.save()

question에 저장된 데이타를 전부다 꺼내볼까요?

>>> Question.objects.all()
<QuerySet [<Question: Question object (1)>]>

처음에는 비어있던 테이블이 이제는 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을 다시 실행하셔야 변경된 내용이 적용되어 보여집니다.

$ python manage.py shell
>>> from polls.models import Choice, Question
>>> Question.objects.all()
<QuerySet [<Question: What's up?>]>

이번에는 Question클래스에 해당 Poll이 최근에 만들어 진건지 알아보는 함수를 추가해볼게요.

import datetime

from django.db import models
from django.utils import timezone


class Question(models.Model):
...
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

파일을 저장하고 쉘을 다시 실행해볼게요.

python manage.py shell

우선 만든 model들을 import하고요

>>> from polls.models import Choice, Question

저장된 Question을 모두 가져옵니다.

>>> Question.objects.all()
<QuerySet [<Question: What's up?>]>

이번에는 filter 함수를 이용해서 검색을 해볼까요? 고유번호가 1인것을 가져옵니다.

>>> Question.objects.filter(id=1)
<QuerySet [<Question: What's up?>]>

고유번호가 2인걸로 검색을 해볼까요? 현재 우리가 넣은 데이타가 1개이기때문에 2번 데이타는 없죠. 하지만 에러는 나지 않고 빈 배열을 넘겨줍니다.

>>> Question.objects.filter(id=2)
<QuerySet []>

이번에는 마찬가지로 filter함수를 이용해서 질문으로 검색해볼게요. 질문이 What으로 시작하는 Question을 검색해볼까요?

>>> Question.objects.filter(question_text__startswith='What')
<QuerySet [<Question: What's up?>]>

이번에는 get함수를 이용애서 올해 만들어진 질문을 가져와볼게요

>>> from django.utils import timezone
>>> current_year = timezone.now().year
>>> Question.objects.get(pub_date__year=current_year)
<Question: What's up?>

이번에는 마찬가지로 get함수를 이용해서 id로 질문을 가져와볼까요?

>>> Question.objects.get(id=1)
<Question: What's up?>

없는 데이타를 가져오려고 하면 어떻게 될까요?

>>> Question.objects.get(id=2)
Traceback (most recent call last):
...
polls.models.Question.DoesNotExist: Question matching query does not exist.

get함수는 없는 데이타를 가져오려고 시도하면 에러가 납니다.

pk로도 가져올 수 있어요.

>>> Question.objects.get(pk=1)
<Question: What's up?>

이번에는 가져온 데이타를 객체에 담아서 해당객체의 함수를 호출해볼게요

>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
True

이번에는 질문에 연계된 선택지를 불러와볼까요. 1번 질문을 가져와서 해당 질문의 선택지 전체를 보도록합니다.

>>> q = Question.objects.get(pk=1)
>>> q.choice_set.all()
<QuerySet []>

결과 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와 비번을 치고 들어가세요.

Django admin login screen

로그인 하시면 아래와 같은 관리자페이지가 보이실거에요. 안전하게 로그인 할수 있는 사용자를 등록할수 있고, 사용자에게 특정권한을 부여할수도 있습니다.

Django admin index page

이제 지금까지 우리가 만든 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에 명시해주어야합니다.

from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

코드위에 주석에서 보시다시피 브라우저에서 /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함수를 아래와 같이 수정해 주세요.

from django.shortcuts import get_object_or_404, render

from .models import Question
...
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

get_object_or_404를 이용하면 try/except코드를 사용하지 않아도 해당 데이타가 없는 경우 자동으로 404페이지를 보여줍니다.

Use the template system

이번에는 detail페이지를 열었을때 해당 질문의 선택사항을 불러다가 보여주도록 하겠습니다. polls/templates/polls/detail.html파일을 열어서 아래와 같이 수정해 줍니다.

<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

Question객체의 question_text를 h1크기로 보여주고 Question객체의 choice_set배열방을 돌면서 ul/li로 선택지를 목록으로 보여줍니다.

Removing hardcoded URLs in templates

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'을 아래와 같이 추가해주세요.

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

이렇게 urls.py에 app이름을 명시함으로써 템플릿에서 아래와 같이 polls:detail앱이름을 붙여서 경로를 명시할수 있습니다.

<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

Write a minimal form

이제 실제 설문조사에 사용자가 투표를 할수 있도록 detail페이지에 form을 만들어볼까요? polls/templates/polls/detail.html을 아래와 같이 수정해주세요.

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
<fieldset>
    <legend><h1>{{ question.question_text }}</h1></legend>
    {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
    {% for choice in question.choice_set.all %}
        <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
        <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
    {% endfor %}
</fieldset>
<input type="submit" value="Vote">
</form>

위의 템플릿에서는 각 선택지별로 라이오상자를 만들어 선택할수 있게 하고 submit버튼을 이용하여 form의 정보를 polls:vote로 전송하는 기능을 합니다.

polls:vote의 위치는 polls/urls.py에 아래와 같이 정의되어있습니다.

path('<int:question_id>/vote/', views.vote, name='vote'),

polls/views.py을 열어서 임시로 만들어뒀던 vote함수를 아래와 같이 수정해주세요.

from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse

from .models import Choice, Question
...
def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

이 함수에서는 해당 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에 아래와 같이 정의되어있습니다.

path('<int:question_id>/results/', views.results, name='results'),

view파일에서 results함수를 아래와 같이 수정해주세요.

def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})

get_object_or_404함수를 이용해서 해당 id의 Question정보를 가져오고, 가져온 객체를 results.html템플릿에 전달해서 질문과 선택지 그리고 각 선택지에 저장된 투표개수를 화면에 출력합니다. results.html은 아래와 같이 생성해 주세요.

<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

이제 결과를 확인해볼까요? 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로 변경해 주셔야합니다.

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

그리고 위의 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 manage.py test polls

Sources:

venv

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를 만들어서 작업을 하면 버젼에 대한 충돌없이 관리가 잘되겠죠? 도움이 되셨길 바랍니다. 그럼 오늘도 좋은 하루 되세요.

Use Apache on Mac 12

Mac 12, Monterey에서는 Apache가 기본적으로 설치되어 있습니다. 아래 명령어를 실행하여 Apache서버를 띄웁니다.

sudo apachectl start

브라우저를 열어 http://localhost에 접속하면 It works!라는 글씨가 적힌 페이지가 뜹니다. Apache서버를 중단하고 싶을때는 아래 명령어를 실행합니다.

sudo apachectl stop

기본적으로 Apache의 Document Root는 /Library/WebServer/Documents입니다. 해당 폴더를 열어보시면 index.html.en라는 파일이 있는데 그 파일 안에 It works!라는 문장이 들어있습니다.

How to install MySQL on Mac

우선 여러분의 컴퓨터에 homebrew가 설치되어있어야합니다. homebrew에 대한 설치 및 사용방법은 여기를 클릭해서 숙지한뒤 다시 돌아와 MySQL설치를 진행해주시기 바랍니다.

homebrew를 이용해서 MySQL을 설치합니다.

brew install mysql

brew service를 이용해서 MySQL server 실행합니다.

$ brew services start mysql
Cloning into '/usr/local/Homebrew/Library/Taps/homebrew/homebrew-services'...
remote: Enumerating objects: 1656, done.
remote: Counting objects: 100% (535/535), done.
remote: Compressing objects: 100% (392/392), done.
remote: Total 1656 (delta 229), reused 354 (delta 130), pack-reused 1121
Receiving objects: 100% (1656/1656), 481.42 KiB | 6.78 MiB/s, done.
Resolving deltas: 100% (705/705), done.
Tapped 1 command (44 files, 616.3KB).
==> Successfully started `mysql` (label: homebrew.mxcl.mysql)

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서버를 띄우는 방법을 배워보았습니다. 도움이 되셨기를 바래요. 바이!

homebrew

homebrew는 Mac운영체제에서 각종 유틸들을 손쉽게 설치하고 사용할수 있게 도와주는 Package manager입니다. homebrew를 한번도 설치하지 않은 분들은 아래 명령어를 입력하여 설치해주세요.

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

기존에 설치를 하신분들은 brew -v 명령어를 이용해서 현재 버젼을 알아낼수 있습니다.

$ 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

homebrew에 대해 더 자세한 사항은 여기를 클릭해주세요.

jsonschema validataion

안녕하세요 오늘은 jsonschema를 통해서 사용자가 입력한 JSON데이타에 어떤 오류가 있는지를 확인하는 방법을 알려드릴게요.

초간단 validation

일단 우리가 사용할 모듈은 jsonschema입니다. import하시구요

import jsonschema

그 다음에 하셔야하는 일이 바로 schema를 정의하는 건데요. 여기서는 군더더기 다 빼고 그냥 기능만 아주 간단하게 알려드릴게요. 우선 여러분들이 입력받고자하는 사용자 입력데이타가 어떤건지 정의를 합니다. 저는 여기에서 우선 정수값의 ID를 받을 거구요, 이름은 문자열로 받을거에요.

SCHEMA = {
    "type": "object",
    "properties": {
        "id": {
            "type": "integer"
        },
        "name": {
            "type": "string"
        }
    }
}

이렇게 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두개로 나눠서 정의할게요

schema = {
    "type": "object",
    "properties": {
        "id": {
            "type": "integer"
        },
        "name": {
            "type": "object",
            "properties": {
                "first": {
                    "type": "string"
                },
                "last": {
                    "type": "string"
                }
            }
        }
    }
}

데이타는 완전 다 틀리게 넣어볼게요.

data = {
  "id": "ellie",
  "name": {
      "first": 1,
      "last": 2,
  }
}
validate_schema(schema, data)
print(data)

결과를 보면 data가 전부 엉뚱한 데이타가 들어와서 data를 출력해보면 아무것도 없는 object인걸 확인하실수 있으실거에요

{}

그럼 이번에는 이름만 틀리게 해볼까요?

data = {
  "id": 1,
  "name": {
      "first": 1,
      "last": 2,
  }
}
==> {'id': 1}

이름중에 하나만 틀리게 하면 어떨까요?

data = {
  "id": 1,
  "name": {
      "first": "ellie",
      "last": 2,
  }
}
==> {'id': 1, 'name': {'first': 'ellie'}}

결과가 잘 나오는거 같네요. 여러분들도 한번 여러분들만의 코드를 만들어서 한번 실행해보세요. 이런것들이 알고나면 참 간단한데 배우기 전에는 헷갈리는 것들이죠. 배우지 않고도 자유롭게 코딩을 할수 있는 그 날이 올때까지 다같이 열심히 배워봅시다. 오늘도 여기까지 따라오느라 수고하셨습니다. 좋은 하루 되세요.

[Additional]

Validate Array

The validation result is a bit different when you have invalid data in an array.

schema = {
    "type": "object",
    "properties": {
        "rates": {
             "type": "array",
             "items": {
                 "type": "number"
             }
        }
    }
}
data = {'rates': ['a', 1, 'b']}
validator = jsonschema.Draft7Validator(schema)
errors = validator.iter_errors(data)
for error in errors:
    print(error.path)

You will get below.

deque(['rates', 0])
deque(['rates', 2])

As you see, it will return the errors for each item.

Array formula in Excel

와우!! 엑셀에서 이런게 되는지 몰랐어요. 기능이 좀 억지스럽긴 하지만 프로그래밍을 못하는 엑셀사용자에게는 어쩌면 이런 기능이 구세주가 되는 상황이 있을수도 있을거 같다는 생각이 들어서 여러분들께 소개해드리려고 가지고 와봤어요.

제가 오늘 소개해드리고자 하는 기능은 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를 아래와 같이 지정해주세요.

그리고 결과를 보여줄 E5에 아래과 같은 공식을 넣어주세요

=ARRAYFORMULA(IFERROR(INDEX(names,SMALL(IF(groups=E$4,ROW(names)-MIN(ROW(names))+1),ROWS($E$5:E5))),""))

그리고 해당 셀의 오른쪽 작은 파란 네모를 드래그해서 복사해주세요.

Array Formular로 간단하게 그룹별로 소속된 사람들을 한눈에 볼수가 있게 되었네요.

원리 이해하기

자 그럼 지금부터 이 formular가 왜 이렇게 동작하는지 원리를 차근차근 설명해드릴게요.

=ARRAYFORMULA(IFERROR(INDEX(names,SMALL(IF(groups=E$4,ROW(names)-MIN(ROW(names))+1),ROWS($E$5:E5))),""))

이 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을 통해서 만들어낸 배열과 연산해볼게요

{   1,     2,     3,     4,     5,    6,     7}
{True, False, False, False, False, True, False}
------------------------------------------------
{1,    False, False, False, False, 6,    False}

이제 이 배열을 가지고 만날 바로 다음 함수가 바로 SMALL인데요.

SMALL(IF(groups=E$4,ROW(names)-MIN(ROW(names))+1),ROWS($E$5:E5))

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가 몇줄짜리 데이타인지를 반환하기 때문에 아래와 같은 결과가 나옵니다.

SMALL함수에서 받을 인자가 2개다 만들어 졌으니 이제 SMALL함수를 실행해 볼까요?

SMALL(IF(groups=E$4,ROW(names)-MIN(ROW(names))+1),ROWS($E$5:E5))

현재 첫번째 인자에는 배열, {1, False, False, False, False, 6, False}가 들어가있고, 두번째 인자에는 현재 첫번째 결과값은 E5한개니까 줄의 개수는 총 1개가 되서 1이 두번째 인자로 들어가겠죠?

SMALL({1, False, False, False, False, 6, False}, 1)

위와 같은 인자가 들어갔을때 SMALL은 우선 배열방을 작은 순서로 아래와 같이 정렬을 합니다. 두번째 인자는 제가 아까 뭐라고 했죠? n번째 라고 했잖아요.

SMALL({1, 6}, 1)

그럼 위와 같이 SMALL함수를 호출하면 두개의 배열방에서 첫번째 방의 값을 반환합니다. 결과는 1이 됩니다.

그러면, 해당 셀을 아래로 복사한 경우에는 range가 2개의 셀이 되어서 총 ROWS의 갯수가 2가 되는데 이런 경우에는 어떤 값을 반환할까요?

SMALL({1, 6}, 2)

네 맞아요. 2번째 값인 6을 반환하겠죠.

그런데 예를 들어서 우리가 복사를 더 밑에까지 했다고 칩시다. 그러면 range의 행수가 3이 되어서 3번째 데이타를 가져오려고 할텐데 배열에는 배열방이 2개 밖에 없죠 이런 경우에는 어떻게 될까요? 네 그 셀에서는 에러가 납니다.

그래서 나중에 함수를 다 끝내고 나가기 전에 에러처러를 해주어야하는 이유입니다. 일단 SMALL에서 받아온 행번호를 가지고 INDEX를 통해서 값을 가져와 볼까요?

INDEX(names,SMALL(IF(groups=E$4,ROW(names)-MIN(ROW(names))+1),ROWS($E$5:E5)))

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를 정리해서 보면 아래와 같습니다.

ARRAY_CONSTRAIN(
  ARRAYFORMULA(
    IF(
      J$2="",
      "",
      IFERROR(
        INDEX(
          Discounts!$F$2:$F$999,
          SMALL(
            IF(
              Discounts!$B$2:$B$1393=J$2,
              ROW(Discounts!$F$2:$F$999)-MIN(ROW(Discounts!$F$2:$F$999))+1,
              ""
            ),
            ROW(Discounts!B1)
          )
          ,1
        )
        ,""
      )
    )
  )
  ,1
  ,1
)

오늘 배운 내용을 가지고 응용해 볼수 있는게 뭐가 있는지 생각해보고 다른 결과를 한번 구현해보는것도 나쁘지 않을것 같아요. 여기까지 따라 오시느라 수고 많으셨습니다. 좋은 하루 되세요.

References:

Table Info in MySQL

To see the collation of the database

SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME
FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '데이타베이스명';

To see the info about a table

SHOW TABLE STATUS where name like '테이블명'\G

To see the info about the columns

SHOW FULL COLUMNS FROM 테이블명;

Python Pickling

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()

위의 코드를 실행하면 아래와 같이 문자열로 저장되어있던 내용이 다시 오브젝트로 로딩이 되어서 보여집니다.

Omkar => {'age': 21,  'name': 'Omkar Pathak',  'key': 'Omkar',  'pay': 40000}
Jagdish => {'age': 50,  'name': 'Jagdish Pathak',  'key': 'Jagdish',  'pay': 50000}

Source: https://www.geeksforgeeks.org/understanding-python-pickling-example