jam 블로그

깔끔한 파이썬 탄탄한 백엔드 -6- 본문

IT Book Study/깔끔한 파이썬 탄탄한 백엔드

깔끔한 파이썬 탄탄한 백엔드 -6-

kid1412 2020. 9. 3. 22:46
728x90

6. 데이터 베이스

  • 데이터를 저장 및 보존하는 시스템

  • 데이터 베이스의 종류

    • 관계형 데이터 베이스 시스템 (RDBMS, Relational Database Management System)

      • 데이터들이 서로 상호관련성을 가진 형태로 표현한 데이터
      • 대표적으로 MySQL, PostgresSQL
      • 모든 데이터는 2차원 테이블로 표현
      • 각 테이블은 칼럼(column), 로우(row)로 구성
      • 각 로우는 고유 키(primary key)가 존재
      • 테이블들의 상호 관련성 종류
        • one to one
          • A테이블의 로우와 B테이블의 로우가 정확히 일대일 매칭일 경우
            • 예시 : 국가에 수도 연결
        • one to many
          • 테이블 A의 로우가 테이블 B의 여러 로우와 연결되는 관계
            • 예시 : 한 유저가 여러 개의 게시물을 쓸수 있음
        • many to many
          • 테이블 A의 여러 로우가 테이블 B의 여러 로우와 연결되는 경우
            • 예시 : 한 유저를 여러 사용자가 팔로우 할수 있고 해당 사용자 또한 여러 사용자를 팔로우 할수 있는 경우
      • 트랜젝션
        • 모든 루틴이 정상적으로 실행되었을때만 데이터베이스에 영구적으로 반영되며, 그게 아닐 경우 이전 상태로 복구
        • ACID
          • Atomicity(원자성) : 모든 작업이 반영되거나 모두 롤백
          • Consistenct(일관성) : 데이터는 미리 정의된 규칙에서만 수정이 가능
          • Isolation(고립성) : A와 B 두개의 트랜젝션이 실행되고 있을 때 A의 작업들이 B에게 보여지는 정도
          • Durability(영구성) : 한번 커밋된 트랜젝션의 내용은 영원히 적용되는 특성을 의미
      • 장점
        • 데이터를 더 효율적이고 체계적으로 저장 관리가 가능
        • 데이터들의 구조를 미리 정의함으로써 데이터의 완전성이 보장
        • 트랜잭션 기능을 제공
      • 단점
        • 데이블 구조 변화등에 덜 유연하다
        • 확장이 쉽지 않다
        • 서버를 늘려 분산 저장이 쉽지 않다. 스케일 아웃(서버 수 늘리기)보다는 서케일 업(서버 성능 업)을 해야한다.
    • 비관계형 데이터 베이스 시스템 (NoSQL, Non-relational Database Mangement System)

      • 테이블들의 스키마(schema)와 테이블들의 관계를 미리 구현해야 하는 필요가 없이 데이터가 들어오는 그대로 저장

      • 저장되는 데이터의 구조에 따라 달라집니다.

        Key Value DB : Key와 Value의 쌍으로 데이터가 저장되는 가장 단순한 형태의 솔루션
        Wide Columnar Store : Big Table DB라고하며, Key, Value에서 발전된 형태로 Column Family 데이터 모델을 사용
        Document DB : JSON, XML과 같은 Collection 데이터 모델 구조를 채택
        Graph DB : Nodes, Relationship, Key-Value 데이터 모델을 채용

      • 장점

        • 저장하는 데이터의 구조 변화에 유연하다.
        • 시스템 확장하기 쉬워 스케일 아웃 방식이 가능하다.
        • 방대한 양의 데이터를 저장하는데 유리
      • 단점

        • 데이터의 완전성이 덜 보장된다.
        • 트랜젝션이 안되거나 되더라도 불안하다.

      SQL

      Structured Query Language이며, 관계현 데이터베이스에서 데이터를 읽거나 생성 및 수정하기 위해 사용되는 언어이며, 아래는 Mysql에서 기본적인 CRUD(Create, Read, Update, Delete)를 위한 기본적인 Query 명령어입니다. Query에서 column이나 table이름같은 고유한 값을 제외한 명령어들은 대/소문자 상관 없이 쓸수 있습니다.

    • SELECT

      // table_name에서 column1, column2를 가져옵니다.
      SELECT column1, column2 from table_name
      
      // table_name에서 모든(*) column을 가져옵니다.
      SELECT * from table_name
    • INSERT

      // table_name에 column1에 column1_value를 column2에 column2_value를 넣습니다.
      INSERT INTO table_name(column1, column2) VALUES (column1_value, column2_value)
    • UPDATE

      // table_name에 column2가 value2인(WHERE 뒤가 조건문) row들에서 column1에 value1으로 update합니다.
      UPDATE table_name SET column1 = value1 WHERE column2 = value2
    • DELETE

      // table_name에 column1이 value1인 row들을 전부 지웁니다.
      DELETE FROM table_name WHERE column1 = value1
    • JOIN

      // table2에서 tabel1의 id와 table2에서 table1_id가 같은 값을 table1에 붙인 후 table1.column1과 table2.column2를 출력합니다.
      SELECT table1.column1, table2.column2 FROM table1 JOIN table2 ON table1.id = table2.table1_id

Mysql 설정

sudo apt update
sudo apt install mariadb-server

# 보안상 해야하면 remove 할 것들은 전부 remove하세요
$> mysql_secure_installation

# 만약 remote에서 mysql을 접속하고 싶을때
# 파일에서 bind_address=127.0.0.1을 0.0.0.0으로 변경합니다.
sudo vi /etc/mysql/my.cnf
sudo service mysql restart

# 보안상 root는 remote를 못하게 막았기 때문에 원격으로 붙을 수 있는 계정을 생성해야합니다.
$> mysql -u root -p
# 사용할 스키마명 생성
mysql> CREATE database 스키마명;
# 생성한 스키마를 사용할 유저 생성
mysql> CREATE user '계정아이디'@localhost identified by '비밀번호';
mysql> CREATE user '계정아이디'@'%' identified by '비밀번호';
mysql> GRANT all privileges on '스키마명'.* to '계정아이디'@'%' identified by '비밀번호' with grant option;
mysql> flush privileges;

API와 데이터 베이스 연결하기

miniter에서 사용할 DB를 만들어 보겠습니다. 위에서 스키마명을 miniter로 하고 진행하면 됩니다.

mysql> use miniter;
mysql> CREATE TABLE users (
    id INT NOT NULL AUTO_INCREMENT,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(80) NOT NULL,
    hashed_password VARCHAR(255) NOT NULL,
    profile VARCHAR(2000) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    UNIQUE KEY email (email)
) DEFAULT CHARSET=utf8mb4;

mysql> CREATE TABLE users_follow_list (
    user_id INT NOT NULL,
    follow_user_id INT NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (user_id, follow_user_id),
    CONSTRAINT users_follow_list_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id),
    CONSTRAINT users_follow_list_follow_user_id_fkey FOREIGN KEY (follow_user_id) REFERENCES users(id)
) DEFAULT CHARSET=utf8mb4;

mysql> CREATE TABLE tweets(
    id INT NOT NULL AUTO_INCREMENT,
    user_id INT NOT NULL,
    tweet VARCHAR(300) NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    CONSTRAINT tweets_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id)
)DEFAULT CHARSET=utf8mb4;

# 이미 테이블을 만들어서 charset을 바꾸고 싶다면 다음과 같이 쓰면됩니다.
mysql> ALTER TABLE users convert to CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
mysql> ALTER TABLE users_follow_list convert to CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
mysql> ALTER TABLE tweets convert to CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci

# 위와 같이 할 때 Specified key was too long; max key length is 767 bytes 이러한 에러가 날 경우 다음과 같이 변경합니다. 
# users에 email VARCHAR(255) NOT NULL를 email VARCHAR(80) NOT NULL 로 변경합니다.
  • 아래는 책과 다르게 파일들을 분할하여 작성한 Full Source 입니다.
# config.py
# user, password는 DB의 id와 password 입니다.
db = {
    'user': '',
    'password': '',
    'host': 'localhost',
    'port': 3306,
    'database': 'miniter'
}

DB_URL = f"mysql+mysqlconnector://{db['user']}:{db['password']}@{db['host']}:{db['port']}/{db['database']}?charset=utf8"
# util.py
from flask.json import JSONEncoder

class CustomJSONEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, set):
            return list(obj)

        return JSONEncoder.default(self, obj)
# api.py
from flask import current_app
from sqlalchemy import text

def get_user(user_id):
    user = current_app.database.execute(text("""
        SELECT
            id, name, email, profile
        FROM
            users
        WHERE id = :user_id
    """), {'user_id': user_id}).fetchone()

    return {
        'id' : user['id'],
        'name' : user['name'],
        'email' : user['email'],
        'profile' : user['profile']
    } if user else None

def insert_user(user):
    return current_app.database.execute(text("""
        INSERT INTO users (
            name,
            email,
            profile,
            hashed_password
        ) VALUES (
            :name,
            :email,
            :profile,
            :password
        )
    """), user).lastrowid

def insert_tweet(user_tweet):
    return current_app.database.execute(text("""
        INSERT INTO tweets (
            user_id,
            tweet
        ) VALUES (
            :id,
            :tweet
        )
    """), user_tweet).rowcount

def insert_follow(user_follow):
    return current_app.database.execute(text("""
        INSERT INTO users_follow_list (
            user_id,
            follow_user_id
        ) VALUES (
            :id,
            :follow
        )
    """), user_follow).rowcount

def insert_unfollow(user_unfollow):
    return current_app.database.execute(text("""
        DELETE FROM user_follow_list
        WHERE user_id = :id
        AND follow_user_id = :unfollow
    """), user_unfollow).rowcount

def get_timeline(user_id):
    timeline = current_app.database.execute(text("""
        SELECT
            t.user_id,
            t.tweet
        FROM tweets t
        LEFT JOIN users_follow_list ufl ON ufl.user_id = :user_id
        WHERE t.user_id = :user_id OR t.user_id = ufl.follow_user_id
    """),{
            "user_id": user_id
        }).fetchall()

    return [{
        'user_id': tweet['user_id'],
        'tweet': tweet['tweet']
    } for tweet in timeline]
# app.py
from flask import Flask, jsonify, request
from sqlalchemy import create_engine
from util import *
from api import *

def create_app(test_config=None):
    app = Flask(__name__)
    app.json_encoder = CustomJSONEncoder

    if test_config is None:
        app.config.from_pyfile("config.py")
    else:
        app.config.update(test_config)

    database = create_engine(app.config['DB_URL'], encoding='utf-8', max_overflow = 0)
    app.database = database

    @app.route('/', methods=['GET'])
    def index():
        return "Hello Flask"

    @app.route('/ping', methods=['GET'])
    def ping():
        return "pong"

    @app.route("/sign-up", methods=['POST'])
    def sign_up():
        new_user = request.json
        new_user_id = insert_user(new_user)
        new_user = get_user(new_user_id)

        return jsonify(new_user)

    @app.route('/tweet', methods=['POST'])
    def tweet():
        user_tweet = request.json
        tweet = user_tweet['tweet']

        if len(tweet) > 300:
            return '300자를 초과했습니다.', 400

        insert_tweet(user_tweet)

        return '', 200

    @app.route('/follow', methods=['POST'])
    def follow():
        payload = request.json
        insert_follow(payload)

        return '', 200

    @app.route('/unfollow', methods=['POST'])
    def unfollow():
        payload = request.json
        insert_unfollow(payload)

        return '', 200

    @app.route('/timeline/<int:user_id>', methods=['GET'])
    def timeline(user_id):
        return jsonify({
            'user_id': user_id,
            'timeline': get_timeline(user_id)
        })

    return app

if __name__ == '__main__':
    app = create_app()
    app.run(host='0.0.0.0', port=5000, debug=True)
  • 기존과 동일하게 python app.py로 실행 후 postman으로 api를 쏴보면 제대로 동작하며 DB에 값이 쌓이는것을 확인 할 수 있습니다.
Comments