여태까지 많은 의사결정과 작업을 섞어가며 우리가 개발에만 집중할 수 있는 환경을 열심히 만들어 봤다. 아직 꽤 부족한 상황이지만, 우리의 프로토타입 어플리케이션을 개발하는 데에는 이 정도면 충분하다. 이번엔 코드를 작성하는 데에 있어서 이런저런 판단의 기반이 될 의사결정을 진행하고자 한다. 깊게 고민할 건 딱히 아니고 체크리스트 정도라 각각의 의사결정은 (아마도)그리 길지 않을테지만, 갯수 자체가 많아서 여러 편으로 나눈다.

결정된 어플리케이션 스택(Python + Flask + Zappa)에 의해 선택지는 한정되더라도, 의사결정의 주제는 한정되지 않으므로 독자 여러분이 나중에 프로젝트를 하게될 때 이걸로 체크리스트를 만들어서 정리해 봐도 좋을 것 같다.

의사결정

어플리케이션 구조

배경과 요구사항

  • Flask는 마이크로 웹 프레임워크기에, Spring이나 Django와 대조되게 정해져 있는 구조가 없다. 모듈 하나에 이것저것 다 밀어넣어도 동작은 하겠지만, 코드에도 퀄리티라는 게 있으며 생산성과 유지보수를 미리 염두에 두면 좋으니 우리가 써먹어 볼만한 좋은 구조를 한 번 찾아보도록 하자.
  • 조직 내에 Flask 어플리케이션 구조를 직접 만들어 본 개발자가 있고, 서비스 레벨에서 사용해본 경험이 있다. Flask-Large-Application-Example
  • API 추가, 설정 데이터 증가, 스키마 추가 등과 같은 확장이 많이 이루어지더라도, 어플리케이션 구조로 인해 생산성이 제약되는 일이 없어야 한다.
  • 구조가 불필요하게 복잡하지 않아야 한다.

선택지

의사결정

Flask-Large-Application-Example으로 구조를 잡겠다. 그 이유는,

  • HermeticaFlask 어플리케이션의 뼈대를 만들어주는 CLI 툴이다. 근데 이게 굳이 이정도까지 필요한가 싶기도 하고, 이전에 한 번 써봤을 땐 Hermetica가 제공하는 뼈대에 100% 종속되지 않으면 차라리 더 불편했던 경험이 있다.
  • Flasky는 O'Reilly의 'Flask Web Development 2nd edition'가 챕터의 진행에 따라 Flask 어플리케이션을 발전시키는 히스토리를 커밋 단위로 기록한 저장소다. Flasky는 필자가 Flask 어플리케이션의 구조에 대해 고민할 때 많은 깨달음을 주었는데, relative import가 너무 많고, 우리에게 불필요한 것들(migration, Dockerfile, Procfile, boot.sh 등)이 조금 있어서 그대로 가져다 쓰려면 좀 많이 다듬어야할 것 같았다.
  • Flask-Foundation처럼 모듈마다 blueprint가 있는 방식확장에 오버헤드가 크다. extensions.py로 확장 패키지의 객체들을 따로 관리하는 것이나 config를 다루는 컨셉은 배울만한 부분이다.
  • cookiecutter-flask-skeleton은 나쁘지 않은 구조같은데, 이것도 우리한테 필요없는 게 너무 많다. 우린 JWT로 사용자를 인증하기로 했으나, 여기는 session을 통해 사용자 인증을 구현flask-login을 사용하고 있어서 이 부분을 걷어내야 하고, form 부분은 JSON 위주로 데이터를 관리하는 우리에게 아예 필요가 없다.
  • flask-boilerplate구조가 과하게 복잡하고, Flask에서 써먹으면 좋을 패턴들은 정작 별로 안 들어가있다. 나중에 참고할만한 코드 블럭이 중간중간에 조금 있긴 하지만 가져다 쓰고 싶지는 않다.
  • Flask-Large-Application-Example은 필자가 Best Practice들을 여기저기서 많이 찾아보고, 2년 가까이 Flask로 크고 작은 WAS를 몇번씩 만들면서 지속적으로 개선해온 구조다. 사실 익숙하다는 게 큰 이유긴 한데, 서비스 레벨에서 직접 써보기도 많이 써봤고 GitHub에 돌아다니는 이런저런 구조들은 솔직히 실속이 별로 없고 겉치레인 경향이 꽤 있다. 남들이 좋다는 거 하나 가져와서 커스텀하는 것보다 낫다.

request data validation 룰

배경과 요구사항

  • 비정상적인(예상 범위를 벗어나는) 데이터가 포함된 요청이 들어와 500 Internal Server Error를 일으키는 일을 줄이려면 검증이 필요하다. 예상 범위를 명시해서 요청 데이터를 미리 검증하고, 문제가 있다면 API 로직이 실행되지 않게 400 Bad Request406 Not Acceptable같은 status code를 내려주기 위함이다.
  • Content Type에 대한 validation이 가능해야 한다. 예를 들어, application/json만 허용하도록 정의해둘 수 있어야 한다.
  • key check가 가능해야 한다. 예를 들어, 게시글 작성 API에서 'title''content'가 요청의 JSON payload에 포함되어야 함을 명시할 수 있어야 한다.
  • nested keynon-required 제약 명시가 가능해야 한다. 예를 들어, 회원가입 API에서 'contact' 내에 'phone', 'email'이 요청의 JSON payload에 포함되어야 함을 명시하거나, 게시글 목록 API에서 'size'가 요청의 query string에 선택적으로 포함됨을 명시할 수 있어야 한다.
  • 값에 대해 타입 검증, 문자열이라면 길이의 범위, 숫자라면 값의 범위, enum 등에 대한 명시가 가능해야 한다.
  • view decorator 형식으로 사용하는 것을 기능으로 제공하고 있거나, view decorator화 시키는 데에 큰 문제가 없어야 한다.

선택지

의사결정

schematics를 선택하겠다. 그 이유는,

  • 선택지에서 schematics 외의 것들처럼 딕셔너리 형태로 정의하는 스키마자동완성의 도움을 받기 어렵다. schematics를 보면 데이터 각각의 제약조건을 StringType, URLType처럼 클래스를 통해 정의하는데, required같은 제약들을 생성자에 인자로 전달하는 형태이므로 IDE에게 자동완성 지원을 잘 받을 수 있다. 어떤 목적을 이루기 위해 코드를 뒤져보는 게 문서를 보는 것보다 낫다는 생각이다. 예를 들어, 'enum을 표현하려면 어떻게 해야 하지?'에 대해 StringType 클래스로 들어가 생성자 메소드의 인자를 보고 'choice'임을 인지하는 게, 공식 문서를 찾아보는 것보다 빠르고 정확하다.
  • 딕셔너리 형태로 정의하는 스키마는 다들 그들만의 세계가 있어서, 익숙해지기 전까진 생산성이 잘 안 나온다. 필자 기준으로 선택지들 중 2순위라고 생각이 드는 voluptuousschematics와 비교하면 되는데, '"size"는 int 타입이며 optional하고, 1부터 100 사이의 값을 가지며 기본값은 30이다'는 voluptuous의 경우 Optional('size', default=30): All(int, Range(min=1, max=100)), schematics의 경우 size = IntType(min=1, max=100, default=30)로 표현할 수 있다. schematics가 voluptuous에 비해 의식의 흐름 자체가 비교적 더 깔끔하다.
  • jsonschema와 cerberus는 문자열과 딕셔너리로만 이루어진 스키마를 정의한다는 점에서 비슷하게 생겼는데, 개인적으로 JSONSchema는 API 문서 작성할 때만 썼으면 좋겠다. 데이터 validation 용도로 쓰이기엔 포맷 자체가 과한 느낌이다.

SQL 쿼리 처리 방식

배경과 요구사항

  • 문자열 결합으로 쿼리를 만드는 것은 지양하려고 한다. 띄어쓰기나 개행, 따옴표, 포맷 문자열의 순서 등을 코드 레벨에서 계산하느라, 좋은 코드를 작성하기가 어렵고 생산성도 보장하기 어렵다.
  • 객체지향적으로 쿼리를 만드는 방식을 선호한다. 객체지향이란 단어만 나오면 이 악물고 달려드는 사람들이 있어서 패턴이고 뭐고 그런 얘기는 안 할거고, 아래와 같은 코드의 흐름을 생각하면 된다.
q = Query.from_(customers).select(
    customers.id, customers.fname, customers.lname, customers.phone
).where(
    (customers.age >= 18) & (customers.lname == 'Mustermann')
).orderby(
    customers.signup_time, order=Order.desc
)

쿼리 만들어주는 겸 쿼리 실행이랑 트랜잭션 관리도 해주면 더 좋다.

선택지

  • 문자열 결합으로 쿼리를 빌드하고 별도의 클라이언트로 직접 쿼리 실행
  • PyPika로 쿼리를 빌드하고 별도의 클라이언트로 직접 쿼리 실행
  • SQLAlchemy로 쿼리 빌드부터 실행까지 도움을 받기
  • Peewee로 쿼리 빌드부터 실행까지 도움을 받기

의사결정

SQLAlchemy를 선택하겠다. 그 이유는,

  • 문자열을 결합하는 방식으로 쿼리를 빌드하다 보면 정말 한숨밖에 안 나온다.
  • PyPika쿼리 빌더 라이브러리다. 객체지향 디자인 패턴 중 하나인 빌더 패턴을 통해 쿼리 객체를 만들도록 하고, 이를 쿼리 문자열로 변환해준다. 필자도 처음 보고 되게 쓸만하다 싶었는데, 라이브러리 특성 상 쿼리 실행 단계부터는 따로 관여하지 않는다는 게 마음에 좀 걸린다. 별도의 MySQL 클라이언트 라이브러리를 통해 쿼리를 직접 실행해줘야 하는데, 쿼리 결과에서 각 row들이 dictionary 타입이어서 너무 날것 그대로 쓰는 느낌이다. 그렇다고 따로 객체로 wrapping해서 쓸 바에 SQLAlchemy나 Peewee같은 ORM을 쓰는 게 더 낫다고 생각했다.
  • SQLAlchemyPeeweeORM + 쿼리 빌더 + 쿼리 실행 헬퍼가 합쳐진 라이브러리다. 테이블의 스키마를 클래스 형태로 정의하고, 그 클래스를 통해 객체지향적으로 쿼리를 빌드하고, 이를 실행하면 해당 클래스의 객체가 결과로 반환된다. 이 둘 중에 하나를 선택하는 건 취향 차이인 것 같은데, 필자의 관점에서 Peewee와 SQLAlchemy를 비교하면, Peewee는 비교적 코드가 깔끔해 보인다는 것 정도가 메리트고 SQLAlchemy만큼 고급 기능을 잘 지원하진 않는다고 생각한다. 이는 나중에 데이터베이스 서버에 읽기 전용 복제본을 추가하는 챕터에서 더 설명하도록 하겠다. 그리고 독자 여러분의 이해를 돕기 위해 ORM 라이브러리의 컨셉을 반영한 코드를 첨부한다.
class User(Model):
    meta = {
        'tablename': 'tbl_users'
    }

    email = StringField()
    name = StringField()
    age = IntField()

    @property
    def json(self):
        return {
            'email': self.email,
            'name': self.name,
            'age': self.age
        }


def get_user_list(age_matches):
    results = User.query(age=age_matches).all()

    return [res.json for res in results]

MySQL Driver(클라이언트 라이브러리)

배경과 요구사항

  • 어차피 실제로 쿼리를 실행하는 건 SQLAlchemy 단에서 처리해 주므로, API가 어떻게 생겼던 SQLAlchemy가 지원하기만 한다면 상관 없다.
  • Python 3를 지원해야 한다.
  • 개발이 몇 년째 진행되지 않다거나 하는 문제만 없다면, CPython 3.6을 기준으로 퍼포먼스가 가장 잘 나오는 드라이버를 선택하자. 이 쪽은 우리가 의사결정에 신경쓸만한 부분이 '쓸 수 있는가(SQLAlchemy와 Python 3를 지원하는지 등등..)'와 속도 정도밖에 없다.

선택지

의사결정

mysqlclient-python를 선택하겠다. 그 이유는,

  • MySQLDB1'legacy version'이라고 명시되어 있다. 버전 1.3.0부터 Python 3를 지원하기로 되어 있었으나, 2014년 1월 3일에 1.2.5 버전이 릴리즈된 후 더이상 개발이 진행되고 있지 않다.
  • 중간에 몇가지 버그 수정과 Python 3을 지원하자는 명목으로 MySQLDB1을 fork하여 MySQLDB2mysqlclient 프로젝트가 시작되었는데, MySQLDB2는 2012년 8월 25일 이후로 더 이상의 개발이 진행되고 있지 않다. 그에 비해 mysqlclientPython 커뮤니티가 개발을 잘 이끌어나가고 있다.
  • 두 벤치마크(methane/bench.py, Benoss/PythonMysqlDriversTest)를 보면 각각 mysqlclient와 MySQLdb1이 가장 준수한 성능을 보여준다. 애초에 mysqlclient가 MySQLdb1의 fork라는 걸 생각하면, 성능 상의 이점을 챙기기 좋을 것이라 판단했다.
  • mysql-connector-pythonOracle의 MySQL group에 의해 개발된 드라이버인데, 퍼포먼스가 그리 좋지도 않고, 라이선스 문제 때문에 PyPI를 통해서 설치할 수도 없다.
  • pymysql은 사실 mysqlclient와 maintainer(프로젝트 관리자)가 동일인물인데(pymysql과 mysqlclient를 한 조직에서 같이 개발하고 있음), 특별한 이유가 없다면 mysqlclient를 권고하고 있다.