조유성
Software Engineer Server Developer Frontend Developer UI Developer Full Stack Developer Developer OSS Lover 철학 전공자 구조주의자 직장인 ù̴̲̭̼n̴̡͔͍̏d̶̛͇̖̻̅̕e̴̬͇͖̊f̸̢͈͂͒ǐ̶̺̳ͅn̴̝̣̹͂͌e̸̟̯̒d̵̢̉ͅ

Apple Silicon에 Python 3.7 설치하기

TL;DR

# Install Homebrew (x86_64)
sudo mkdir -p /opt/homebrew-x86_64
chown "$USER" /opt/homebrew-x86_64
curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C /opt/homebrew-x86_64

# Install Python 3.7
arch -x86_64 /opt/homebrew-x86_64/bin/brew install python@3.7

# Install pyenv-register (optional)
git clone https://github.com/doloopwhile/pyenv-register.git $(pyenv root)/plugins/pyenv-register
exec "$SHELL"

# Register Python 3.7 to pyenv (optional)
/opt/homebrew-x86_64/opt/python@3.7/bin/python3 -m pip install virtualenv
pyenv register /opt/homebrew-x86_64/opt/python@3.7/bin/python3

지난 주에는 업무용 노트북을 바꿨습니다.

사실 개인적으로는 Apple의 전자제품 전반에 별로 관심이 없어서 글을 작성하면서 정보 창을 열어보니, 2020년형 13인치 M1 버전이라고 하네요.

이 Mac에 관하여

꽤 만족스럽습니다. 프로그램 실행 속도가 전반적으로 조금씩 빨라진 것 같은 느낌이고 (측정해 보지는 않았습니다), 무엇보다도 이전에 쓰던 버터플라이 키보드에 비해서 키보드의 내구성이 개선되어서 너무 다행입니다. 사실 말도 많고탈도 많은 버터플라이 키보드의 키감에 대해서는 특별한 불만은 없었고, 굳이 따지자면 오히려 좋아하는 편에 가까웠습니다. 하지만 제가 물건을 워낙 험하게 쓰는 편이다보니 기존의 키보드는 이미 이것저것 눌러 붙어버려서 외장 키보드 없이는 개발을 할 수 없는 지경이었습니다.

새 노트북을 받아서 이것저것 설정을 하면서 가장 많은 시간을 쓴 일은 Python 3.7을 설치하는 것이었습니다. 여기에 삽질을 하면서 알게된 설치 방법을 정리해보려고 합니다.

개인적으로는 블로그에 이런 내용의 글은 잘 적지는 않는 편입니다. 제가 재직 중인 회사의 엔지니어들이 모두 그렇듯이 잘 시간이 나지 않기도하고, 도메인으로부터 독립적인 일반적인 설계에 관해서는 이미 좋은 글이 많다고 생각하기 때문이에요.

그런데 Apple Silicon에 Python 3.7을 설치하는 방법을 Google에 검색을 해보면 도움이 되는 정보가 이것저것 있긴하지만, 이렇다 할 잘 정리된 가이드는 없기도 하고, 특히 C 라이브러리에 의존성이 있는 mysqlclient, confluent-kafka-python 등은 설치 가이드가 전무했습니다.

결론부터 말하자면…

결론부터 말하자면, arm64 아키텍처 네이티브로 Python 3.7을 쓸 수 있는 방법은 없습니다.

없습니다

제가 해보니, C 라이브러리 등의 네이티브 확장을 전혀 사용하지 않고 pure Python 라이브러리만 사용한다면 CPython 3.7을 직접 빌드해서 사용하는 것은 가능합니다. 다만 그것도 빌드가 가능하다 뿐이지, 공식적으로는 지원되는 것은 아닙니다.

참고로 글을 작성하는 21년 9월 현재 PyPy도 Apple Silicon을 지원하지 않습니다. arm64 Linux는 지원하고 있지만, arm64 macOS 지원을 위한 개발자와 후원자를 모집하고 있습니다.

아무튼, Python 3.7과 네이티브 확장을 사용하고 싶다면, 둘 모두 x86_64 바이너리로 설치해서 Rosetta로 에뮬레이션 해야합니다.

Homebrew (x86_64) 설치

먼저 Python부터 설치해 봅시다. 여러가지 방법이 있겠지만 Homebrew를 통해서 설치하고, 프로젝트 별로 virtualenv를 사용하는 방법을 추천합니다. CPython 하나만 설치한다면 pyenv 등등의 도움을 받아서 빌드하는 것도 방법이겠지만, 어차피 네이티브 확장도 빌드해야 하는 걸 생각하면 Homebrew를 쓰는 편이 여러모로 편리하기 때문입니다. 직접 최신 소스코드를 설치하는 게 취향인 분도 있을 수 있지만 그런 분들은 이 글을 읽고 있지 않을 것 같아요.

# x86_64 Homebrew가 설치될 위치입니다.
# arm64와 겹치지 않도록 설정해주세요.
sudo mkdir -p /opt/homebrew-x86_64

# 현재 로그인한 사용자가 write 할 수 있도록
# 적당한 권한을 부여해주세요.
chown "$USER" /opt/homebrew-x86_64

# Homebrew를 설치합니다.
curl -L https://github.com/Homebrew/brew/tarball/master | \
  tar xz --strip 1 -C /opt/homebrew-x86_64

설치가 완료되었습니다. 이제 아래 명령을 실행하면 x86_64 아키텍처용 Homebrew가 실행됩니다.

arch -x86_64 /opt/homebrew-x86_64/bin/brew

매번 이렇게 입력하는 건 귀찮으니 alias를 등록합시다.

echo 'alias brew-x86_64="arch -x86_64 /opt/homebrew-x86_64/bin/brew"' \
  >> .zshrc  # 또는 자신이 사용 중인 shell의 환경 파일

arch 명령에 관해

참고로 arch는 macOS에 포함된 실행 파일인데요, 아무 인자 없이 실행하면 현재 아키텍처를 표시하지만, 추가적인 인자들을 주면 바이너리를 특정 아키텍처 환경에서 실행해줍니다. 아래는 man arch의 내용입니다.

man arch

문서의 내용에 따르면, arch 명령은 마법처럼 서로 다른 아키텍처의 코드를 실행할 수 있게 해주는 건 아닙니다. (아마 그렇게 하려면 QEMU 같은 하이퍼바이저가 필요했을 거에요.)

이 명령은 기본적으로 하나 이상의 아키텍처에서 실행할 수 있도록 여러 버전의 머신 코드가 포함되어 있는 유니버설 바이너리에서 현재 머신의 아키텍처와 일치하는 버전을 골라서 실행합니다.

일치하는 버전이 존재하지 않거나, 우리가 위에서 한 것처럼 강제로 현재 머신과 다른 아키텍처를 지정한 경우에는 Rosetta를 사용하게 됩니다. Apple Silicon 기반 macOS에 포함된 Rosetta 2는 x86_64 머신코드를 arm64 머신코드로 번역하는 일종의 에뮬레이터입니다. 인터넷에 떠도는 정보들을 살펴보면 Apple은 Rosetta 2의 성능을 높이기 위해서 여러가지 수단을 동원한 것 같습니다. JIT/AOT 최적화를 도입하기도 했고, 심지어 하드웨어 레벨에서 Intel CPU의 작동 구조를 흉내내는 ‘꼼수’도 동원했다고 하네요.

arch 명령이 지원하는 아키텍처 목록은 아래와 같습니다:

Python 3.7 설치

이제 Python 3.7을 설치하는 건 쉽습니다.

brew-x86_64 install python@3.7

설치된 Python 3.7 (x86_64)은 아래 명령으로 실행할 수 있습니다.

/opt/homebrew-x86_64/opt/python@3.7/bin/python3

개발 환경에서 새로운 virtualenv를 만들고 싶다면 이렇게 하면 되겠죠?

/opt/homebrew-x86_64/opt/python@3.7/bin/python3 \
  -m venv venv

pyenv (arm64)에 Python (x86_64) 등록하기

물론 매번 /opt/...로 시작하는 경로를 입력할 수는 없습니다. python3.7에 대해서 alias를 등록하거나 symlink를 생성하는 것도 방법입니다.

저는 개인적으로 모든 Python 환경을 pyenv로 관리하는 것을 선호합니다. 하지만 안타깝게도 위에서 설명한 방법으로 설치한 Python은 pyenv가 인식하지를 않습니다. $(pyenv root)/versions에 설치하지 않아서 그렇습니다.

물론 다 방법이 있습니다. pyenv-register라는 플러그인을 설치하면 됩니다. (GitHub 저장소가 archive 상태인데, 사실 되게 간단한 셸 스크립트라서 특별히 유지보수가 필요할 것 같지도 않습니다.)

git clone https://github.com/doloopwhile/pyenv-register.git $(pyenv root)/plugins/pyenv-register

터미널을 껐다 켜거나 실행 중인 셸을 재시작하면 아래 명령을 실행해줍니다:

# wget이 설치되어 있지 않으면 오류가 납니다.
# pyenv-register는 virtualenv가 설치되어 있지 않으면
# wget을 통해 설치하려고 시도하기 때문입니다.
# 아래 명령으로 미리 설치해주면 됩니다.
/opt/homebrew-x86_64/opt/python@3.7/bin/python3 \
  -m pip install virtualenv

pyenv register /opt/homebrew-x86_64/opt/python@3.7/bin/python3

이제 pyenv에 Python 3.7 (x86_64)이 등록되었습니다.

$ pyenv versions
* system
  system-3.7.11

평범하게 다른 환경처럼 사용할 수 있습니다.

pyenv virtualenv system-3.7.11 some-project
pyenv local some-project

mysqlclient 설치해보기

이제 네이티브 확장을 사용하는 라이브러리를 설치해봅시다. 여기에서는 mysqlclient를 예시로 사용하려고 하지만, 다른 라이브러리도 요지는 동일합니다. 먼저 brew (x86_64)로 의존성을 설치하고, 적절한 환경변수와 함께 pip를 실행하면 됩니다.

mysqlclient README에 따르면 macOS에서는 다음과 같이 설치하라고 하는데요,

# mysqlclient README가 제안하는 방법
brew install mysql-client
echo 'export PATH="/usr/local/opt/mysql-client/bin:$PATH"' \
  >> ~/.bash_profile
export PATH="/usr/local/opt/mysql-client/bin:$PATH"
pip install mysqlclient

여기에서 우리는 brew 대신 brew-x86_64를 사용해야 하기도 하지만, 저는 개인적으로 셸 스크립트에 이런저런 내용이 추가되는 것을 별로 좋아하지 않아서 아래와 같이 했습니다:

brew-x86_64 install mysql-client
PATH="/opt/homebrew-x86_64/opt/mysql-client/bin:$PATH" \
  LDFLAGS="-L/opt/homebrew-x86_64/opt/mysql-client/lib" \
  CPPFLAGS="-I/opt/homebrew-x86_64/opt/mysql-client/include" \
  pip install mysqlclient

confluent-kafka-python도 비슷합니다.

brew-x86_64 install librdkafka
LDFLAGS="-L/opt/homebrew-x86_64/opt/librdkafka/lib" \
  CPPFLAGS="-I/opt/homebrew-x86_64/opt/librdkafka/include" \
  pip install confluent-kafka

나가며

새 Mac에 Python을 설치할 때면 언제나 애를 먹습니다. 요즘은 회사에서 프론트엔드 개발을 주로 하고 있어서 그렇게 자주 필요한 것은 아니지만, 여전히 종종 다른 개발자의 업무를 도와주거나 코드 리뷰를 하기 위해서 필요합니다.

대부분의 Linux 배포판에서는

sudo pacman -S --needed base-devel openssl zlib xz
pyenv install 3.7.11

또는 이와 비슷한 명령 몇 줄이면 되는 일을 macOS에서 매번

순서대로 설치하고 있자면 참 가슴이 답답합니다. 그냥 설치가 되기라도 하면 모르겠는데, 깨끗한 환경이 아니라 뭔가 OS 업데이트라도 한 상황이면 과거 버전의 SDK를 지우고 재부팅 하고 어쩌구 저쩌구 하다보면 참..

차라리 그냥 싹 다 Docker로 올려버릴까 싶다가도 IDE에서 디버거라도 걸려고 치면 그것도 여의치가 않습니다.

아무튼 macOS에서 Python 설치는 안 그래도 복잡하고 오래 걸리는 작업이었는데, M1으로 오면서 어째 더 불편해진 것 같아서 마음이 아플 따름입니다.

도움 받은 문서