/home/caml-shaving

올해의 삽질들

끝이 없다

2020-12-10

태그: dev life

2020 한해의 회고… 라기엔 너무 거창하고, 그냥 올해 했던 기억에 남는 삽질들을 몇 개 기록해보려고 한다.

장고와 셀러리

연구개발로 만든 엔진을 사내에 서비스하려니 자연스럽게 백엔드를 개발 하게 되었다. 뭘로 백엔드를 짜야 가성비가 쩔게 나와줄지, 즉 노력 대비 생산성이 최대를 찍을지 꽤 고민한 결과, 파이썬의 Django (이하 장고) 프레임워크 위에 올라타기로 했다. 워낙 유명한 프레임워크라서 이미 많은 사람들이 여러가지 장단점을 정리해뒀으니, 여기서는 내가 선택한 이유만 기록해 보자면:

  1. (그나마) 파이썬이다. 웹, 특히 백엔드를 하려면 크게 자바(스프링)와 자바스크립트(노드)가 두 축인데, 개인적인 취향으로는 두 언어 모두 마음에 들지 않아서 이걸로 개발했다간 코딩이 즐겁지 않을 것 같다. 최애 언어로 개발하고 싶지만 그랬다간 이 레거시를 누가 감당하게 될지 모르고 무엇보다 아직까지 OCaml의 웹 백엔드는 성숙하지 못하니, 그나마 파이썬이 괜찮은 선택인 것 같다.
  2. 장고의 철학인 “Batteries included”, 즉 “니가 필요한 웬만한 건 우리가 다 지원해줄게” 라는 말이 내 마음을 편하게 해준다. 반면 비슷한 파이썬 웹 프레임워크인 Flask 는 미니멈을 추구하는 것이 철학이라, 예를 들면 ORM 같은 걸 디폴트로 제공해주지 않고 외부 라이브러리에 의존하는 것 같다. 이런걸 고민하는 재미도 있지만, 이번에는 뭘 쓸지 고민하는 것보다는 프레임워크가 제공해주는 걸 편하게 쓰고 싶다.
  3. 관리자 페이지가 기본으로 만들어진다. 이게 정말 강력한 기능이다. 따로 UI 고민할 필요 없이 모델에 대한 어드민 뷰 페이지를 알아서 뽑아주고 계정 별 권한 컨트롤까지 제공해주는데, 이것 덕분에 엄청난 시간 절약을 할 수 있었다.
  4. 파이썬 웹 프레임워크 중 가장 메이저라고 할 수 있다. 덕분에 프로젝트가 갑자기 사라지거나 (…) 혹은 에러를 겪었을 때 혼자 고립될 걱정을 덜 해도 된다. 장고 소프트웨어 파운데이션은 비영리 재단의 지원을 받긴 하지만, 장고로 만들어진 무수한 메이저 서비스들이 장고의 단단함을 대신 말해준다1: 디스커스, 인스타그램, 스포티파이, …

순수 장고 프레임워크만으로 여기까지는 다 좋았다. 하지만 GIL로 인해 태생적으로 Sequential 한 파이썬과, 웹을 위한 프레임워크인 장고의 한계로 인해 내가 원하는 만큼의 성능을 뽑아내긴 힘들어보였다.

그래서 처음에는 요청마다 직접 multiprocessing 으로 fork해서 엔진을 돌리려고 했다. 그런데 나도 생각한 걸 다른 사람들이 생각하지 않을리가 없다고 생각해서 좀 검색해보니, 과연 Celery (이하 셀러리) 라는 좋은 게 있더라. Redis 같은 인메모리 캐시를 미들웨어로 활용하는 분산 작업 큐이다. 그리고 무엇보다 장고와의 통합을 나이스하게 지원한다.

덕분에 아주 가성비 좋은 서비스 백엔드를 만들 수 있었다. 뼈대는 장고+셀러리로, 물론 지금은 여기에 여러가지가 추가되어서 좀더 커지긴 했지만, 기본적으로 내가 원했던 “최소한의 노력으로 최대한의 생산성을 잡아낼 수 있는 분산 백엔드”를 달성할 수 있었다.

이제 다음에는 그 유명한 리액트나 Next.js 같은 물건을 건드릴 날이 오려나. 온다면 최대한 천천히 찾아 오길.

스크립트: OCaml, 파이썬, 쉘

반복적인 실험을 위해서는 스크립팅이 필수적이다. 스크립트 짤 때만이라도 최애 언어 OCaml을 굴려보려고 다방면의 시도를 해보았지만, 결국 파이썬과 순수 쉘 스크립트의 조합이 가장 생산성이 좋은 것 같다. JaneStreet 쯤은 가야 모든 스크립트가 OCaml로 짜여져 있는 그런 환경을 겪어볼 수 있을까? 아무래도 이번 생에는 무리인 것 같다.

OCaml

OCaml은 바이트코드 인터프리터도 딸려 오기 때문에 사용법은 여타 쉘 스크립트와 크게 다르지 않다. 파일의 시작 부분에 Shebang으로 OCaml 인터프리터로 해석하라고 알려주고 평소처럼 짜면 된다.

좀 사용해본 결과, OCaml로 짠 스크립트의 성능 자체는 굉장히 만족스러웠다. 인터프리터 자체도 파이썬보다 훨씬 빠르고, 무엇보다 타입 안전해서 파이썬으로 짤 때 발생하는 익셉션의 대부분을 사전에 예방할 수 있었다. 대신 코드를 짤 때 조금 더 심사숙고 해야 하는 부분이 있어서 파이썬이나 쉘처럼 후다닥 실행해보고 디버깅하진 못하지만, 타입 덕분에 최종 생산 속도는 비슷하거나 더 빠른 느낌이다.

스크립트의 성능은 마음에 들지만, 문제는 Portability (이식성?) 이다. 로컬에서 쓰는 건 아무 문제 없는데, 사실 대부분 스크립트가 필요한 곳은 컨테이너 속이었다. 그런데 이런 경우, 일단 200MB 정도 되는 용량의 OCaml 런타임을 새로 깔아야 한다는 것, 그래서 인터넷이 안되거나 속도가 느린 환경에서는 설치부터 느리다는 것, 그리고 이렇게 깔린 버전이 OS(이미지)마다 조금씩 차이나서 라이브러리 사용에 주의해야 한다는 것(이건 파이썬도 일정 부분 공유하는 문제이긴 하다), 그리고 외부 패키지를 쓰려면 opam을 깔아야 하는데, opam을 깔고, switch로 컴파일러를 컴파일하고, 버전에 맞는 패키지를 설치하고… 이 과정이 제법 비싸다는 것 등, 여러가지가 이식성의 발목을 잡더라. python3python3-pip 설치한 후에 pip install -r requirements.txt 한줄이면 런타임부터 패키지까지 다 설치 가능한 파이썬과는 사뭇 달랐다.

결국 OCaml 스크립트는 쓸만한 물건이지만, 들고 다니기 힘들기 때문에 로컬에서만 적당히 사용하기로 했다. 나만 쓰는 물건 하나 정도는 있어도 괜찮겠지.

파이썬

파이썬은 대부분의 리눅스 배포판에 기본으로 깔려있다는 장점이 있다. 그리고 패키지 설치 과정이 꽤 가볍고 pyenv 같은걸로 opam switch를 흉내낼 수도 있다. 물론 이것도 훨씬 가볍다.

그리고 웬만한 작업은 표준 라이브러리를 이용해서 몇 줄 안되는 코드로 가능한 점이 가장 만족스러웠다. 파일 입출력, 정규식, 파일 시스템 탐색, HTTP 요청, fork 기반 병렬처리까지. 그리고 OCaml의 utop에 해당하는 ipython과, 그 위에서 실행 가능한 장고 쉘 덕분에 DB에 직접 쿼리를 날리지 않고 장고 ORM 으로 간단한 내용은 확인 해볼 수 있다는 점도 좋았다. 다만 타입이 없어서, 코너 케이스를 잘 생각하지 않으면 생각보다 자주 예외가 발생한다는 점은 아쉬웠다.

예전에 트위터의 한 개발자 분이 했던 비유 중에 “파이썬은 프로그래밍 언어 계의 WD-40 이다” 라는 게 아직까지 기억에 남아있는데, 정말 그런 것 같다. 물론 요즘이야 하드웨어 성능이 좋아져서 파이썬으로도 일정 수준의 프로덕션이 가능하긴 하지만, 파이썬의 진가는 말 그대로 WD-40처럼 여러가지 복잡한 작업을 매끄럽게 이어주는 것이라고 생각한다.

결론적으로 파이썬은 여기저기 아무렇게나 갖다 쓸 수 있는 점이 너무 강력해서, 항상 고맙게 잘 사용하고 있다. 아마도 당분간은 이게 주된 밥벌이 수단이지 않을까.

튜닝의 끝은 역시 순정이다. 파이썬이 아무리 가볍고 좋다고 해도, 파이썬 파일을 만들고, 스크립트를 짜고, 인터프리터를 호출하는 과정마저도 번거로울 때가 있다. 특히 몇 가지 커맨드를 파이프라이닝하거나 특정 커맨드의 결과를 리다이렉션하거나 하는 경우에는 파이썬의 subprocess 를 사용하는 게 오히려 번거롭다.

이럴 때는 그냥 순정 쉘 스크립트를 쓰는 것이 직빵이다. 특히 내 삶을 윤택하게 만들어줬던 몇 가지 원라이너를 소개한다.

for f in path/*.ext; do ...; read var; done
[ -f path ] && ....
find . -name "*.ext" -print0 | xargs -0 ...
rm -rf * && git checkout -- .

무의식적으로 사용하고 있는 것들은 이것보다 더 많긴 하다. 그리고 사실 이런 원라이너보다 중요한건 무조건 fzf를 써야한다는 것이다. 중요하니 한번 더 말해야지. 무조건 fzf를 써야한다. fzf는 쉘 스크립팅의 빛이요 지혜의 광명이다.

도커 길들이기

전에도 말한 적 있는 것 같은데, 이제 도커 없이는 살 수 없는 몸이 되었다. 그런데 이 도커를 길들이기가 꽤 까다로웠다. 삽질했던 두 가지를 기록해둔다.

용량 길들이기

실험을 위해서 이미지를 받고 컨테이너를 띄우다 보면 용량 잡아먹는 속도가 어마무시하다. 도커는 기본적으로 /var/lib/docker 에서 이미지나 볼륨, 컨테이너 파일을 관리하기 때문에 애초에 /var 를 용량이 큰 하드에 마운트한 경우가 아니라면 금새 다 차버린다.

이럴 때는 용량 큰 볼륨을 /var 에다가 마운트하는 것도 괜찮은 방법이지만, 아예 도커 전용 볼륨을 따로 관리하는 것도 괜찮더라. 방법은,

  1. 일단 서비스를 멈춘다: service docker stop
  2. 용량 큰 볼륨 마운트한 곳에다가 도커 디렉토리를 통째로 옮긴다: mv /var/lib/docker /huge-volume
  3. 데몬 설정을 열어서 "data-root": "/huge-volume"을 추가해준다. 없으면 새로 만들어준다: vi /etc/docker/daemon.json

  4. 재시작 한다: service docker start

덕분에 /var 에 볼륨 마운트를 위해 새로 포맷하거나2 하지 않고 도커 디렉토리만 큰 곳으로 옮겨도 되고, 무엇보다 기존 도커 설정이 그대로 남아있어서 떠있던 도커 컨테이너나 풀 받아둔 이미지가 그대로 있다. 덕분에 실험 서버를 포맷하거나 도커를 재설치하는 삽질을 피할 수 있었다.

도커 안에서 도커 사용하기

한 컨테이너에서 모든 작업이 끝나면 좋겠지만, 그렇지 않은 경우도 있다. 예를 들면 어떤 프로그램은 계정마다3 단 하나의 프로세스만 허용하기도 한다 (이런 프로그램을 싱글톤 프로그램이라고 부르자). 이런 프로그램을 사용하는 경우, 들어온 요청끼리 프로그램 사용을 위해서 서로 경쟁하도록 락을 잡아도 되지만, 애초에 외부 프로그램이라 한 요청의 실행 시간이 꽤 길어서 생각없이 락 걸었다가는 온통 타임아웃 나기 십상이다.

이럴 때는 도커 안에서 도커를 활용하는게 가장 좋다. 그렇다고 도커 컨테이너 안에서 도커를 설치해서 사용하는 일은 불가능하기도 하고 미련한 짓이다. 도커 데몬을 띄울려면 1번 프로세스 init이 필요한데 컨테이너에서는 보통 첫 시작인 /bin/bash가 1번 프로세스가 되기 때문에 불가능하고, 이렇게 새로 도커 환경을 꾸려버리면 기존 호스트에 남아있던 각종 설정들, 예를 들면 캐싱해둔 도커 이미지, 도커 로그인 세션들, 등을 재활용할 수 없어 엄청난 낭비를 하기 때문에 미련한 짓이다.

그래서 내가 찾은 방법은 가장 처음 뜨는 컨테이너에서 호스트의 도커 설정을 마운트해서 먹고 들어가는 것이다. 아래 세 가지를 필수적으로 마운트 해주면 된다:

여기까지 들고 들어가면 컨테이너 안에서 별도의 설치나 설정 없이 docker psdocker run 같은 도커 커맨드를 마치 호스트에서 실행하는 것처럼 사용할 수 있다.

여기서 추가로, 컨테이너들끼리 통신을 원활하게 하려면 경험 상 /tmp 디렉토리까지 먹고 들어가는게 좋더라. 특히 커맨드라인 옵션 말고 파일을 입력으로 주고 싶다거나, 혹은 콘솔 몇 줄로 간단하게 결과를 뿌리는 게 아니라 수십/수백메가의 파일로 그 결과물을 떨어뜨리는 프로그램을 컨테이너로 실행하고 싶을 때는 /tmp 에서 약속 장소를 잡고 서로 만나면 쉽게 얘기를 나눌 수 있다.

단, 주의사항은 호스트의 경로와 완전히 똑같은 경로를 마운트해줘야 한다는 것이다. 그리고 권한 오류가 발생할 수 있기 때문에 컨테이너를 priviledged로 실행하는 것이 편하다.

빌드 재현하기

아무리 좋은 기술을 개발했어도 리얼월드에 적용하지 못하면 빛 좋은 개살구일 뿐이다. 특히 C/C++ 프로젝트에서 뭔가를 분석하려면, “빌드” 라는 큰 산을 반드시 넘어야 한다. 그런데 이 산은 정말 넘기 힘들다. 왜냐하면 프로젝트마다 컴파일 환경도 제각각이고4 , 빌드 시스템도 제각각이고, 심지어 브랜치마다 빌드 정책이 다르기도 하다.

특히 지금 구상하고 있는 서비스는 PR의 head 정보를 가지고 분석을 시도하는데, 이게 험준한 빌드의 산과 섞이면서 별 희한한 이유로 빌드 자체를 재현하기가 힘든 경우가 있었다. 예를 들면,

… 등 그 외에도 온갖 다양한 이슈로 분석은 커녕 빌드도 못하고 주저 앉는 경우가 많았다. 이런 경우 하나하나를 땜질해가면서 어떻게든 fully-automated 분석 시스템을 만들고 있는데, 문득 예전에 교수님께서 말씀하셨던 “양 손에 피를 묻히는 작업”이 과연 이런 것인가, 생각하게 된다.


적고 보니 결국 모든 삽질의 목표는 “최소한의 노력으로 최대한의 생산성 달성하기” 였던 것 같다. 어떻게든 일을 덜 하려고 했던 노력이 아이러니 하게도 적당한 수준의 생산성을 가져다주는 것 같다.

내년엔 또 어떤 삽질을 하게 될까? 적어도 여기서 했던 삽질들은 하지 않겠다는 심정으로 글을 마친다.


  1. 출처. 2018년 글이라 지금은 많이 다를수도. 

  2. 포맷 하지 않고도 옮길 수 있나? 안해봐서 잘 모르겠지만 어려울 것 같다. 

  3. 정확히는 init 프로세스마다 

  4. 이건 그나마 도커가 나오면서 일정 부분 해결되었다.