내게 가장 익숙한 언어는 Python이고, 백엔드 엔지니어인 내게 가장 익숙한 웹 프레임워크는 Flask다. 처음 백엔드를 시작하고 썼던 게 JavaVert.x였는데, Flask는 비교적 훨씬 간단하게 웹 API 서버를 개발할 수 있었다. Flask를 처음 마주쳤을 땐 소프트웨어 쪽으론 아는 게 정말 없었기에, '간단함'은 Flask라는 프레임워크에 빠져들기에 충분한 이유였던 것 같다.

사실 내가 처음 Flask를 시작했을 땐 Flask의 메이저 버전이 0대였고, 0.11, 0.12같은 걸 stable 버전으로 사용했었다. 동시에 Flask의 인기가 꽤 많아지기 시작했고, 2018년 4월 27일 새벽에 Flask 1.0이 릴리즈되었다. Flask는 마이크로 프레임워크기에, 정석적으로 정해져 있는 틀이 딱히 없어서 구조나 모범 사례들에 대해 자주 고민하게 됐었다. 근데 지금 생각해 보면, 무언가에 대해 그토록 깊게 고민해봤던 적이 없던 것 같다. 그래서 Flask는 내게 정말 고마운 프레임워크다.

Flask

마이크로 프레임워크, 멀티 스레드 형태의 요청 처리 방식을 사용하는 웹 프레임워크이다. Python으로 개발되었으며, WSGI(CGI의 Python 구현체) 툴킷으로 Werkzeug를, 템플릿 엔진으로 Jinja2를 사용한다. Flask는 The Pallets Projects라는 팀에서 개발하고 있는데, Flask가 의존하고 있는 Werkzeug와 Jinja2 또한 동일한 팀에서 개발되고 있다.(The Pallets Project는 clickitsdangerous라는 라이브러리로도 유명하다.)

Flask는 비슷한 느낌의 이름을 가진 bottle이라는 마이크로 프레임워크를 겨냥하여 만들어졌고, 따라서 API도 비슷한 점이 많다.

마이크로 프레임워크 답게 간단한 웹 서버REST API 서버를 빠르게 개발하기 좋고, 나만의 어플리케이션 구조를 만들어나가는 재미도 쏠쏠하다. AWS Lambda + AWS API Gateway 위에 서버리스 어플리케이션의 배포를 돕는 zappa도 Django, Pyramid, Bottle과 함께 Flask를 지원한다. 풀 스택 프레임워크인 Django와 함께, 가장 인기가 많은 파이썬 웹 프레임워크가 아닐까 싶다.

다양한 스타일의 라우팅 방식 지원, context와 설정 데이터 관리, 테스트 등에서 프레임워크가 정말 많은 부분에 대해 깊게 고민해 주었다는 것을 느낄 수 있고, 공식 문서와 코드가 매우 잘 정리되어 있다. '파이썬을 여행하는 히치하이커를 위한 안내서'라는 책에서는, '훌륭한 코드를 읽기' 챕터에서 Flask를 예로 들어 설명하고 있을 정도다. Lightweight, Minimal, Open source, Documentation, Easy to use, Well designed 정도가 Flask를 사용하는 이유다.

GET '/' -> 'Hello World'

Flask를 이용해, 3000번 포트에서 동작하며 '/'에 대해 GET 요청을 하면 'Hello World'를 반환하는 간단한 웹 서버를 만들어 보도록 하자. 먼저 pip를 통해 Flask를 설치해야 한다. 가상 환경이 필요하다면 별도로 준비하도록 하자.

$ pip3 install flask

setup.py에 의해 Flask와 함께 의존성 라이브러리인 Werkzeug, Jinja2, itsdangerous, click이 동시에 설치된다. 그리고 아래는 위에서 이야기한 요구사항을 맞춘 Flask 어플리케이션이다.

1번 라인에서는 flask 패키지에서 Flask 클래스를 import했다. __init__.py가 import를 중계해 주기에, 실제로 잡히는 경로는 flask/app.py 모듈의 Flask 클래스다.

3번 라인에서는 Flask 객체를 생성하고 있다. import_name이라는 인자에 Flask 어플리케이션 패키지의 이름을 전달하며, 일반적으로 __name__을 사용한다.

5~7번 라인은 API의 정의에 대한 부분이다. Flask에서 지원하는 라우팅 방식 중 가장 기본형은, 위와 같이 route 데코레이터함수를 이용하는 것이다. 첫 번째 위치 인자로 전달되는 문자열은 해당 API의 URL rule이 되고, 키워드 인자로 HTTP 메소드 등 추가적인 옵션을 붙일 수 있다. 별도로 메소드를 명시하지 않으면 GET에 대해서만 동작하며 다른 메소드로 접근 시 405 Method Not Allowed가 response된다.

Flask에선 위처럼 API의 로직을 처리하는 함수를 view function이라고 부른다. view function의 이름은 식별자로 사용되기에 unique해야 하며, return 문에서 반환된 값은 Flask에서 Response라는 클래스의 인스턴스로 wrapping하여 응답으로 전해진다. 위의 경우, 'Hello World'라는 문자열을 return했으므로 response data는 'Hello World'가 되고, Flask에서 content type과 status code를 각각 기본값인 text/plain, 200으로 설정하여 response한다.

10번 라인은 Flask 어플리케이션을 실행하는 구문이다. 여기에도 debug 모드 등 몇가지 옵션을 추가할 수 있다.


결론적으로, 우리는 10줄 남짓의 코드를 통해 '/'에 대한 GET 요청을 처리할 수 있는 HTTP 서버를 구현했다. python 또는 python3 명령을 통해 모듈을 실행하고, localhost:3000에 접속하면 'Hello World'가 보일 것이다.

Django와 같은 풀 스택 프레임워크에 비해, 준비 과정이 간단하고 '파일 하나'로 천천히 시작할 수 있다. 사람마다 다르겠지만, 굳이 지금 필요없는 건 보이지조차 않아서, 내겐 차근차근 배워가기에 정말 좋았다.

'Python 계열 > Flask' 카테고리의 다른 글

POST '/' -> 'Hello World'  (0) 2018.09.20

boto3는 워낙 기능이 방대하다 보니 대부분의 경우에는 gist 등에서 예제를 찾아보게 되는데, boto3.resource를 사용하는 예제도 있고, boto3.client를 사용하는 예제도 있었다. 둘을 비교해 보자.

Client

  • low-level 인터페이스
  • service description에 의해 만들어짐
  • botocore 수준의 client를 공개(botocore는 AWS CLI와 boto3의 기초가 되는 라이브러리)
  • AWS API와 1:1 매핑됨
  • 메소드가 스네이크 케이스로 정의되어 있음

가끔가다 보면 botocore의 API를 찾아봐야 하는 경우가 생길 때도 있다. boto3.client는 AWS API와 1:1 매핑된다는 게 꽤 큰 메리트였던 것 같다.

Resource

  • high-level, 객체지향적 인터페이스
  • resource description에 의해 만들어짐
  • 식별자(identifier)와 속성(attribute)을 사용
  • 자원에 대한 조작 위주

'자원에 대한 조작 위주'라는 게 정확히 무슨 의미인지는 잘 모르겠지만, s3.Bcuket과 같은 구문을 의미하는 것이 아닐까 싶다. 당연하게도, boto3.resourceboto3.client를 wrapping한 high-level 인터페이스지만 boto3.client의 모든 기능을 wrapping하진 않으므로 때때로 boto3.clientboto3.resource.meta.client를 사용하여 작업해야 하는 경우가 생길 수 있다.

로직 수준의 직관성코드 수준의 직관성의 차이가 아닐까 싶다. 나는 boto3를 회사에서나 가끔 쓰는 정도였어서, 어떤 게 더 나았다거나 하는 주관적인 입장도 사실은 없다. 사용량 기준으로는 boto3.client가 더 많았던 것 같고, boto3를 써야 하는 작업이 있다면 boto3.client로 먼저 방법을 찾아보지 않을까 싶다. 그것보다 boto3는 네이밍 컨벤션이 참 특이한 것 같다.

SQL에는 스키마가 동일한 두 테이블을 합쳐 쿼리하는 UNION과, SELECT 절에 리터럴한 expression을 두는 Literal Select라는 개념이 존재한다.

SQLAlchemy에서는 Query 객체의 union(*q)union_all(*q)로 각각 UNION/UNION ALL을, sqlalchemy.sql.expression.literal_column 함수를 통해 literal select를 표현할 수 있다. 위에서 예를 든 union 쿼리를 SQLAlchemy로 표현해 보자.


engine은 일반적으로 sqlalchemy.engine.create_engine 함수에 의해 생성되는, SQLAlchemy에서 사용되는 lowest level의 객체다. engine은 어플리케이션이 데이터베이스와 통신할 때마다 사용할 수 있는 connection pool을 유지한다. 쿼리를 위해 사용하는 engine.executeengine.connect(close_with_result=True)를 수행해 Connection 객체를 얻고, conn.execute를 호출하는 편리한 메소드다. ORM을 사용하지 않아서, sqlalchemy.text 함수로 객체에 바인딩되지 않은 straight SQL 쿼리를 수행하는 경우 이처럼 engine과 connection을 사용하는 것이 적절한 방법이다.

어플리케이션이 SQLAlchemy ORM을 사용한다면, 객체에 바인딩된 쿼리를 위해서 Session 객체를 사용해야 한다. 이는 session.add(), session.rollback(), session.commit(), session.close()를 통해 트랜잭션을 단일 작업 단위로 관리하기 좋고, 이러한 특징을 통해 Python의 Context Manager 패턴을 사용하기에도 좋다.

session을 생성해 yield하고, 정상적인 경우 commit, 오류가 발생하면 rollback, 어느 상황이든 close하도록 만든다. 쿼리가 필요한 부분을 with-as 블럭으로 감싸기만 하면, global한 트랜잭션 관리는 context manager가 모두 수행해 준다. SQLAlchemy 문서에서는 위와 같은 구현을 scoped session이라고 이야기하고 있다. 따라서 raw string SQL을 사용하는 경우 engine이나 connection, ORM을 사용하는 경우 session과 scoped session 패턴을 사용하는 것이 적합하다고 생각한다. session 객체에서도 물론 execute 메소드가 지원되기 때문에, 어플리케이션 전체에 걸쳐 그냥 session만 사용하는 practice도 꽤 있었던 것 같다.

앞으로 블로그의 SQLAlchemy 카테고리에 올라올 포스트들의 예제는, scoped session이 적용되어 있다(session_scope 함수가 이미 존재한다)는 것을 가정하고 작성될 예정이다.

sqlalchemy.sql.text는 raw string 형태의 SQL을 Connection.execute나 그에 상응하는 함수(engine.execute, session.execute)에 전달하여 쿼리하기 위한 함수다.

placeholder 기능을 지원하며, :[name] 포맷을 사용하고 키워드 인자로 이들에 값을 채운다.

되도록이면 ORM을 쓰자. 개인적으로는 ORM만 충실히 써도 어플리케이션 코드와 데이터베이스 간의 결합도를 많이 낮출 수 있었던 것 같다.

query 객체에는 filter 메소드가 있다. SQL로 따지면 WHERE clause의 역할이다. filter 메소드에 expression을 전달하면, 새로운 query 객체를 반환한다. 아래 예제는 scoped session이 적용되어 있고, TblUsers라는 ORM 클래스가 적당히 존재한다고 가정한다.

조건 결합, 추가적인 관계 연산자

query 객체의 filter 메소드에 sqlalchemy.sql 하위 함수, 또는 ORM 클래스 필드의 SQL expression 메소드를 전달하여 조건식을 결합하거나, 일반적인 관계 연산자로 표현하기 힘든 IN, BETWEEN 등의 조건식을 표현할 수 있다.

SQLAlchemy의 query 객체가 메소드 형식으로 WHERE절을 빌드하는 컨셉만 알고 있도록 하자. 나중에 필요한 쿼리가 있을 때 구글링해서 찾아보기 더 편하다.

boto3에서 거의 대부분의 기능은 AWS API를 사용하고, 자원에 대한 자격을 증명하기 위해 AWS IAM에서 얻어낼 수 있는 AWS access key IDAWS secret access key, 또는 임시 자격 증명을 위한 aws_session_token을 사용한다. 일반적으로 awscli 등의 도구를 통해 ~/.aws/credentials에 자격 증명 데이터를 설정하고, boto3는 여기에 접근해 자격 증명을 진행한다. 그러나 boto3를 사용하는 어플리케이션에서 여러 개의 자격 증명을 동시에 수행해야 하는 경우가 생길 수 있다. 데이터를 수집하여 원하는 곳으로 서빙해주는 B2B 서비스인 Segment의 Amazon S3 Integration 기능을 예로 들 수 있다.

다행히도, boto3는 자격 증명 데이터를 얻어낼 때까지 여기저기에 순서대로 접근한다.

client, resource 함수에서 자격 증명 데이터를 매개변수로 전달

client, resource, Session 등을 얻을 때 aws_access_key_id, aws_secret_access_key, aws_session_token이라는 문자열 타입의 선택 인자를 사용할 수 있다. 시스템 설정을 무시하고, 자격 증명 데이터를 직접 전달하고 싶을 때 대부분 이 방법을 사용한다.

환경 변수

client, resource, Session에 자격 증명 정보가 인자로 전달되지 않으면, 환경 변수를 확인한다.

AWS_ACCESS_KEY_ID

AWS 계정의 access key

AWS_SECRET_ACCESS_KEY

AWS 계정의 secret key

AWS_SESSION_TOKEN

AWS 계정의 세션 키

공유 자격 증명(Shared credentials) 파일

그 다음부턴 파일들에 접근하기 시작한다. 공유 자격 증명 파일의 기본 위치는 ~/.aws/credentials이고, AWS_SHARED_CREDENTIALS_FILE 환경 변수를 설정하여 위치를 변경할 수 있다. credentials는 .ini 형식의 파일이고, 각 섹션마다 자격 증명을 위한 세 가지의 변수를 지정할 수 있다.

섹션 각각을 profile이라고 부른다. session을 생성할 때 AWS_PROFILE 환경 변수를 설정하거나 profile_name 인자에 이름을 전달하여 어떤 profile을 사용할 지 명시할 수 있다. 여기서 session은 client나 resource를 생성할 수 있도록 자격 증명에 대한 '상태'를 저장하고 있는 객체다.

별도로 명시하지 않으면, [default] profile을 사용한다. 보통은 이렇게 credentials를 사용하는 방식을 쓰는 것 같다.

AWS Config 파일

기본 위치는 ~/.aws/config이고, AWS_CONFIG_FILE 환경 변수를 설정하여 위치를 변경할 수 있다. config 파일은 credentials 파일과 동일하게 작성된다. 유일한 차이점은, profile 섹션을 [profile [name]] 포맷으로 표현한다는 것이다.

AssumeRole을 호출시키기

AWS_CONFIG_FILE 환경 변수에 명시된 파일이나, 기본 경로인 ~/.aws/config 파일에서 role에 대한 정보를 명시하여, boto3가 자동으로 AWS STS에 AssumeRole을 호출하도록 할수도 있다. role_arn, source_profile, role_session_name 등의 파라미터가 필요하다.

boto2 config

그 후에는, boto2 config 파일에서 자격 증명을 로드한다. BOTO_CONFIG 환경 변수가 나타내는 경로의 파일을 먼저 검사하고, 그렇지 않으면 /etc/boto.cfg~/.boto 파일을 검사한다. [Credentials] 섹션만 사용한다.

IAM Role

Amazon EC2에서 실행 중이며 위의 방법들로 자격 증명을 찾지 못한 경우, boto3는 EC2 인스턴스의 메타 데이터 서비스에서 자격 증명을 로드한다. EC2 인스턴스를 시작할 때 사용할 IAM role만 지정하면, 별도의 명시적인 구성을 하지 않아도 boto3가 알아서 인스턴스의 자격 증명을 사용한다.

Best Practice

boto3는 자격 증명 정보를 얻기 위해, 결론적으로 아래의 순서를 따른다.

  1. client/resource/Session에 전달되는 자격 증명 정보
  2. 환경 변수
  3. AWS_SHARED_CREDENTIALS_FILE에 명시된 파일 또는 ~/.aws/credentials에 접근
  4. AWS_CONFIG_FILE에 명시된 파일 또는 ~/.aws/config에 접근
  5. AWS_CONFIG_FILE에 명시된 파일 또는 ~/.aws/config에 접근하여 role 데이터(arn 등)로 AWS STS에 AssumeRole
  6. BOTO_CONFIG에 명시된 파일에 접근
  7. /etc/boto.cfg에 접근
  8. ~/.boto에 접근
  9. EC2 인스턴스의 IAM role 설정에 접근

EC2 인스턴스에서 boto3를 사용하는 경우 IAM role을 사용하는 것이 좋고, 그렇지 않은 경우라면 공유 자격 증명 파일(~/.aws/credentials)을 쓰는 것이 좋다고 한다.

'Python 계열 > boto3' 카테고리의 다른 글

boto3.resource와 boto3.client의 차이  (1) 2019.02.12
S3에서 객체의 용량을 알아내기  (0) 2019.02.12

SQLAlchemy에서 query 객체의 filter 메소드에 전달하는 관계 연산자는 Python에 내장된 관계 연산 기호(<, >, == 등)나, Column 객체에서 지원하는 메소드(Column.between, Column.in_ 등)를 사용할 수 있었다. SQLAlchemy의 query 객체의 filter 메소드를 통해 LIKEILIKE(case sensitive LIKE) 쿼리를 표현하려면, 각각 Column.likeColumn.ilike 메소드를 사용할 수 있다.

not

NOT을 표현하는 방법은, and와 or이 각각 sqlalchemy.and_, sqlalchemy.or_로 지원되었던 것을 생각하면 된다. sqlalchemy.not_ 함수를 사용한다.

또는, 단항 비트 반전을 위해 한번쯤 사용했던 unary operator(~)를 사용할 수 있다.

'Python 계열 > SQLAlchemy' 카테고리의 다른 글

text()  (0) 2019.02.12
모델 정의와 Generic Type vs SQL Standard/Multiple Vendor Type vs Vendor-Specific Type  (0) 2019.02.12
특정 컬럼만 SELECT  (0) 2019.02.12
aliasing과 함수  (0) 2019.02.12
limit  (0) 2019.02.12

SQLAlchemy에서 ORM을 사용하기 위해, 스키마를 class로 정의하는 방법이 있다.

sqlalchemy.declarative_base 함수를 통해 Base를 얻고, 이를 상속받는 형태로 모델을 구현한다. 클래스의 내부에는 __tablename__이라는 클래스 필드로 테이블 이름을 명시하며, Column 클래스의 생성자에 sqlalchemy.types 모듈 하위에 구현되어 있는, 타입을 나타내는 클래스를 넘겨 Column 타입의 객체들로 컬럼을 명시한다. 타입 정보를 생성자에 전달할 때, 클래스 자체를 넘겨도 되고 클래스의 인스턴스를 넘겨도 된다. Column(Integer)Column(Integer())가 동일하다는 것이다. sqlalchemy의 타입 클래스, SQL DDL에서의 타입, Python의 빌트인 타입 간 매핑은 다음과 같다. 자주 사용하는 타입 위주로 작성했다.

  • sqlalchemy.types.BigInteger - BIGINT - int
  • sqlalchemy.types.SmallInteger - SMALLINT - int
  • sqlalchemy.types.Integer - INT - int
  • sqlalchemy.types.Boolean - BOOLEAN or SMALLINT or TINYINT - bool
  • sqlalchemy.types.DateTime - TIME or TIMESTAMP - datetime.datetime
  • sqlalchemy.types.Float : FLOAT or REAL - float | asdecimal 인자를 True로 설정하면, decimal.Decimal 타입으로 다뤄진다.
  • sqlalchemy.types.PickleType - BLOB or TINYBLOB or MEDIUMBLOB or LONGBLOB - pickle로 직렬화 가능한 모든 객체 | list나 dict를 관리할 때 편리하게 사용할 수 있다.
  • sqlalchemy.types.String - CHAR or VARCHAR or TEXT - str
  • sqlalchemy.types.Text - CLOB or TEXT - str

BigInteger와 BIGINT, Boolean과 BOOLEAN, Text와 TEXT, ...

SQLAlchemy로 모델을 정의하다 보면, uppercase된 알파벳으로만 정의된 타입을 확인할 수 있다. SQLAlchemy 문서에서는 BigInteger, Boolean, Text 등을 Generic Type이라 부르고, BIGINT, BOOLEAN, TEXT 등을 SQL Standard and Multiple Vendor Type이라고 부른다.

Generic Type

BigInteger, Boolean, Text, DateTime, Float, Integer, ...

'Python 타입'의 데이터를 읽고, 쓰고, 저장할 수 있는 column을 명시한다. SQLAlchemy는 Generic Type의 컬럼에 대해 CREATE TABLE문을 만들어낼 때, 대상 데이터베이스에서 사용할 수 있는 최상의 타입을 알아서 선택한다. VARCHAR로 정의해 뒀는데 데이터베이스가 VARCHAR를 지원하지 않는다면, 그를 대체할 수 있는 타입으로 변경하여 DDL을 작성한다. 대충 써두면, 알아서 테이블을 생성해준다는 의미다. 코드 레벨에서 조금 더 편하다.

SQL Standard and Multiple Vendor Type

BIGINT, BOOLEAN, TEXT, BLOB, CHAR, CLOB, ...

SQLAlchemy에서 여기에 속하는 타입의 컬럼을 통해 CREATE TABLE문을 만들 때는, 데이터베이스 엔진에 상관 없이 컬럼의 타입을 항상 동일하게 만들어낸다. SQLAlchemy가 원하는 타입으로 정확한 DDL을 작성하도록 만들 때 유용할 수 있으나, Generic type과 달리 이들은 모든 데이터베이스에서 잘 작동한다는 보장이 없다. 따라서, 대충 'VARCHAR'라고 했다가 데이터베이스가 VARCHAR를 지원하지 않으면 DDL에 실패한다.

Vendor-Specific Type

mysql.BIGINT, mysql.BINARY, postgresql.REAL, oracle.VARCHAR2, ...

'벤더별로 명시되어 있는 타입'을 의미하는데, 여기서 벤더는 MySQL, PostgreSQL 등을 떠올리면 된다. sqlalchemy.dialects 패키지 하위에 데이터베이스별로 패키지가 존재하고, 여기에 명시되어 있는 타입을 사용하는 방식이다.


'Python 계열 > SQLAlchemy' 카테고리의 다른 글

Query 객체의 WHERE절 작성  (0) 2019.02.12
text()  (0) 2019.02.12
Column.like, Column.ilike, not_, ~expr  (0) 2019.02.12
특정 컬럼만 SELECT  (0) 2019.02.12
aliasing과 함수  (0) 2019.02.12

SQLAlchemy를 통해 쿼리하는 경우, 테이블 모델 자체를 넘기는 것이 가장 일반적이다.

SQL로 치면, SELECT * FROM ... 꼴이 될 것이다. SELECT id FROM ...처럼 특정 컬럼만 조회하고 싶다면, 해당 모델의 컬럼 객체들을 가변 인자 형태로 전달해주면 된다.

위 예제는 TblUsers에서 id, name, email을 SELECT한다. TblUsers의 테이블 이름이 tbl_users라면, SELECT id, name, email FROM tbl_users로 표현할 수 있을 것이다. 이 경우 쿼리의 결과 객체에서는 id, name, email만 접근할 수 있다. 다른 필드에 접근하려고 하면, TblUsers에 명시되어 있는 컬럼이더라도 에러가 발생한다.

+ Recent posts