PyQt5 GUI(뷰/입력)와 **프랙탈 그래픽 엔진(계산/렌더링)**을 완전히 분리해서, 엔진은 **Native Python(표준 라이브러리 중심)**으로 구성하는 건 “장기적으로 이득이 큰 선택”이에요. 대신 프랙탈은 계산량이 커서, 분리 설계만으로 해결 안 되는 성능/동시성 함정이 몇 개 있습니다.
아래에 **장점/단점 → 특징 → 주의점(실전 체크리스트)**로 정리할게요.
1) 장점
1) 테스트/검증이 쉬워짐
- 엔진이 Qt에 의존하지 않으면 pytest로 순수 함수/클래스 단위 테스트가 가능
- 예: “같은 파라미터 → 같은 픽셀 결과(결정성)” 검증, 회귀 테스트(렌더 결과 비교) 가능
2) 재사용/이식성이 커짐
- 같은 엔진을
- PyQt5 GUI
- CLI 렌더러(이미지 파일 출력)
- 웹 백엔드(타일 렌더링)
- 다른 GUI(Tkinter, Delphi, Electron)
로 쉽게 교체 가능
- “엔진 = 제품”, “GUI = 어댑터”가 됨
3) 설계가 명확해짐(DDD/포트-어댑터에 잘 맞음)
- 엔진 쪽은
- RenderSpec(입력)
- RenderResult(출력)
- Renderer(서비스)
- Palette, Viewport(값 객체)
같은 구조가 깔끔하게 자리잡음
- GUI는 “현재 뷰포트/팔레트/프리셋 상태”만 관리
4) 디버깅이 쉬움(재현 가능성 ↑)
- Qt 이벤트 루프/스레드 문제와 분리되면 “렌더링 버그”를 순수 엔진 코드로 재현 가능
- 크래시/멈춤/글리치 원인 분리가 쉬움
5) 의존성/배포가 단순해짐(엔진은 표준 라이브러리만)
- 엔진을 별도 패키지로 떼면:
- 엔진은 pip 의존 거의 없음
- GUI만 PyQt5 의존
- 유지보수 비용이 줄어듦
2) 단점
1) 성능 한계가 빨리 드러남(특히 pure Python 루프)
프랙탈은 픽셀마다 반복 연산(복소수/반복)이 많아서:
- “엔진을 Native Python으로만” 만들면
- 큰 해상도/높은 iteration에서 CPU가 쉽게 한계
- 결국 어느 시점엔
- multiprocessing(표준)
- 또는 NumPy/Numba/C 확장(비표준)
같은 옵션을 고민하게 됨
2) 경계 설계 비용이 생김(데이터 전달/형식)
GUI ↔ 엔진 사이에 주고받는 데이터(픽셀 버퍼, 타일, 팔레트)가 커서:
- 무작정 list[list[int]] 같은 구조로 주고받으면 느리고 메모리 폭발
- “표준+효율” 사이에서 데이터 포맷 설계가 필요
3) 동시성 복잡도(스레드/프로세스/취소/진행률)
GUI는 항상 반응해야 하고, 렌더링은 오래 걸리니:
- 렌더 작업을 백그라운드로 돌리고
- 취소/재시작/진행률 업데이트
를 구현해야 함
→ 분리하면 더 “정교한 작업 관리”가 필요해짐
4) Qt 이미지 변환 비용(브릿지 비용)
엔진 결과를 Qt에 표시하려면 결국:
- raw bytes → QImage → QPixmap
변환이 들어가는데, 이 변환이 병목이 될 수도 있음
(특히 프레임이 자주 갱신될 때)
3) 특징(이 구조에서 “잘 되는” 패턴)
✅ 엔진은 “결정적(deterministic) + 순수”에 가깝게
- 같은 입력(RenderSpec)이면 같은 출력(RenderResult)
- 내부 상태 최소화(캐시/랜덤 최소화)
- I/O 없음(파일/Qt/네트워크 금지)
✅ GUI는 “상태 머신 + 이벤트” 중심
- 줌/팬/팔레트 변경 = 상태 변경 이벤트
- 이벤트가 발생하면 “새 RenderSpec 생성 → 렌더 요청”
- 렌더 완료 신호가 오면 화면 갱신
✅ 경계 데이터는 “작고 명확”하게
- 입력: viewport, size, max_iter, palette_id, formula_id …
- 출력: bytes(RGBA), width/height, stats(시간, iter 히트맵 요약 등)
4) 주의해야 할 점(실전 함정)
1) GUI 스레드에서 렌더링 금지
- Qt 메인 스레드는 이벤트 루프용
- 렌더링을 여기서 하면 UI가 멈춤
- 해결:
- QThread + 작업 객체
- 또는 concurrent.futures.ThreadPoolExecutor
- CPU-bound면 multiprocessing이 더 유리
2) 취소(Cancel) 설계를 처음부터 넣기
프랙탈 탐색은 “사용자가 계속 줌/팬”하므로:
- 이전 렌더는 의미 없어짐 → 즉시 중단해야 함
- 해결 패턴:
- render 요청마다 job_id 발급
- 엔진은 루프 중간중간 cancel_token.is_cancelled() 확인
- GUI는 최신 job_id만 수용(낡은 결과 폐기)
3) 데이터 포맷: list 대신 bytes/bytearray/memoryview
- 픽셀 버퍼는 bytearray(RGBA) 추천
- memoryview로 슬라이스/타일 처리하면 복사 줄어듦
- struct / array('I')도 옵션
4) 프로세스 사용 시 “pickle 비용” 관리
multiprocessing으로 타일을 나누면 좋은데:
- 큰 버퍼를 작업마다 넘기면 직렬화 비용이 큼
- 해결:
- 타일 단위로 계산해서 작은 결과만 전달
- 가능하면 shared_memory(표준 lib)로 공유 버퍼 사용
5) 부동소수/확대(zoom) 정밀도 한계
깊게 확대하면 float 정밀도가 깨져서 아티팩트가 생김
- Native Python만 쓰면 decimal로 가는 선택지가 있지만 매우 느림
- 현실적 대안:
- 일정 줌 이상에서는 품질/속도 타협(샘플링/iteration 조절)
- 또는 고정소수/임의정밀(비표준 라이브러리) 고려
6) 색상 매핑(팔레트)도 엔진 규칙으로 고정
GUI에서 색을 “그때그때” 바꾸면 재현이 어려움
- 팔레트 정의는 엔진 쪽 “순수 함수”로
- GUI는 팔레트 ID/파라미터만 전달
7) 이벤트 폭주(thrashing) 방지
줌/팬 드래그 중 렌더 요청을 매번 보내면:
- 작업이 계속 쌓이고 CPU만 낭비
- 해결:
- 디바운스(예: 50~120ms)
- 드래그 중엔 저해상도 프리뷰, 손 뗄 때 고해상도
5) 한 줄 결론
- 장점: 테스트/재사용/유지보수/이식성이 크게 좋아지고 구조가 깨끗해짐
- 단점: 성능·동시성·데이터 전달(버퍼/변환) 설계를 제대로 안 하면 오히려 복잡해짐
원하면 다음 단계로, 당신이 원하던 스타일(DDD + 이벤트)로:
- RenderSpec / RenderResult / Viewport / Palette
- Renderer 인터페이스 + TMandelbrotRenderer급 구현(파이썬)
- RenderJobManager(취소/디바운스/최신결과만 수용)
- PyQt5 어댑터(QImage 변환 포함)
이렇게 “바로 붙여서 실행” 가능한 스켈레톤을 코드 파일 세트로 정리해줄까요?
'Native Python Programing' 카테고리의 다른 글
| [Native Python Programing] 재사용 가능한 소프트웨어 만들기 (0) | 2026.01.17 |
|---|---|
| [Native Python Programing] Native Python Programing이란? (0) | 2026.01.17 |