API 서버를 개발하다보면, 반드시 DB와 통신이 필요한 순간이 찾아오게 됩니다.
애초에 대부분의 API 서버는 DB와 연동하여 작업하기 위해 만들게 되니까요.
하지만 DB와 통신하기 위해 직접 규약서를 읽고 순수 TCP 통신으로 구현하는 것은 그닥 좋지 않은 생각입니다. 그럴거면 파이썬을 쓰기보단 전부 C로 다 짜는게 낫지 않겠어요?
이와 같은 생각은 저희만 하는 건 아니기에, DB와 통신하는 일은 보통 2가지 방법 중 한가지를 택하게 됩니다.
DB와 통신하는 두 가지 방법
1. pymysql과 같은 DB 통신 보조 라이브러리 활용
https://github.com/PyMySQL/PyMySQL
flask 강의글이라던가, 약간의 데이터베이스 통신을 필요로 하는 분야에서 주로 사용하는 방식입니다.
매우 간단히 DB와 통신하게 해주고, 안정적으로 데이터를 주고받을 수 있습니다.
유의해야할 점은, pymysql은 오직 '통신'부분만 보조해준다는 것입니다.
서버의 테이블 구조를 파악하고, SQL문을 작성하는 것은 개발자가 직접 해야합니다.
2. sqlalchemy와 같은 ORM(Object Relational Mapping) 라이브러리 활용
또 다른 방법은 ORM 라이브러리를 활용하는 것입니다.
ORM은 기본적인 통신만이 아닌, DB를 다른 관점으로 다루는 기능들을 제공합니다.
ORM 라이브러리를 활용하게 되면, 대표적으로 아래와 같은 장점들이 있습니다.
1. DB를 객체로서 다룰 수 있습니다. 즉, SQL문을 직접 작성할 필요성이 사라지게 됩니다. 이로 인해 SQL Syntax Error로부터 원천적으로 벗어날 수 있습니다.
2. 객체로서 다루기때문에, 개발자 입장에서 더욱 편하고 유지보수에도 이점이 있습니다.
3. 대부분의 ORM 라이브러리는 DB에 종속적이지 않습니다. 즉, 다른 DB에 이주하게 되어 원래대로라면 다른 SQL문을 작성해야되는 상황이 되어도, 유연하게 처리해줍니다.
따라서 대부분의 모던 웹 프레임워크는 ORM 라이브러리를 활용하는것을 전제로 제작되게 되고, 이는 Fastapi역시 마찬가지입니다.
https://fastapi.tiangolo.com/tutorial/sql-databases/
Fastapi는 공식 문서에서 관계형 데이터베이스와 통신할 경우, sqlalchemy 라이브러리를 활용하는 것을 전제로 설명합니다.
이해하지 못할 일은 아닙니다. 대부분의 개발자들은 SQL문 한 줄 쓰기도 싫어하고, 열심히 작성했더니 튀어나오는 그놈의 Syntax Error를 보면 치가 떨리는게 정상이니까요.
...물론 장난이고, 처음에는 조금 어렵더라도 제대로 활용하게 되면 결국 ORM의 장점들이 추후 개발을 더 편하게 해준다는 것을 학습하였기에 그렇습니다.
다 좋은데, 귀찮아요...
하지만 이렇게 장점만 가득해보이는 ORM에도 문제는 존재합니다.
바로, DB를 직접 객체화하는 작업이 필수라는 겁니다.
마법처럼 내가 지금 사용하려고 하는 DB를 알아서 분석해서 객체화시켜주면 좋겠지만, 그렇게 쉽게 돌아가진 않습니다.
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from .database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
위와 같이, ORM을 위해선 DB의 모든 테이블을 형식에 맞춰 객체화해주는 작업이 필요하게 됩니다.
물론 sqlalchemy는 훌륭한 라이브러리이기 때문에 쉽게 구현 가능한 편이긴 합니다만, 매번 DB를 갖다 쓸 때마다 이 작업을 새로 해주는 작업이 여간 귀찮은 게 아닙니다.
장기적으로 활용할 계획이 있는 DB라면 ORM 기반으로 개발하는게 좋겠지만, 지속적으로 DB 테이블 구조에 변동이 생기는 단계에서 위와 같은 작업은 비효율적일 수 있습니다.
또, ORM 라이브러리마다 정해진 규약이 있기 때문에 API 서버 개발과 별도로 해당 라이브러리의 사용법도 따로 익혀야 합니다.
fastapi에서 pymysql로 DB와 통신하기
그러면 기존에 사용하던 pymysql만을 활용해서 DB와 통신하는 것은 어떨까요?
공식 가이드에 나와있는 방법은 아니지만, 충분히 가능합니다. 다만 몇 가지 주의해야 할 부분이 있어, 이제 설명드리고자 합니다.
fastapi 기본 구조 이해하기
우선, fastapi가 어떻게 다중 처리를 지원하는지에 대한 이해가 필요합니다.
fastapi는 파이썬 기반 웹 프레임워크이고, 따라서 기본적으로 멀티스레딩으론 제대로 된 다중 처리가 불가합니다.(GIL로 인한 문제)
따라서 제대로된 자원 활용을 위해선 멀티프로세싱이 필수이고, 각 프로세스의 생성과 관리는 uvicorn이 처리합니다.
uvicorn이 프로세스를 생성해주면, 그 프로세스 안에서 fastapi가 정해진 규칙(함수)에 따라 처리를 진행하게 됩니다.
그렇다면, 아래와 같이 코드를 작성해서 동작시키면 어떨까요?
from fastapi import FastAPI
import pymysql
# FastAPI 객체 생성
app = FastAPI()
# DB와 접근하는 conn, cur 객체 생성
conn = pymysql.connect(host="testdb.codedbyjst.com", user="mysqltest", password="AMAZINGPASSWORD1234",
db='TESTDB', charset="utf8", cursorclass=pymysql.cursors.DictCursor)
cur = conn.cursor()
# 어떻게 처리해야하는지 함수 정의
@app.get("/userInfo/{username}")
def getUserInfoByName(username: str):
sql = "SELECT * FROM userInfo WHERE username=%s"
cur.execute(sql, (username))
row = cur.fetchone()
return row
위와 같은 코드를 작성했다면, 아래와 같은 생각을 갖고 작성했을 겁니다.
"uvicorn이 프로세스를 자동으로 생성해주니까,
각 Request별로 conn, cur 객체가 생성되서 함수를 실행한 후에, 프로세스가 죽으면서 객체들도 날라가겠지?"
실제로, 코드를 돌리면 하루정도는 문제없이 돌아가는 걸 볼 수 있을 겁니다.
이를 믿고 프로덕션에 코드를 올리고 다음 날이 되면...
pymysql이 DB와 connection에 문제가 생겨 아예 DB에 접근이 안 되는 일이 발생할겁니다.
이는 어째서일까요? 프로세스의 특징을 잘 안다고 생각해서 위와 같이 짰을텐데, 왜 오류가 나는 걸까요?
위 코드에는 2가지 문제점이 있습니다.
[문제1]. conn, cur이 전역 변수
uvicorn이 fastapi가 처리할 수 있도록 멀티프로세스를 생성해주고, 관리해주는 것은 맞습니다.
하지만 이는 오직 처리 루틴(함수)에 한해서입니다.
conn, cur는 전역변수인 객체이기에, 각 프로세스가 공유해서 사용하게 됩니다.
즉, 각 프로세스가 독립된 conn, cur 객체를 가지지 않고, 최초에 생성된 한 쌍만을 공유해서 사용한 것입니다.
이는 당연히 매우 위험한 일이기도 한데요, 동시에 접근을 시도한다거나, 어떤 프로세스에서 fetch한 결과를 다른 프로세스에서 넘겨받을 수도 있기 때문입니다.
물론 asyncio와 같은 라이브러리를 사용하면 mutex를 해결할 순 있습니다만, 다른 문제도 존재합니다.
[문제2]. DB와의 연결이 종료되지 않음
위 코드 어느 부분에서도 DB와의 연결을 끊는 부분(conn.close())이 존재하지 않습니다.
DB와의 연결은 한 번 구성된다고 영구적이지 않습니다.
자원 관리를 위해 DB측에서 일정 시간동안 요청이 없으면 자체적으로 끊어버리기도 하고, 필요한 만큼 사용 후 정상적으로 connection을 종료시켜주지 않으면 좀비 connection이 그대로 남아 DB측에 과도한 부담이 되어 DB가 뻗어버릴수도 있습니다.
또, 한 번 연결이 끊겼다고 전체 프로그램이 멈춰버리면 안되겠죠?
물론 라이브러리에 따라 후자의 경우는 해결해줄 수 있습니다만(끊어지면 자동 재연결), pymysql에서 해당 내용을 구현하기 위해선 추가적인 작업이 필요합니다. 혹시 관심이 생긴다면 아래 내용을 참조하면 좋을 것 같네요.
https://stackoverflow.com/questions/22699807/python-mysql-using-pymysql-auto-reconnect
하지만 기본적으로 이는 현 상황에서 그닥 적절한 접근이 아닙니다.
어떻게든 conn, cur을 자동으로 DB와 재접속되도록 한다고 해도, 결국 전역 변수이기 때문에 각 프로세스가 별개로 할당받지 않아 [문제1]로 돌아오게 됩니다.
[해결책]지역 변수로서 활용
이를 어떻게 해결할 수 있을까요? 쉽습니다!
각 프로세스가 지역 변수로써 conn, cur 객체를 사용하면 됩니다.
예를 들자면, 아래와 같습니다.
from fastapi import FastAPI
import pymysql
# FastAPI 객체 생성
app = FastAPI()
# DB와 접근하는 conn, cur 객체 생성 후 반환
def mysql_create_session():
conn = pymysql.connect(host="testdb.codedbyjst.com", user="mysqltest", password="AMAZINGPASSWORD1234",
db='TESTDB', charset="utf8", cursorclass=pymysql.cursors.DictCursor)
cur = conn.cursor()
return conn, cur
# 어떻게 처리해야하는지 함수 정의
@app.get("/userInfo/{username}")
def getUserInfoByName(username: str):
# conn, cur이 각 프로세스별로 생성되어 지역 변수로서 활용됨
conn, cur = mysql_create_session()
try:
sql = "SELECT * FROM userInfo WHERE username=%s"
cur.execute(sql, (username))
row = cur.fetchone()
finally:
# try문이 성공/실패해도 반드시 connection은 종료됨
conn.close()
return row
위 코드를 보면 아래와 같은 단계를 따릅니다.
1. 각 프로세스가 생성될때마다 mysql_create_session()함수가 call되어, 새로운 DB와 connection을 형성
2. 해당 connection을 활용할 수 있는 conn, cur 객체가 반환되어 해당 프로세스의 지역 변수로서 활용됨
3. try문을 활용하여 try문에서 원하는 작업을 실행함.
4. finally문을 활용해 반드시 열린 connection이 닫히게 함.(비정상처리되어도 DB단의 문제 발생 방지)
즉 위에서 제시한 문제들이 해결되는 것을 알 수 있습니다.
이처럼, fastapi와 pymysql을 같이 사용할때는 별도의 함수를 통해 객체를 받아 활용하면 됩니다!
결론
사실, 왜 굳이 이렇게 몸을 비틀어가면서까지 pymysql을 쓰는거지...? 란 생각이 들 수도 있습니다.
ORM에는 확실히 장점들이 많습니다. 위에 언급한 것들 외에도 수많은 장점이 존재하기에, 대부분의 개발자들이 사용하는 거겠죠.
모던 웹 프레임워크에서 ORM은 거의 필수입니다. django는 애초부터 자체 ORM을 강제하고, ORM 자체로서도 훌륭해서 django를 쓰지 않아도 ORM만 갖다 쓰기도 하니깐요.
하지만 초창기에, 수많은 데이터베이스 수정이 있을 때에는 되려 거추장스러울 수 있습니다. 일반적으로 ORM 라이브러리는 큰 규모의 DB와 통신하는걸 대비해서 제작되었기 때문에 따라야 하는 규칙이 많습니다.
또한 해당 ORM 라이브러리에 대한 공부시간도 엄연히 소요되기때문에, 장기적으로는 몰라도 단기적으로는 손해일 수도 있습니다.
그런데 fastapi 공식 문서에서 아예 ORM을 사용하지 않는 방법을 안내하지 않아, 직접 경험해보면서 작성해보게 되었습니다. 여러분들의 개발에 도움이 됐기를 바랍니다.
다음 글에서는 pymysql 및 fastapi를 활용하여 여러 DB작업(CRUD)을 하는 것에 대해 말씀드리겠습니다.
'개발팁' 카테고리의 다른 글
Nginx와 Docker Compose로 무중단 배포하기 (0) | 2023.08.20 |
---|---|
Certbot HTTPS용 SSL 인증서 발급 / Nginx로 적용하기 (2) | 2023.08.15 |
스프링 부트 CORS 설정법 정리 (0) | 2023.08.12 |
Spring Boot + Redis Cache 사용법 정리 (0) | 2023.08.11 |
[Fastapi]파라미터 올바르게 다루기 (0) | 2023.03.03 |