⚡️ API 개발 with Flask - 9

codeamor·2020년 7월 14일
0

API Development

목록 보기
9/9

✔️ SQLAlchemy

파이썬 코드에서 DB와 연결하기 위해 사용할 수 있는 다양한 라이브러리가 있는데,
SQLAlchemy라는 라이브러리는 가장 널리 쓰이는 라이브러리 중 하나이다.

📌️ SQLAlchemy는 ORM(Object Relational Mapper)이다.
ORM이란, 관계형 데이터베이스의 테이블들을 프로그래밍 언어의 클래스로 표현할 수 있게 해주는 것을 말한다.
즉, 클래스(class)를 사용해서 테이블들을 표현하고 데이터를 저장, 읽기, 업데이트 등을 할 수 있게 해준다.
하지만 여기서는 ORM 부분은 사용하지 않고 CORM 부분(데이터베이스와의 연결)만을 사용할 것이다.
처음부터 ORM을 사용하면 SQL을 배울 수가 없기 때문이다.

SQLAlchemy 설치

다음 명령어를 실행한다.

pip install sqlalchemy

SQLAlchemy에서 MySQL을 사용하기 위해서는 MySQL용 DBAPI 또한 설치해야 한다.
DBAPI는 이름 그대로 DB를 사용하기 위한 API이다.
그 중 MySQL의 공식 파이썬 DBAPI인 MySQL-Connector를 사용하도록 한다.

pip install mysql-connector-python

앞서 말했듯이 SQLAlchemy를 사용하여 데이터베이스에 연결하여 SQL을 실행시켜 데이터베이스로부터
데이터를 읽어 들이거나 생성하거나 할 수 있다.

예를 들어, users 테이블에서 사용자의 이름과 이메일을 다음과 같이 읽어 들일 수 있다.

from sqlalchemy import create_engine, text

db = {
    'user'     : 'root',
    'password' : 'test1234',
    'host'     : 'localhost',
    'port'     : 3306,
    'database' : 'miniter'
}

db_url = f"mysql+mysqlconnector://{username}:{password}@{host}:
{port}/{database}?charset=utf8"
db     = create_engine(db_url, encoding = 'utf-8', max_overflow = 0)

params = {'name' : 'Moses'}
rows   = db.execute(text("SELECT * FROM users WHERE name = :name"),
params).fetchall()

for row in rows:
    print(f"name : {row['name']}")
    print(f"email: {row['email']}")
  1. from sqlalchemy import create_engine, text
    : sqlalchemy 모듈에서 필요한 함수들을 임포트한다. 여기서는 create_engine과 text를 임포트한다.
  • create_engine을 사용하여 데이터베이스에 연결하고 text를 사용하여 실행할 SQL을 만든다.

  1. db = {}
    : 데이터베이스에 연결하기 위해서는 데이터베이스 접속 정보가 필요하다.
  • 데이터베이스에 접속할 사용자 아이디, 비밀번호, 접속할 데이터베이스 시스템의 주소, 그리고 데이터베이스 이름 등이 필요하다.

  1. db_url = f"mysql+mysqlconnector://{username}:{password}@{host}:{port}/{database}? charset=utf8"
    : 2번 항목의 데이터베이스 접속 정보를 사옹해 DB URL을 구성한다.
  • DB URL을 사용해서 실제 데이터베이스에 접속할 수 있다.
  • 마치 URL을 사용해 웹사이트에 접속하는 것과 같은 개념이다.

  1. db = create_engine(db_url, encoding = 'utf-8', max_overflow = 0)
    : sqlalchemy의 create_engine 함수를 통해서 db_url에 명시된 데이터베이스에 접속한다.
  • create_engine 함수는 Engine 객체를 리턴한다.
  • 연결된 데이터베이스와 SQL 실행을 Engine 객체를 사용해서 할 수 있다.
    • 여기서는 Engine 객체를 db 변수에 저장했다.

  1. rows = db.execute(text("SELECT * FROM users WHERE name = :name"), params).fetchall()
    : Engine의 execute 메소드를 통해 SQL을 데이터베이스에 전송해 실행한다.
  • 여기서는 users 테이블에서 이름이 "Moses"인 사용자의 데이터를 읽어 들이는 SQL 구문을 실행한다.
  • execute 메소드는 크게 2가지 parameter를 받는다.
    실행할 SQL 구문과 SQL 구문에 필요한 인자들의 값이다.
  • SQL 구문에 필요한 인자들은 딕셔너리 형태로 보내야 한다.
    • text 함수에 넘겨진 SQL에 만일 :이 포함되어 있으면 : 다음에 오는 단어와 동일한 키를 사용해 딕셔너리에서 읽어 들여 치환한다.
      즉, 여기서
      SELECT FROM users WHERE name = : name은
      SELECT
      FROM users WHERE name = :"Moses"
      로 변경된다.
  • execute 메소드는 ResultProxy 객체를 리턴하는데 ResultProxy의 fetchall 메소드를 사용해 실제 데이터들을 리스트의 형태로 리턴한다.

  1. for row in rows: print(f"name : {row['name']}") print(f"email: {row['email']}")
    : 5번 항목에서 읽어 들인 데이터들을 for loop을 사용해 각 로우를 읽어 들여 원하는 칼럼의 값을 출력한다.
  • 각 로우는 딕셔너리처럼 사용할 수 있으며(딕셔너리는 아니다.), 칼럼 이름이 키 값으로 사용된다.


✔️ SQLAlchemy를 사용하여 API와 데이터베이스 연결하기

이제 실제로 미니터 API를 MySQL 데이터베이스에 연결하도록 한다.

먼저, 데이터베이스의 연결 정보를 저장할 파일인 config.py을 만들어 두자.

그리고 아래와 같이 데이터베이스 연결 정보를 저장시켜 놓는다.

 
  1. 'user' : 'root',
    : 데이터베이스에 접속할 사용자 아이디

  2. 'password' : 'test1234',
    : 사용자의 비밀번호

  3. 'host' : 'localhost',
    : 접속할 데이터베이스의 주소이다.
  • 지금은 컴퓨터에 설치되어 있는 데이터베이스에 접속하므로 localhost로 지정해 두었다.
  • 만약 외부 서버에 설치되어 있는 데이터베이스에 접속한다면 해당 서버의 주소를 지정해 주어야 한다.

  1. 'port' : 3306,
    : 접속할 데이터베이스의 포트 넘버이다.
  • 관계형 데이터베이스는 주로 3306 포트를 통해 연결된다.
  • API나 사이트와 마찬가지로 데이터베이스도 네트워크를 통해 연결되는 시스템이므로 당연히 포트 정보가 필요하다.

  1. 'database' : 'miniter'
    : 실제 사용할 데이터베이스 이름.

📌️ 이렇게 설정 파일을 따로 만드는 이유는 2가지이다.
첫 번째는, 설정 정보를 따로 관리함으로써 민감한 개인 접속 정보를 노출하지 않아도 된다.
두 번째는, 각 환경과 설정에 맞는 설정 파일을 적용할 수 있게 된다.
.gitignore 파일에 config.py파일을 지정해 놓음으로써 config.py 파일이 git 레포지토리에 포함되지 않게 하므로 개인정보 노출을 막고, 각 개발 호스트 혹은 서버에 맞는 config.py을 생성하도록 함으로써 각 환경에 적합한 설정을 적용하도록 하는 것이다.

이제 app.py 파일을 수정하여 config.py파일에서 데이터베이스 설정 정보를 읽어들여 데이터베이스와 연결하도록 할 것이다.

이미 본 대로 sqlalchemy의 create_engine을 사용하여 데이터베이스를 연결한다.

from flask import Flask, jsonify
from sqlalchemy import create_engine, text

def create_app(test_config = None):
    app = Flask(__name__)

    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
    
    return app
  1. def create_app(test_config = None):
    : create_app 이라는 함수를 정의한다.
  • Flask가 create_app이라는 이름의 함수를 자동으로 팩토리(factory) 함수로 인식해서 해당 함수를 통해서 Flask를 실행시킨다.
  • 중요한 점은 create_app 함수가 test_config라는 인자를 받는다는 것이다.
    단위 테스트(unit test)를 실행시킬 때 테스트용 데이터베이스 등의 테스트 설정 정보를 적용하기 위함이다.
    이 부분에 대해서는 단위 테스트를 다룰 때 자세히 알아볼 것이다.

  1. def create_app(test_config = None):
    : 만일 test_config 인자가 None이면 config.py 파일에서 설정을 읽어들인다.
  • 만일 test_config 인자가 None이 아니라면, 즉 test_config 값이 설정되어 들어왔다면 test_config의 설정을 적용시킨다.

  1. database = create_engine(app.config['DB_URL'], encoding = 'utf-8', max_overflow = 0)
    : sqlalchemy의 create_engine 함수를 사용해 데이터베이스와 연결을 한다.

  2. app.database = database
    : 3번 항목에서 생성한 Engine 객체를 Flask 객체에 저장함으로써 create_app 함수 외부에서도 데이터베이스를 사용할 수 있게 한다.

  3. return app
    : Flask 객체를 리턴한다.
  • 앞서 말했듯이 create_app이라는 함수는 Flask가 자동 인지하여서 Flask 객체를 찾아서 실행될 수 있게 한다.

이제 create_app 함수 안에 엔드포인트들을 구현할 것이다.
그리고 엔드포인트들을 구현할 때 앞서 본 대로 sqlalchemy를 사용하여 MySQL 데이터베이스에 연결하여 데이터를 데이터베이스에 저장 및 읽어 들이도록 할 것이다.

회원가입 엔드포인트

먼저 회원가입 엔드포인트를 데이터베이스를 사용해서 구현하도록 한다.
기본적인 구조는 이전에 구현했던 것과 동일하다.
다만 데이터를 하드코드하거나 메모리상에 저장하지 않고 데이터베이스에 저장한다.

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

        INSERT INTO users (
            name,
            email,
            profile,
            hashed_password
        ) VALUES (
            :name,
            :email,
            :profile,
            :password
        )
        """), user).lastrowid

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

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

        return jsonify(created_user)

Explanation

  1. new_user_id = app.database.execute(text("""
    ...
    : HTTP 요청을 통해 전달받은 회원 가입 정보를 데이터베이스에 저장한다.
  • app.database는 SQLAlchemy를 통해 MySQL 데이터베이스에 연결된 Engine 객체이다.
  • app.database를 통해 원하는 SQL 구문을 해당 데이터베이스에 실행하게 된다.
  • 이 경우에는 새로운 사용자 정보를 저장하는 것이므로 INSERT 구문을 통해 새로운 사용자 정보를 users 테이블에 저장하도록 한다.

  1. """), user).lastrowid
    : 1번 항목의 SQL 구문에 사용될 실제 데이터들은 HTTP 요청(request)에서 읽어 들인 데이터를 그대로 사용한다.
  • 이는 HTTP 요청을 통해 전송된 JSON 데이터의 구조가 1번 항목의 SQL에서 필요한 필드를 모두 포함하고 있기 때문이다.(name, email 등)
  • 만일 필드 이름이 틀리거나 필드가 부재인 경우 오류가 나게 된다.
  • 새로 사용자가 생성되면, 새로 생성된 사용자의 id를 lastrowid를 통해 읽어 들인다.

  1. row = current_app.database.execute(text("""
    ...
    : 2번 항목에서 새로 생성한 사용자의 정보를 데이터베이스에서 읽어 들인다.

  2. create_user = {
    ...
    : 3번에서 읽어 들인 새 사용자의 정보를 딕셔너리로 변환한다.
    그래서 JSON으로 HTTP 응답(response)으로 보낼 수 있도록 한다.

sign-up 엔드포인트는 다음의 명령어를 실행함으로써 테스트 할 수 있다.

http -v POST "http://localhost:5000/sign-up" \ name=Moses \ email=ssm2319@gmail.com \ password=test1234 \ profile="Christian. Student"

그런데 필자는 이 부분부터 진행이 안된다..
2달 전쯤에 책을 처음 읽었을 때에도 여기서 막혔는데.. 지금도 똑같다..


터미널 1

터미널 2


...
...
...

sqlalchemy.exc.ProgrammingError: (mysql.connector.errors.ProgrammingError) 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)

이 에러를 해결하지 못하겠다.
이전 게시글에서 똑같은 종류의 에러를 봤을 때는 비밀번호를 설정함으로써 해결하였는데
이 경우는 mysql의 비밀번호 유무와 상관없이 똑같은 메시지가 출력된다..
stackoverflow를 아무리 뒤져봐도 방법을 모르겠다..

mySQL의 DBAPI인 MySQL-Connector에 문제가 있는 것 같은데..

저자의 깃허브에서 모든 코드를 복사해와도 모든 http명령어가 에러가 뜬다.

0개의 댓글