프로젝트를 진행하는 과정에서, 파이썬 프로젝트의 내부 비즈니스 로직을 숨기고자 하는 요구사항이 생겼다.
- 파이썬 프로젝트는 도커 이미지로 빌드하더라도, 그 내부에 프로젝트 디렉토리 구조와 소스코드가 그대로 유지된다.
- 자바 프로젝트는 .jar 파일 프로젝트를 gradle 빌드하고, 이를 도커 이미지로 만드는 방식인데
.jar 내부에 컴파일된 파일들이 있어 원본 소스 코드를 바로 확인하기는 어렵지만, 이 또한 디컴파일하면 확인할 수 있다.
이를 해결하기 위해, 파이썬 코드 난독화 방법들을 리서치했고 Cython과 Nuitka 라이브러리를 알게 되었다.
이번 글에서는 각 라이브러리가 어떤 방식으로 동작하는지 큰 그림에서 이해한 내용을 정리하고, 실제 적용을 어떻게 했는지 기록하고자 한다.
1. Cython
Cython을 사용할 경우 기존 `.py` python 코드들을 C 코드로 변환하고, C 컴파일러로 빌드해서 `.so` 바이너리 파일을 만든다.
간단히 흐름으로 표시를 하면 `.py` → `.c` → `.so` 로 표현할 수 있다.
이 때 만들어진 `.c` 파일은 파이썬 코드와 동등한 로직을 가진 순수한 C 코드가 아니라 CPython의 내부 함수들을 호출하는 C 코드라고 한다.
이 때 CPython의 내부 함수들을 호출하는 C 코드 라는 부분이 이해가 되지 않아 조금 더 찾아보았다.
- python 동작 방식
이를 이해하기 위해서는 먼저 파이썬의 동작 방식을 한 번 짚어볼 필요가 있다.
- 기본적으로 Python은 인터프리터 언어로, C/C++ 같은 컴파일 언어처럼 전체 소스 코드를 기계어로 미리 변환해두지 않고 한 줄씩 번역하고 실행한다.
- 실제 동작 과정은 `.py` 소스코드 → `.pyc` 바이트코드로 변환하고, 이를 파이썬 인터프리터가 한 줄씩 읽어 실행하는 방식이다.
- 대표적으로 C언어로 구현된 CPython 인터프리터가 사용되고, 이 CPython은 내부에 이미 기계어로 변환된 수많은 C 함수들을 가지고 있다. 파이썬 코드를 실행한다는 것은 결국 이 C 함수들을 호출하여 작업을 처리하는 구조인 것이다.
그래서 여기에서 말하는 CPython C-API를 호출한다는 것은 Cython이 파이썬 코드를 CPython 인터프리터가 제공하는 내장 C 함수(API)들을 직접 호출하는 형태의 C 소스 코드로 변환한다는 의미로 이해할 수 있다.
따라서 이렇게 만들어진 `.c`, `.so` 바이너리 파일은 순수 `.c` 파일처럼 C 컴파일러 위에서 실행할 수 없고, CPython 인터프리터 위에서만 동작할 수 있게 된다.
Q. 그럼 왜 Cython을 사용할까?? CPython 인터프리터로 실행되는 건 똑같은 것 같은데..
일반적인 파이썬 프로젝트는 `.py`나 `.pyc`로 배포되고 CPython 인터프리터가 이를 기계어로 해석하고 실행한다.
이러한 방식은 내부 비즈니스 로직이 노출될 위험이 있다.
Cython을 사용할 경우, `.py`을 기계어로 번역된 `.so`로 만들어서 배포가 가능하기 때문에 내부 로직을 이해하기가 어려워진다.
(이론적으로 이를 디컴파일하여 다시 `.py`로 되돌릴 수는 어렵다..)
Cython 난독화 (with. 도커 이미지 빌드)
1. 먼저 setup.py에서 난독화 할 파일들을 지정해주면 된다.
- 모든 `.py` 파일을 난독화하기 위해 glob 라이브러리를 사용해 지정해주었다.
- 프로그램 실행 지점인 `main.py`와 모듈 구조를 유지하기 위해 `__init__.py`는 제외했다.
# setup.py (필요한 파일만 컴파일)
import glob
from setuptools import setup
from Cython.Build import cythonize
# '.py' 확장자를 가진 모든 파일을 찾되, 제외할 파일들 정의
py_files = glob.glob('**/*.py', recursive=True)
exclude_files = ['setup.py', 'main.py']
# __init__.py 파일들 제외 (모듈 구조 유지용)
exclude_files.extend([f for f in py_files if f.endswith('__init__.py')])
# 제외할 파일들 제거
for exclude_file in exclude_files:
if exclude_file in py_files:
py_files.remove(exclude_file)
setup(
ext_modules=cythonize(
py_files,
compiler_directives={'language_level': "3"}
)
)
2. DockerFile에서 Cython 라이브러리를 설치해주고, 앞서 `setup.py`에서 정의한 파일들을 Cython으로 컴파일 해주면 된다.
- Cython 컴파일 이후, 원본 코드 노출을 막기 위해서 `.py`, `.c` 파일은 삭제해주도록 한다.
- 최종 이미지에 프로젝트에 필요한 의존성을 모두 설치한 이후 `requirements.txt`도 삭제해주도록 한다.
# ===== Builder =====
FROM python:3.9 AS builder
WORKDIR /app
COPY . .
# Cython 및 필요한 파이썬 라이브러리 설치
COPY requirements.txt .
RUN pip install --upgrade pip setuptools cython \
&& pip install --no-cache-dir -r requirements.txt
# Python을 C로 컴파일 (.so 생성)
# .c 파일 삭제 및 .py 파일 삭제 (__init__.py 제외)
RUN python setup.py build_ext --inplace \
&& find . -name "*.c" -delete \
&& find . -name "*.py" ! -name "__init__.py" ! -name "main.py" -delete \
&& strip --strip-unneeded $(find . -name "*.so" || true)
# ===== Runtime =====
FROM python:3.9-slim
WORKDIR /app
RUN apt-get update && apt-get install -y \
fontconfig \
libhdf5-dev \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt \
&& rm requirements.txt
# 빌드한 결과물 최종 이미지로 복사
COPY --from=builder /app/app/ ./app/
COPY --from=builder /app/font ./font
COPY --from=builder /app/main.py ./
ENV APP_ENV=production
ENV TZ=Asia/Seoul
ENV PYTHONUNBUFFERED=1
CMD ["python", "-u", "./main.py"]
2. Nuitka
Nuitka는 `.py` 코드를 C++ 코드로 변환하고, 바로 C++ 컴파일러로 빌드해서 바이너리 파일을 만들어준다.
여기서도 마찬가지로 순수 C++ 코드로 변환되기 보다는, CPython API를 활용해서 빌드되는 듯 하다.
특징으로는 프로젝트 전체를 빌드해 하나의 `.exe` 실행파일로 만드는데 있다.
이 때 CPython 인터프리터도 같이 패키징하여, 파이썬 설치 없이 배포가 가능하다.
(도커로 배포하려고 하는 관점에서는 큰 이점은 아닐 것 같기도 하다.)
그리고 `requirements.txt`에 있는 프로젝트의 의존성 패키지 라이브러리 역시 C++ 코드로 변환하고 컴파일을 한다. (라이브러리에서 미리 컴파일 된 바이너리 파일이 있으면, 이를 사용할 수도..!)
이 과정에서 실행에 필요한 모든 파일을 통째로 넣기 때문에, 의존성이 많은 프로젝트들은 빌드 시간과 메모리 사용량, 실행 파일 용량이 매우 커질 수 있다.
Nuitka 난독화 (with. 도커 이미지 빌드)

Cython 테스트 이후, Nuitka도 적용해보기 위해 Dockerfile을 수정하고 이미지 빌드를 해보았다!
결과는 약 27분동안 빌드하다가.. Nuitka 컴파일 단계에서 메모리가 부족해 강제 종료되었다..
추가로 더 테스트해보고 싶은 마음도 있었지만,
- Cython으로 충분히 난독화를 적용할 수 있었고
- Nuitka의 긴 빌드 시간
- 서버의 높은 리소스를 요구한다는 점
에서 Nuitka 적용은 여기에서 마무리하기로 했다!
3. PyInstaller
번외로 PyInstaller를 일부 블로그에서 난독화의 방법으로 소개되고 있어 간략히 적어본다.
PyInstaller의 공식문서를 보면 `.py` 코드가 포함되어 있지는 않지만, `.pyc` 바이트코드가 제공되어 디컴파일하여 코드의 로직을 파악이 가능하다고 한다. 그래서 소스 코드를 숨기려면 Cython으로 컴파일하는 것을 공식문서에서도 권하고 있다.

참고사항
1. Cython docs
- https://cython.readthedocs.io/en/latest/src/quickstart/overview.html
2. PyInstaller docs
- https://pyinstaller.org/en/stable/operating-mode.html#hiding-the-source-code
'TIL' 카테고리의 다른 글
| 블로그 글쓰기의 '이미지 업로드'는 어떻게 동작할까?? (0) | 2025.12.17 |
|---|---|
| jar 파일 실행해 보셨어요? (1) | 2025.05.25 |
| MCP 에 대해 알아보자 (0) | 2025.05.21 |
| 자바 모듈, Gradle 모듈 무엇이 다를까? (feat. 멀티 모듈) (0) | 2025.05.15 |
| 애플 소셜 로그인 (with. Spring) (1) | 2024.11.05 |