문서화도 다 끝났으니 로직을 코드에 옮기기만 하면 된다. 그러나 아직 어떤 언어를 쓸지/의존성을 어떻게 관리할지/어떤 데이터베이스를 어디서 운영할지와 같은 것들이 정해지지 않아서 바로 개발에 착수하기는 어렵다. 이번에는 어플리케이션 기술스택(프로그래밍 언어와 프레임워크)을 결정하고, /GET 요청 시 'Hello World'text/plain으로 반환하는 서버를 작성해 보자.

도입 이유

웹 어플리케이션 서버를 작성할 수 있는 언어는 수없이 많고, 프레임워크도 그렇다. 그만큼 조직의 현재 상황과 미래, 서비스의 형태, 요구사항, 일정 등에 맞춰서 매우 신중하게 결정해야 한다. '요즘 다 파이썬 쓰니까 파이썬', 'Node.js는 한글 문서가 잘 돼있으니까', '장고가 쓰는 사람도 많고 쉬우니까'같은 트렌드 차원의 이유로 어플리케이션 기술스택을 가볍게 결정하고 나면, 조직에게 매우 큰 기술 부채가 되어 돌아올 수도 있다. 기술 선택은 팀과 비즈니스를 고려해야 한다. 오늘은 두 의사결정 모두 하나의 요구사항 집합으로 정리된다.

배경과 요구사항

  • 조직에게 익숙하거나, 러닝커브가 감당할 수 있는 수준인가? - 생산성이 보장되며 유지보수에 큰 문제가 없는가?
  • 조직이 납득할 수 있는가?
  • 너무 마이너한 기술스택은 아닌가? - 라이브러리나 예제는 결국 그 언어/프레임워크의 사용자가 만들기 마련인데, 인기가 없으면 커뮤니티의 사이즈가 작고, 직접 만들어야 하는 도구의 범위가 넓어질 수 있다.(memcached 클라이언트, StatsD 클라이언트, PostgreSQL ORM 라이브러리 등등) 그리고 커뮤니티 파워가 약하면 먼 미래를 보장하기 어려울 수 있다.
  • 안정적인 응답 속도에 rps(requests per second)를 잘 뽑아주거나, TTL/LRU Cache, 경량 스레드, 비동기 프로그래밍 지원처럼 성능 최적화를 위한 도구가 잘 준비되어 있는가? - 비용 최적화를 위해 매우 중요하다.

쭉 써놓고 나니, 대충 인기 많은 스택이라면 이 조건에 대부분 부합하지 않을까 싶다. 데이터 티어나 모니터링 티어의 인프라를 결정할 땐 조금 더 자유도 높게 선택지를 둘 수 있을텐데, 이번 의사결정은 어쩔 수 없이 인기를 좀 따르게 될 것 같으니 양해 바란다.

의사결정 - 프로그래밍 언어

선택지

WAS 개발에 있어서 주류 언어로 꼽히고 + 필자(조직)가 조금이나마 경험해본 언어들을 선택지로 두자.

  • Python
  • Scala
  • Java
  • Go
  • Node.js

의사결정

Python을 선택하겠다. 그 이유는,

  • 조직(필자)에게 가장 익숙하다.
  • 커뮤니티 파워가 강하다.
  • 언어가 강력하다. 대부분 언어들이 어디 하나씩은 맘에 안드는 부분이 있는데, Python은 이런저런 면에서 평타 이상 쳐준다.
  • unpacking, comprehension, 비교 연산자 chaining과 같은 syntactic sugar나 arrow같은 drop-in replacement 라이브러리들이 생산성을 잘 커버해 준다.
  • 라이브러리 풀이 꽤 크고, 의존성 관리와 가상 환경 지원도 꽤 쓸만하다.
  • 공식 코드 컨벤션 가이드라인PEP8 덕분에 코드 퀄리티 유지도 괜찮게 할 수 있다.
  • Scala액터 모델 기반의 짱 좋은 동시성을 가지고 있고, 동시성 모델 자체가 Immutability에 기반되어 있어서 동기화에 관한 삽질이 비교적 적다. 그러나 조직 내에 Scala를 잘 다루는 개발자가 없어서 생산성을 보장할 수 없다.
  • Java는 비즈니스 관점에서 인력풀이 풍부하고 레퍼런스가 많지만, 필자의 기준으론 JSON 리터럴이 없어서 꽤 불편하다. Go의 [string]interface 타입 map이나, Scala의 List/Map 타입, Kotlin의 hashMapOf 함수처럼 JSON 리터럴을 우회할만한 방법도 딱히 없다. Map 객체를 만들어 put문을 발라두거나 별도로 VO(Value Object)를 정의하고 JSON string을 VO 객체로 역직렬화/VO로 만든 객체를 JSON string으로 직렬화하는 식으로 사용한다. VO를 사용하는 방식은 꽤 직관적이지만, WAS의 로직과 데이터 모델이 복잡한 게 아니기 때문에 JSON 리터럴 표현식이 언어 차원에서 지원되는 게 좋을 듯 싶었다. 게다가 나중에 VO를 쓰는 방식으로 스타일이 바뀌더라도, 파이썬이라고 못하는 것도 아니고 말이다.
  • Go는 웬만하면 성능 정말 잘 뽑아주고, 사용자 층도 세계적으로 꽤 많아지는 추세지만, 이게 생각보다 생산성이 좋지 않고 의존성 관리가 마음아프다. 그래도 트래픽 처리 비용을 낮추려고 시도하게 된다면, 고성능 파이썬 웹 프레임워크들과 Go를 벤치마킹해 볼 것 같다. 내가 Go를 많이 안 해봐서 삽질하는 게 아닐까 하는 믿음이 있다(아직은).
  • Node.js는 JavaScript 런타임인데, 자바스크립트라는 언어가 생각보다 잘 쓰기 정말 어렵고, 굳이 러닝커브를 감수하고 Node.js를 선택하는 것에 당장의 메리트가 별로 없다.

결론은 조직에게 익숙한 Python을 선택하게 됐다. 물론 조직에게 아무리 익숙한 기술이더라도, 그 기술이 의사결정의 기준에 부합하지 못한다면 보류시키는 게 맞다.

만약 백엔드 개발자가 여럿 있고 각각에게 익숙한 기술스택끼리의 교집합이 없거나 적은 상황이라면, 서비스와 개발 조직의 요구사항(예산, 처리량, 개발 일정 등)을 기반으로 서로를 설득하는 시간을 갖자.

의사결정 - 웹 프레임워크

선택지

써볼만한 프레임워크는 이정도가 있다.

의사결정

Sanic을 사용하겠다. 그 이유는,

  • Flask마이크로 프레임워크이기에, 자유도가 매우 높다. 원하는 설계 방향대로 프레임워크를 유연하게 써먹을 수 있다. 그러나 멀티스레드 모델의 원천적인 한계(컨텍스트 스위칭) + Python의 GIL 구조로 인해 여러 스레드가 시분할해서 동작하기 때문인지, 성능이 조금 아쉽기도 하고 요청 객체가 의존성 주입 형태가 아니라 global 객체로 제공되어서 자칫하면 안티패턴 코드를 작성하게 된다. Flask를 가장 많이 써봤지만, 얘를 열심히 튜닝하기 전에 대체제를 찾아보고 싶었다.
  • Sanicuvloop: Blazing fast Python networking이라는 글에서 영감을 받아, Flask-like한 APIasync 개념을 넣고 uvloop로 asyncio 이벤트 루프를 튜닝한 비동기 웹 프레임워크다. 벤치마크 상황에 따라 다르지만, 동일 조건에서 웬만하면 Sanic이 Flask보다 2~4배정도 처리량이 많다. 그리고 요청/응답 객체가 의존성 주입 형태로 제공되고, 미들웨어 개념이 잘 잡혀 있다.
  • Vibora도 Flask-like API에 뭔가 이것저것 붙여서 퍼포먼스를 높이는, Sanic과 비슷한 컨셉을 가지고 있다. 그리고 실제로 정말 빠르다(Flask를 기준으로, Sanic이 2~4배, Vibora가 5~6배 정도). 그러나 레퍼런스가 너무 없다. Sanic은 공식 문서라도 잘 작성되어 있는데, Vibora의 문서는 '이게 다인가?' 싶다.
  • Japronto는 내가 아는 파이썬 웹 프레임워크 중 rps가 가장 높다(웹 프레임워크 전체에 대한 벤치마크에서도 거의 선두권). 그런데 뭐 프레임워크가 딱히 제공해주는 게 별로 없다. 라우팅 메소드 하나랑 요청 객체, 응답 헬퍼 몇 개 정도. 2017년 2월에 미들웨어 관련 이슈가 올라왔지만 '계획이 있다'고만 하고 아직도 open되어 있다. 그리고 Vibora보다 크게 빠르지도 않고, 사용 사례가 많지 않다.
  • Falcon은 사용 사례도 꽤 있고 + 성능도 Japronto, Vibora와 함께 파이썬 웹 프레임워크 중에서 선두권 + 문서도 잘 만들어져 있지만 프레임워크 디자인 자체가 꽤 새로운 느낌이라 러닝커브를 감당하기 힘들 수 있을 듯 싶었다.
  • aiohttp는 생각보다 퍼포먼스가 안나온다. Sanic이나 aiohttp나 async/await 기반인데, 굳이 aiohttp를 선택할 이유가 없다.
  • Tornado는 이름만 보면 엄청 빨라 보이는데, 파이썬의 주류 웹 프레임워크들 중에 퍼포먼스가 가장 안나온다. 퍼포먼스를 포기하고서라도 쓸만한 이유도 없다. 생각보다 커뮤니티 파워도 약하다.
  • DjangoDjangoRestFramework가 있긴 하지만, Django에 익숙한 조직이 아니므로 제외했다. DjangoRestFramework를 쓰는 후배한테 물어봤는데, 이것저것 챙겨주는 게 많아서 익숙해지기 시작하면 생산성이 아주 괜찮다고 하니 구미가 당긴다면 배워봐도 좋을 것 같다.

작업 - Hello World 서버 작성

이슈를 생성하고, /GET 요청 시 'Hello World'text/plain으로 반환하는 서버를 작성해 보자. 서버 작성 직후의 스냅샷

지금까지

  • 버전 관리 시스템으로 Git을 사용하기 시작했다.
  • Git 웹호스팅 서비스로 GitHub를 사용하기 시작했다.
  • GitHub Issues와 Projects로 이슈 트래킹을 시작했다.
  • 개발 프로세스와 브랜칭 모델을 정립했다.
  • HTTP API 아키텍처 기반으로 API 스펙을 디자인하기로 했다.
  • JSON을 직렬화 포맷으로 결정했다.
  • Authorization 헤더로 인증 정보를 명시하기로 했다.
  • 인증 스키마에 JWT 기반의 Bearer를 사용하기로 했다.
  • API 스펙을 정의했다.
  • GitBook으로 API를 문서화했다.
  • Python + Sanic 조합으로 WAS를 개발하기로 했다.


집계 함수와 조건(LIMIT, WHERE 등)을 함께 사용하고 싶을 때가 있다. '소득'을 다루는 테이블에서 '상위 1만 명의 소득의 평균'을 집계하거나, '집값'을 다루는 테이블에서 '대전광역시에서 가장 비싼 원룸' 등을 집계하는 일이다. 일차원적으로 생각하면 '굉장히 쉽게 쿼리할 수 있지 않을까' 싶겠지만, 결과를 보면 갸우뚱하게 된다.

아무튼 결과는 나오지만, 사실 이는 원하는 결과가 아닐 가능성이 높다. 위 쿼리는 'tbl_people_incomes에서 AVG(income)을 계산한 결과를 income으로 내림차순 정렬한 후 LIMIT(10000)'을 의미한다. 이미 SELECT AVG(income) FROM tbl_people_incomes로 인해 만들어진 하나의 row를 정렬하고 잘라내는, 효과 없는 쿼리다. 집계 함수와 조건식을 함께 사용하려면, FROM의 뒷부분을 써먹어야 한다.

SELECT FROM SELECT절에는 nested(내부) SELECT를 소괄호로 감싸고 aliasing해줘야 한다. MySQL이 아닌 다른 SQL 엔진에서도 동일하게 적용되는지는 모르겠다.

'데이터베이스 > SQL' 카테고리의 다른 글

Literal SELECT  (0) 2019.02.12
GROUP BY  (0) 2019.02.12

이번엔 의존성 관리 도구를 결정하자. pip, npm, yarn, gem, maven, gradle 등과 같은 의존성 관리/빌드 도구를 써본 적 없다면 이해하기 어려울 수 있으니 의존성 관리 도구(Dependency Manager)라는 글을 읽어보자. 프로젝트 한두번 하면서 의존성 관리를 해 본 경험이 있다면 더욱 좋다.

도입 이유

소프트웨어 어플리케이션 개발에는 대부분 라이브러리가 필요하다. 언어 차원에서 지원되는(built-in) 것일 수도 있고, 우리가 사용하기로 한 Sanic처럼 외부 라이브러리 저장소에 의존하는 것일 수도 있다. 프로젝트가 어떤 외부 라이브러리를 사용하고 있는지를 별도로 관리하는 것의존성 관리라고 한다. 프로젝트가 사용하고 있는 외부 라이브러리들을 남들이 알도록 하는 것이 기본적인 목표인데, 이런 의존성 관리가 중요한 이유는 다음과 같다.

  • 먼저, 언어 차원에서 지원되는 라이브러리는 이미 다들 설치되어 있으니 따로 관리할 필요가 없다.
  • 백엔드 팀에서 함께하는 타 개발자의 입장에서 생각해 보자. 의존성이 제대로 설치되어 있지 않으면 어플리케이션을 실행해볼 수 없다. 혼자 개발하는 입장이라면 그냥 필요한 라이브러리들을 컴퓨터에 미리 설치해 두기만 하면 되겠지만, 협업이 힘들어지는 것이 문제다.
  • 오픈 소스로 관리되는 라이브러리의 소스 코드를 그냥 그대로 붙여넣거나, 따로 무슨무슨 라이브러리 쓴다고 문서화해둬도 되기야 하겠지만, 의존성의 의존성과 같은 재귀 의존 현상라이브러리 각각의 버전 업데이트 대응을 별도의 도구 없이 해결하기엔 너무 낭비다. 그 예로 Sanic은 httptools, websockets, multidict 등의 외부 의존성을 가지고 있다. 그리고 라이브러리 한 20개 쓰고 있는 상황에서, 버전 맞춰서 하나하나 설치하는 것도 귀찮은 일이다.
  • 배포 과정에서 필요하다. 예를 들어 테스트와 같은 배포 전처리를 위해 어플리케이션의 의존성들을 모두 다운로드해야 하고, 빌드/패키징 등과 같이 컴파일이 들어가는 과정에서 항상 필요하다.

이름은 거창하지만 의존성 관리 도구들이 자체적으로 많은 부분을 도와주기에, 우리에게 보여지는 겉모습 자체는 별 거 없다. 대부분 어떤 라이브러리의 무슨 버전을 쓸 지 목록화시킨 파일로서 의존성을 관리한다. 아래는 차례대로 Pythonrequirements.txt, JavaScriptyarn, RubyGemfile 예제다.

의사결정

선택지

  • pip와 requirements.txt
  • pipenv

의사결정

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

  • Python은 일반적으로 가상 환경(virtual environment)을 통해 독립된 개발 환경을 구성하게 되는데, pipenv는 가상 환경 구성의존성 관리한번에 해결해주기 때문이다. JavaScript를 해봤다면, npm을 떠올리면 된다.

준비

pipenv란 무엇인가Pipenv으로 Python 프로젝트 관리하기라는 글로 pipenv에 입문하자.

작업

pipenv를 통해 프로젝트 디렉토리에 pipenv를 초기화하고, 가상 환경에 들어가 Sanic을 설치한 후 lock하자.

lock 직후의 스냅샷

+ Recent posts