어플리케이션 레벨의 입장에서, raw string 형태의 SQL은 여간 귀찮은 일이 아닐 수가 없다. 수많은 객체 간의 작업, 또는 함수와 콜백 간의 인터랙션이 주가 되는 소스 코드에서, raw string으로 되어 있는 쿼리 문자열은 가독성에 문제를 주기도 하고, 테이블이 alter되거나 데이터베이스 엔진을 마이그레이션했다면 쿼리 문자열들을 하나하나 찾아 수정해 주어야 하는 문제점도 있다.

한가지 예를 들어보자. tbl_users라는 테이블에 사용자에 관련된 몇가지 컬럼들이 있어서, 특정 나이에 해당하는 사용자들의 리스트를 반환하는 함수를 작성한다고 가정하면, 아래와 같은 코드가 나올 수 있다.

위 코드는 그리 문제가 없어 보이지만, tbl_users 테이블에 대한 쿼리를 다루는 곳이 프로젝트 전체에 걸쳐 10군데가 넘는다고 생각해 보자.

  1. 테이블이 alter되어 email 컬럼이 user_email이라는 이름으로 변경되었다면, '이름 변경'이라는 단순한 작업임에도 불구하고 해당 테이블을 사용하는 쿼리의 갯수만큼 작업 공수가 높아진다.
  2. 데이터베이스 서버를 마이그레이션한다면, 예를 들어 PostgreSQL을 사용하던 데이터베이스를 MySQL로 옮긴다면 쿼리를 수정해야 할 필요가 있다.

ORM

ORM은 Object Relation Mapping의 약자다. ORM은 RDB의 테이블 구조객체지향 프로그래밍 방법론에서의 객체가 꽤 많이 닮아 있다는 것에서 출발한다. 테이블에서 하나의 레코드는 하나의 객체와 매핑되고, 하나의 열과 타입은 객체의 필드와 타입으로 매핑할 수 있다. 결론적으로 ORM은 쿼리와 쿼리의 결과를 각각 string과 Map으로 관리하지 않고, 메소드와 객체로 이루어진 list로 관리하자는 것이다. 이것도 코드로서 예시를 들어 보겠다.

객체지향 표현 방식 중 하나인 클래스로 스키마를 매핑하고, 특별한 메소드를 이용해 쿼리하여 객체로 이루어진 리스트를 얻어온다. 문자열 형태의 SQL을 사용할 때에 비하면, 어플리케이션과 데이터베이스 간의 이질감이 줄어들었다. 그림으로 표현하면 아래와 같다.

객체 단위로 쿼리를 생성하고, ORM 라이브러리는 이를 raw string SQL로 변환하여 실제로 쿼리를 수행한다. ORM 라이브러리는 데이터베이스를 제공하는 vendor마다 어떤 방식으로 SQL을 작성해야할 지를 알고 있기 때문에, ORM은 데이터베이스 액세스에 대한 추상화 레벨을 높일 수 있는 가장 쉬운 방법이다. 그러나 ORM은 매우 많은 논쟁을 끌고 다니는 기술이다. 이는 Should I or should I not use ORM?이나 마틴 파울러의 ORMHate, Why we don't use ORM등의 글을 읽어보면 도움이 될 것 같다.

'배경지식' 카테고리의 다른 글

ProxySQL  (1) 2019.02.12
직렬화와 JSON  (0) 2019.02.12
HTTP 메소드  (0) 2019.02.12
StatsD  (0) 2019.02.11
HTTP 헤더  (0) 2018.11.02

HTTP라는 프로토콜은 그 이름(Hypertext Transfer Protocol)에 맞게 하이퍼미디어 문서 전송을 위해 웹 브라우저와 웹 서버 사이의 통신을 위해 설계되었지만, 연결을 열고 - 요청을 보낸 뒤 - 응답을 대기하는 고전적인 클라이언트-서버 모델이 필요한 곳이라면 웹 문서 전송이 아닌 다른 목적을 위해서도 사용할 수 있다. 이런 곳에서는 백엔드 레이어를 구현할 때 메소드(method) 개념을 생각하게 된다. 클라이언트에게 동일한 URL에 대해 생성/조회/삭제 등의 동작을 제공하는 게 명료하기 때문이다. 메소드는 HTTP 요청에서 사용하며, 클라이언트가 웹서버에게 요청의 목적이나 종류를 알리는 수단으로서 사용한다. HTTP 메소드에는 종류가 참 많은데도 불구하고 나는 GET/POST/PATCH/PUT/DELETE 정도만 사용하고, 알고 있었던 것 같아 한 번 정리해보고자 한다.

HTTP 메소드

HTTP의 요청에서 URL, 헤더, body와 함께 중요한 구성 요소다. 위에서 말했듯 자원(URL)에 대한 행위(verb)를 명시하기 위해 사용된다. 조회, 전송, 삭제 등의 행위에 따라 여러 종류의 메소드가 존재한다.

GET

GET /news HTTP/1.1

리소스의 표시를 요청한다. URL에 웹 브라우저로 접속하는 것이 대표적인 GET 요청의 예다. 게시글 목록, 주소록 등을 불러오는 것처럼, 데이터를 받아오는 맥락의 HTTP 요청은 통상적으로 GET을 사용한다. 대부분의 경우 GET 요청에는 body가 없다.

POST

POST /post HTTP/1.1

리소스에 데이터를 전송한다. 새로운 게시글을 작성하거나, 회원가입을 하는 등 새로운 데이터를 서버에 등록하기 위해 일반적으로 POST를 사용한다.

PUT

PUT /meal/2018/12/25 HTTP/1.1

POST와 동일하게, 리소스에 데이터를 전송한다. '생성하되, 이미 존재한다면 변경한다'라는 논리가 깔려 있는 것이 한 가지 다른 점이다. 사용자의 설정 데이터 적용이나, 날짜별 급식 정보 업로드처럼 POST와 PATCH를 구분짓기 애매하거나 클라이언트 레벨에서 생성과 수정을 따로 구현하기 귀찮아하는 기능에서 종종 사용했던 것 같다.

PATCH

PATCH /post/9e15c13aed1c48b5 HTTP/1.1

리소스의 특정 부분을 수정하는 데에 쓰인다. 게시글 수정이나 닉네임, 자기소개 변경과 같이 update 개념의 기능에서 자주 쓰인다.

DELETE

DELETE /me/profile-picture HTTP/1.1

리소스를 삭제한다. 친구 삭제나 프로필 사진 제거와 같은 기능에서 쓰인다.

HEAD

HEAD /dumps/log/1540993830.csv HTTP/1.1

리소스를 GET 메소드로 요청하는 경우에 어떤 헤더들이 반환되는지를 요청한다. 큰 용량의 리소스를 다운로드 받을지 말지 결정하기 위해서 Content-Length를 알아내거나, 응답에 영향을 줄 수 있는 헤더 목록을 조회하기 위해 Vary 헤더를 얻어오는 용도 등으로 사용할 수 있다. HEAD 메소드의 요청에 대한 응답은 body를 가질 수 없고, 만약 가지더라도 무시된다.

OPTIONS

OPTIONS * HTTP/1.1

자원에 대한 통신 옵션을 요청하기 위해 사용된다. 웹 클라이언트의 Axios 모듈에서 Access-Control-Allow-Origin 등과 같은 CORS 정보를 얻어내는 것이 한가지 예다. 서버 전체를 나타내기 위해 asterisk(*)를 사용하기도 한다.

클라이언트 사이드 렌더링HTTP API 기반의 웹 서비스가 활발히 만들어지고 있는 요즘에는 HEAD, OPTIONS와 같은 특별한 것들을 제외한 메소드들은 어떤 용도로 사용되어야 하는지에 대한 정리가 사실상 proposal 수준으로 받아들여지고 있는 것 같다. 따라서 '조회에는 무조건 GET을 써야 한다'라기보단 '조회와 비슷한 맥락의 기능에는 GET을 쓰는 게 좋다' 정도로 생각하면 될 것 같다. 내 경우에도 '목록 조회' 동작임에도 불구하고 요청 데이터의 양이 많아서 JSON body를 사용하도록 하기 위해 POST로 API를 구성할 때가 종종 있었다. ElasticSearchDruidGET에도 body를 포함시킬 수 있는데, 이게 웬만한 웹 프레임워크들에서도 지원된다면 참 좋겠다.

'배경지식' 카테고리의 다른 글

ProxySQL  (1) 2019.02.12
직렬화와 JSON  (0) 2019.02.12
ORM  (0) 2019.02.12
StatsD  (0) 2019.02.11
HTTP 헤더  (0) 2018.11.02

SQLAlchemy를 쓰면서 가장 혼란스러운 것 중 하나가 'Query 객체는 도대체 언제 SELECT 쿼리를 수행하는가?'였다. 마주치게 되는 상황은 이렇다.

  • query.all()로 SELECT의 결과를 모두 가져올 수 있다고 한다. 이는 전형적인 반복자(iterator)로, for row in query.all()처럼 for문에서 사용할 수 있다.
  • 그런데 그냥 query 자체를 for문에 넣는 예제도 보인다. for row in query를 해도 문제가 없다.
  • LIMIT 쿼리를 어떻게 표현하는지 검색했더니, 그냥 슬라이싱하라고 한다. 그래서 for row in query[:10].all()로 했더니 에러가 발생하고, for row in query[:10]처럼 표현하니 제대로 동작한다.

그냥 SQLAlchemy의 소스 코드를 GitHub에서 살펴보면, 그 비밀을 알 수 있다. sqlalchemy.orm.query.py 모듈을 보면 되고, 아래는 그들 중 일부를 가져온 snippet이다.

__iter__

Query 객체는 __iter__ 메소드에서 실제로 SELECT를 수행한다고 요약할 수 있다. __iter__ 메소드가 호출되는 타이밍 중 몇개를 예로 들면,

  • iter 함수에 전달되었을 때
  • for문에 사용되었을 때(for문은 in 우측에 전달된 객체를 iter 함수에 전달하므로)
  • list 함수에 전달되었을 때

그럼 의문이 조금 풀린다.

  • for row in query : for문에 의해 간접적으로 __iter__가 호출되므로 잘 동작한다.
  • for row in query.all() : 위 snippet을 보면 알 수 있듯, list(self)의 반환을 iteration하므로 잘 동작한다.

__getitem__

__getitem__ 메소드는 인덱싱과 슬라이싱 연산을 오버라이딩한다. Query.__getitem__ 메소드는

  • 슬라이스를 받았다면 현재 Query 객체에 LIMIT 쿼리를 추가하는 self.slice 메소드를 호출한 후, 이를 통해 LIMIT이 반영된 Query 객체의 SELECT 결과를 반환하거나
  • 인덱스를 받았고 그 인덱스가 -1이라면 list(self)[-1]을 반환하고, 아니라면 list(self[item:item + 1])[0]으로 __getitem__을 재귀호출하여 해당 인덱스에 대한 row 하나만을 가져오도록 LIMIT 쿼리를 수행해서 경제적으로 row를 반환한다. 예를 들어 query[15]에 대해서는, LIMIT 15, 1 쿼리가 추가된다.

따라서,

  • for row in query[5:8] : __getitem__에 슬라이스가 전달되고, LIMIT 5, 3 쿼리가 추가된 Query 객체의 iterator가 반환된다.
  • row = query[10] : __getitem__에 인덱스가 전달되고, LIMIT 10, 1 쿼리가 추가된 Query 객체의 결과 중 0번째 인덱스가 반환된다.

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

Column.like, Column.ilike, not_, ~expr  (0) 2019.02.12
특정 컬럼만 SELECT  (0) 2019.02.12
aliasing과 함수  (0) 2019.02.12
limit  (0) 2019.02.12
Multiple Primary Key  (0) 2018.10.18

+ Recent posts