무스마와 파이썬

파이썬은 개발자는 물론 비개발자 그룹에서도 두루 사용하는, 세계적으로 인기 있는 프로그래밍 언어 중의 하나입니다. 서점을 가봐도 파이썬 책이 한 칸을 다 차지하고 있을 정도로(기본서, 데이터과학, 머신러닝, 증권매매(?), …) 많이 사용하고 있지요. 우리 무스마에서도 인공지능(AI)을 연구하고 개발하는 R&D팀에서 파이썬을 많이 사용하고 있습니다. Node.js 플랫폼을 주로 사용하는 SW 개발팀과는 달리, R(Research)이 강한 R&D팀에서는 데이터 과학과 인공지능 개발에 특화된 장점이 있는 파이썬을 더 선호하고 있습니다.

글을 쓴 이유

그동안 연구 개발 과제를 수행할 때, 데이터를 수집하는 서버를 SW 개발팀의 지원을 받아 Node.js로 개발했습니다. 뭐 그리 대단한 것은 아니고, 데이터를 수집하고 잘 저장만 하면 되는, 대개 한 번 쓰고 버리는 그런 것들이었죠. 앞으로 R&D팀에서 자체적으로 서버를 구현할 수 있으면 과제를 수행하는데 더 수월하지 않을까 하고 생각하게 되었는데요. 이 기회에 SW 개발팀에 그동안 축적된 개발 기술을 R&D팀과 공유할 필요도 느끼게 되었습니다. SW 개발팀 개발자들이 파이썬 언어와 그 에코시스템에 관해 어느 정도 감을 잡아놓으면 나중에 R&D팀과 SW개발팀이 협업하는데 분명 도움이 될 것이라고 생각합니다.

이 포스팅에서 Node.js 개발자 입장에서 파이썬을 살짝 맛만 보도록 하겠습니다.



개발 환경 준비

SW 개발팀에서 주로 사용하는 macOS 환경을 기준으로 설명합니다.

잘 몰라도, 일단 묻지 말고 그냥 설치합니다. (…)


pyenv 설치

비유하자면, Node의 nvm 같은 도구입니다. 여러 버전의 파이썬을 설치해두고 전환할 수 있습니다.

$ brew install pyenv
$ brew install pyenv-virtualenv


파이썬 설치

nvm을 사용하는 방법과 비슷하게, 특정 버전의 파이썬 인터프리터를 설치합니다.

// 설치할 수 있는 배포판 목록
$ pyenv install --list

$ pyenv install 3.8.10

python-build: use openssl@1.1 from homebrew
python-build: use readline from homebrew
Downloading Python-3.8.10.tar.xz...
-> https://www.python.org/ftp/python/3.8.10/Python-3.8.10.tar.xz
Installing Python-3.8.10...
python-build: use readline from homebrew
python-build: use zlib from xcode sdk

Installed Python-3.8.10 to /Users/civilizeddev/.pyenv/versions/3.8.10


파이썬 가상 환경 생성

파이썬을 개발할 때는 가상 환경(venv)을 사용합니다. 비유하자면, 커맨드라인에서 python을 실행 했을 때 어떤 버전의 파이썬 인터프리터를 가리키게 되는가를 나타내는 일종의 컨텍스트(context)라고 생각할 수 있습니다.

nvm에서는 nvm install 16으로 Node v16.x을 설치 한 뒤, nvm use 16 으로 현재 Node 컨텍스트를 16 버전으로 전환하기 때문에 특정 Node 버전이 곧 환경이 됩니다.

하지만 파이썬에서는 인터프리터 버전과 venv로 한 단계 더 나눠서 구분합니다.

// python 3.8.10 버전을 사용하는 'python-3.8.10' 라는 이름의 venv 생성
$ pyenv virtualenv 3.8.10 python-3.8.10

Looking in links: /var/folders/tn/_zl806dn0257ck7_r4jl1hlr0000gn/T/tmpk510rqfv
Requirement already satisfied: setuptools in /Users/civilizeddev/.pyenv/versions/3.8.10/envs/python-3.8.10/lib/python3.8/site-packages (56.0.0)
Requirement already satisfied: pip in /Users/civilizeddev/.pyenv/versions/3.8.10/envs/python-3.8.10/lib/python3.8/site-packages (21.1.1)

// 같은 버전(3.8.10)을 가리키는 또 별도의 venv도 생성 가능
$ pyenv virtualenv 3.8.10 python-3.8.10-backup


IDE 혹은 에디터 설치

Node.js로 개발할 때는 물론 VSCode가 가성비 최고입니다.

그건 파이썬의 경우도 마찬가지입니다. VSCode에 파이썬 확장을 설치하고 개발하시면 문제 없습니다.

혹은 PyCharm이라는 IDE도 있는데, Community 버전은 무료이니 이것을 사용해도 좋습니다.

제 느낌상 PyCharm을 써도 VSCode 보다 크게 나을 것이 별로 없습니다.


poetry 설치

Node에 npm이 있듯이, Python에는 pip가 있습니다.

그러면 poetry는 또 무엇이냐?

Node 개발자에게 쉽게 설명하자면, yarn과 같은 지위를 가진 도구라고 보면 됩니다.

즉 pip와 poetry는 둘 다 파이썬에서 사용하는 패키지 매니저이지만, pip는 원래부터 많이 사용하던 것이고 poetry는 새로운 도구입니다. 아재 판별기

pip는 requirements.txt 파일을 사용해서 패키지 목록을 관리하고, poetry는 pyproject.tomlpoetry.lock을 생성하고 관리합니다.

Node의 yarn으로 비유하면,

  • pyproject.tomlpackage.json
  • poetry.lockyarn.lock

에 대응합니다.

따라서 poetry를 사용하는 방식이 Node 개발자에게는 더 친숙한 느낌입니다.

  • pip의 requirements.txt는 Node의 package.jsondependencies 부분의 내용만 관리합니다. 패키지 목록과 버전만 관리하지요.
  • poetry의 pyproject.tomlpackage.json의 구성이 모두 들어있습니다. (이름, 버전, 메인테이너, 저장소, 라이선스, 스크립트, 패키지 등등)

이렇게 설치합니다.

$ brew install poetry

딱봐도 poetry를 안 쓸 이유가 없어보입니다.




기본 개념 실습

프로젝트 생성

우선, 저장소를 하나 팝니다.

$ mkdir [디렉터리]
$ cd [디렉터리]
$ git init

그리고 파이썬 프로젝트 저장소용 .gitignore 파일도 받아줍니다. 제발 .idea, build 같은 것 좀 올리지 말아주세요.

$ curl https://github.com/github/gitignore/blob/master/Python.gitignore -o .gitignore

보통 저장소를 생성하면 디렉터리가 아래와 같은 상태입니다.

├─ .gitignore
└─ README.md

무스마 SW 개발팀은 저장소 루트에다가 바로 프로젝트를 올리지 않고, 대신 monorepo (multi projects in a repo) 구조를 많이 사용합니다.

주로 이런 식으로 만들고 있습니다.

├─ packages
│  ├─ project1
│  │  └─ package.json
│  ├─ project2
│  │  └─ package.json
│  └─ ...
├─ package.json
├─ .gitignore
└─ README.md

poetry로 파이썬 프로젝트를 생성할 때도 이와 같이 하면 됩니다.

$ poetry new project1

그럼 이렇게 생성됩니다.

├─ project1
│  ├─ project1
│  │  └─ __init__.py
│  ├─ tests
│  │  ├─ __init__.py
│  │  └─ test_project1.py
│  └─ README.rst
├─ .gitignore
└─ README.md

만약 여러 프로젝트를 projects 디렉터리로 묶고 싶으면 이렇게 하면 됩니다.

$ poetry new projects/project1
$ poetry new projects/project2

그럼 이렇게 생성됩니다.

├─ projects
│  ├─ project1
│  │  ├─ project1
│  │  │  └─ __init__.py
│  │  ├─ tests
│  │  │  ├─ __init__.py
│  │  │  └─ test_project1.py
│     ├─ pyproject.toml
│  │  └─ README.rst
│  └─ project2
│     ├─ project2
│     │  └─ __init__.py
│     ├─ tests
│     │  ├─ __init__.py
│     │  └─ test_project2.py
│     ├─ pyproject.toml
│     └─ README.rst
├─ .gitignore
└─ README.md


아까도 말씀드렸다시피 SW 개발팀은 파이썬을 밥 벌어먹고 살만치 세세하게 공부할 시간이 없습니다. 때문에 위에서 생성된 디렉터리 구조를 보고 그동안 Node.js에서 개발한 경험을 비벼서 직관적으로 매칭할 수 있도록 설명해드리겠습니다.


프로젝트, 모듈, 패키지

위에서 최상위 projects 디렉터리 이름은 어떤 의미가 있는 것이 아니고, 그냥 마음대로 이름을 지으면 됩니다. SW 개발팀에서 하는 대로 packages로 지어도 되지만, 파이썬에서는 패키지가 Node와 다른 의미로 쓰이기 때문에 혼동을 피하는 것이 좋겠습니다. 위 디렉터리 구조를 놓고 봤을 때 Node에서의 명칭과 Python에서의 명칭은 대략 아래와 같이 대응합니다.

  • projects/ 바로 아래의 project1, project
    • Node에서는 패키지라고 부릅니다. (package.json과 1:1)
    • 파이썬에서는 프로젝트라고 부릅니다. (pyproject.toml과 1:1)
  • projects/project1/, projects/project2/ 바로 아래에 있는, 프로젝트와 이름이 같은 project1, project2
    • Node에서는 하나의 *.js(ts) 파일도 모듈이고, 또 디렉터리와 index.js(ts)를 합쳐서 하나의 모듈입니다.
    • 파이썬에서는 디렉터리와 __init__.py 를 합쳐서 패키지라고 부릅니다.
      • 파이썬 3.3 이후로는 __init__.py 파일이 없는 디렉터리도 패키지로 인식할 수 있다고도 하는데, 그냥 있는게 낫습니다.
    • 기본적으로 프로젝트 이름과 같은 기본 패키지가 생성됩니다.
      • 예: project1 프로젝트 밑에 project1 패키지 생성
      • 만약 프로젝트 이름에 패키지 이름에 쓸 수 없는 문자(‘-‘ 등)가 들어가면 ‘_‘로 바꿔서 생성됩니다.
  • 기타 *.py 파일들
    • Node에서와 마찬가지로 개별 소스코드 파일은 모듈이라고 부릅니다.
    • 소스코드 본문에서 from xxx import ...에서 xxx 부분은 패키지나 모듈을 지정할 수 있습니다.

import 사용 패턴

파이썬도 Node.js(commonjs 혹은 es6+) 모듈 방식과 비슷합니다.

python es6+
from 패키지 import 모듈 import 모듈 from '패키지/모듈'
from 패키지.모듈 import (모듈 내 요소) import { (모듈 내 요소) } from '패키지/모듈'
import 패키지 import * as 패키지 from '패키지'
import 패키지.모듈 import * as 모듈 from '패키지/모듈'

yarn workspaces와 비교

아직 많이 연구해보지는 않았지만, yarn workspaces 같이 node_modules를 루트에 모아주는 것 같은 기능은 못 찾았습니다.


패키지 추가하기

의존 패키지 관리는 yarn 사용법과 비슷합니다.

공개 패키지를 찾아보고 싶으면 PyPI에서 검색합니다.

이렇게 만들어진 디렉터리 구조가 있다고 합시다.

├─ projects
│  ├─ k8s
│  └─ message-collector
├─ .gitignore
└─ README.md

먼저 k8s 프로젝트에 cdk8s를 추가해보겠습니다.

// 먼저 해당 프로젝트로 이동합니다.
$ cd projects/k8s

$ poetry add cdk8s

보니까 yarn이랑 똑같죠?

poetry로 관리하는 프로젝트는 Node의 package.json과 같이 depedencies와 devDependencies를 별도로 관리할 수 있습니다.

예를 들면,

$ poetry add cdk8s --dev

하면 devDependencies로 추가됩니다.

파이썬에서 널리 쓰이는 웹 프레임워크 Flask를 message-collector 프로젝트에 추가해보겠습니다.

$ cd projects/message-collector

$ poetry add flask


기타 패키지 관리 명령어

yarn과 완전 똑같습니다. 사용법을 더 알아보려면 poetry --help를 치면 됩니다.

패키지 제거

$ poetry remove 패키지

버전을 지정한 패키지 추가

$ poetry add 패키지@^x.y.z

최신 버전으로 업그레이드

$ poetry add 패키지@latest


실행하기

예를 들어, 아래와 같은 디렉터리 구조가 있고 아래 app.py 파일이 애플리케이션의 시작 부분이라면,

└─ message-collector
   └─ message_collector
      └─ __init__.py
      └─ app.py

이렇게 실행하면 됩니다.

$ cd message-collector

$ python message_collector/app.py

Flask를 사용한다면 flask 명령으로도 실행할 수 있습니다.

$ cd message-collector
$ FLASK_APP=message_collector/app poetry run flask run


message-collector 디렉터리 밑에 message_collector 디렉터리?

$ poetry new projects/message-collector

위와 같이 프로젝트를 생성하면 이렇게 만들어집니다.

└─ message-collector
  └─ message_collector
     └─ __init__.py

프로젝트 디렉터리는 message-collector인데 그 아래 패키지 디렉터리는 message_collector 입니다. 관례적으로 기본 패키지의 이름이 프로젝트 이름을 따릅니다.
여기서 프로젝트 이름과 패키지 이름이 달라진 것은, 파이썬 패키지 이름에 -를 쓸 수 없어서 poetry가 자동으로 _로 바꿔서 생성해준 것입니다. 이름에 -이 들어간 프로젝트의 패키지는, 소스코드에서 참조할 때 -_로 바꿔서 입력해야 됩니다.
예를 들어, dependency-injector 패키지를 설치했다고 합시다.

$ poetry add dependency-injector

소스코드에서는 dependency_injector로 import 해야 합니다.

from dependency_injector import containers

주의: 프로젝트 이름

프로젝트 이름에는 보통 -가 들어가는 게 맞습니다. 패키지랑 맞춘다고 일부러 _로 쓸 필요 없습니다.
어색하지만 그냥 그런 걸로 합시다

image


wheel: 배포를 위한 패키징

파이썬 프로젝트는 어떻게 배포하면 될까요? poetry에는 배포를 위한 패키징 기능이 있습니다.

app.py 파일을 간단하게 작성합니다. (GET / 요청에 200 OK로 응답하는 간단한 웹 애플리케이션 서버입니다.)

from flask import Flask

def create_app() -> Flask:
    app = Flask(__name__, static_folder=None)

    return app
$ cd [프로젝트]

$ poetry build -f wheel

이렇게 하면, 프로젝트 아래 dist/ 디렉터리에 패키지-버전-파이썬-ABI-플랫폼.whl 형식의 파일이 생성됩니다.

└─ message-collector
   ├─ dist
   │  └─ message_collector-0.1.0-py3-none-any.whl
   ├─ message_collector
   │  ├─ app.py
   │  └─ __init__.py
   ├─ poetry.lock
   └─ pyproject.toml

위 명령어는 프로젝트를 wheel이라는 형식으로 패키징 합니다.

파이썬은 이전에 py 스크립트 파일을 모아서 압축해서 배포하는 형식으로 사용했습니다. 여러 스크립트 파일을 __main__.py 파일과 함께 묶어놓고 파이썬 인터프리터에 전달하면 알아서 실행되는 그런 구조였습니다. (혹은 그냥 파일을 모두 복사해서 배포하는 방법도 있지요) 이제는 그런 방식도 구식이 되었습니다.

whl 파일을 배포할 곳에 복사해서 가져간 다음,

$ pip install message_collector-0.1.0-py3-none-any.whl

하면 배포 및 설치가 완료됩니다. (참 쉽죠?)

프로젝트에 필요한 의존 패키지도 모두 함께 설치가 됩니다. (인터넷 연결 필요)

그리고 프로덕션 환경에서는 굳이 poetry가 필요 없고 pip로도 wheel을 설치할 수 있습니다.

잘 모르지만 (혹은 잘 모르기 때문에) 앞으로 wheel을 쓰는 것이 바람직하게 생각됩니다.
제가 굳이 파이썬 옛날 방식을 알아서 뭣 하게요


Docker 컨테이너 이미지로 배포하기

무스마 SW개발팀에서는 워크로드를 쿠버네티스 클러스터에 배포하기 때문에 주로 도커 컨테이너 이미지 형식으로 패키징 합니다. 앞으로 R&D팀에서도 쿠버네티스를 사용할 계획이 있어서, 파이썬 애플리케이션을 도커 컨테이너 이미지로 배포하는 방법도 알아놓으면 좋을 것 같습니다.

먼저, Docker Hub에 가서 파이썬 베이스 이미지를 검색합니다. (혹은 pypy를 쓰든지 알아서 고르시고)

Dockerfile을 작성합니다.

$ touch projects/message-collector/Dockerfile
FROM python:3.8.10
WORKDIR /dist
COPY dist/message_collector-0.1.0-py3-none-any.whl /dist/
RUN pip install message_collector-0.1.0-py3-none-any.whl
ENTRYPOINT ["waitress-serve", "--call", "message_collector.app:create_app"]

프로덕션 환경 배포

프로덕션 환경에 배포할 때는 flask run을 사용하지 않습니다.

https://flask.palletsprojects.com/en/2.0.x/tutorial/deploy/

그리고 이미지를 빌드합니다.

$ cd projects/message-collector
$ docker build -t tips-ai/message-collector .

실행해봅니다.

$ docker run --rm -p 8080:8080 tips-ai/message-collector

INFO:waitress:Serving on http://0.0.0.0:8080

요청을 보내봅니다.

$ curl -v http://localhost:8080

...
> HTTP/1.1 200 OK
...

이 정도면 컨테이너 이미지로 배포하는 것은 문제 없겠지요?

이제 Docker Hub에 올리든지, Amazon ECR 올리든지, GitHub Containter Registry에 올리든지 알아서 하시면 됩니다.


정리

Node + TypeScript 개발 경험이 있는 개발자가 파이썬 개발 환경에 빨리 적응할 수 있도록 비슷한 것끼리 맞춰가면서 기본적인 개념을 알아보았습니다.

  • 파이썬 설치
  • 파이썬 가상 환경 설정
  • 파이썬 패키지 관리자 poetry 사용법
  • 배포용 wheel 형식으로 패키징 하기
  • Docker 컨테이너 이미지 빌드하기

다음 Node.js 개발자를 위한 Python 급하게 배워보기 - (2/3) 편에서 계속됩니다.