
__init__.py로 공개 API를 고정하는 법: 파이썬 코드리뷰에서 제일 먼저 합의할 한 가지
패키지가 커지면, 실력 좋은 팀도 똑같은 데서 늦어진다. 기능이 복잡해져서가 아니라, import가 길어지고, import가 길어지면 코드리뷰가 길어진다.
리뷰 화면에서 from app.domain.user.handlers.signup import do_the_thing 같은 줄을 볼 때마다, 나는 기능 구현보다 “이 이름이 밖에서 보이는 이름이 되어도 괜찮나?”를 먼저 떠올린다. 밖에서 보이는 이름이 늘면, 유지보수는 늘어난다. 심지어 좋은 리팩터링도 “다 깨질까 봐” 못 하게 된다.
그래서 이 글은 여러 규칙을 이야기하지 않는다. 한 가지만 이야기한다.
패키지의 공개 API를 __init__.py에서 고정하자.
그걸로 끝이다. 나머지는 팀 취향이다.
왜 하필 __init__.py인가: import 동선을 “짧고 일정하게” 만들기
__init__.py는 파이썬 패키지의 얼굴이다. 어떤 팀은 여기를 비워두고, 어떤 팀은 여기서 이것저것 다 한다. 둘 다 가능하다.
문제는 “가능하다”와 “지속 가능하다”가 다르다는 점이다.
패키지가 작을 때는 누가 어디서 무엇을 import하든 큰 문제가 없다. 파일 하나 옮기면 IDE가 알아서 고쳐주기도 하고, 깨져도 금방 고친다. 그런데 패키지가 커지고, 기능이 늘고, 사람이 바뀌면 import 동선이 팀의 습관이 된다.
동네 팀에서 자주 보이는 패턴이 이거다.
- 처음엔 “그냥 여기에서 가져오면 되겠지”로 깊은 경로를 import한다.
- 다음엔 비슷한 코드를 다른 디렉터리에 또 만든다.
- 어느 날 파일을 정리하려고 옮기면, 생각보다 많은 곳이 깨진다.
- 깨진 걸 고치면서 “그냥 옮기지 말자”가 팀의 결론이 된다.
이때부터 코드리뷰는 “좋은 구조로 정리하는 시간”이 아니라 “깨질까 봐 확인하는 시간”이 된다.
__init__.py로 공개 API를 고정하면, import 동선이 짧아지고, 밖에서 보이는 이름이 정리된다. 중요한 건 짧아지는 게 아니라, 일정해진다는 것이다.
리뷰어 입장에서도 바뀐다.
- 깊은 경로 import가 나오면 “기능” 코멘트가 아니라 “공개 API에 넣을 이름인가?”로 코멘트하게 된다.
- 반대로 공개 API에서 제공하는 이름만 쓰고 있으면, 내부 폴더 구조를 바꿔도 리뷰가 덜 불안해진다.
요약하면 이렇다.
“리팩터링을 쉽게 하려면, 리팩터링을 잘하는 사람을 늘리는 게 아니라, 밖에서 보이는 이름을 줄여야 한다.”
우리 팀에서 바로 쓰는 규칙: 공개 API는 여기로만 나간다
규칙은 간단해야 한다. 그래야 리뷰에서 매번 같은 말을 하지 않아도 된다.
내가 권하는 형태는 이거다.
- 패키지 외부에서 쓰게 할 이름은
mypkg/__init__.py에 모은다. - 외부에서 보이면 곤란한 이름은
mypkg/_internal/같은 디렉터리에 둔다(이름에 의도가 드러나게). - 리뷰에서 깊은 경로 import가 나오면, “그 경로가 싫다”가 아니라 “이걸 공개 API로 올릴 건지”만 물어본다.
여기서 포인트는 “어디에 모을지”를 정했지, “무엇을 모을지”는 아직 정하지 않았다는 점이다. 무엇을 모을지는 팀이 결정한다. 다만 결정한 것만 밖으로 내보내자는 거다.
__init__.py는 “내보내기 목록”이 되어야 한다. 간단한 예시는 아래처럼.
# mypkg/__init__.py
"""mypkg 공개 API
외부 사용자는 여기서 제공하는 이름만 import한다.
"""
from .client import Client
from .errors import ApiError
from .text import normalize_spaces
__all__ = ["Client", "ApiError", "normalize_spaces"]
클래스는 여기서 “기술 선택”이라기보다 “이름 묶음”이다. 그래서 클래스를 공개 API로 올릴 땐, 정말로 외부 호출자가 장기적으로 그 이름에 기대도 되는지(그리고 단순한 함수/데이터로는 부족한지)를 한 번 더 묻는다. (참고자료)
이렇게 해두면 팀의 대화가 단순해진다.
- “Client를 바꾸면 어디가 깨질까요?” → 공개 API에 있으니 영향이 크다.
- “errors.py를 쪼개도 되나요?” → 공개 API에서 이름만 유지하면 내부는 바꿀 여지가 크다.
- “text.py에 함수 더 추가할까요?” → 외부에서 보이게 할 것만
__init__.py에 올리자.
이 규칙이 생기면, PR에서 다음 두 종류의 변화가 구분된다.
- 내부 파일 이동/정리(리뷰가 빠르고, 부담이 작다)
- 공개 API 변경(리뷰가 느리고, 부담이 크다)
이 구분이 팀 속도를 만든다.
리뷰에서 실제로 자주 터지는 문제들
1) __init__.py가 무거워지는 문제
__init__.py에서 설정을 읽고, 환경 변수를 로딩하고, 로깅을 세팅하고, 레지스트리를 채우기 시작하면 어느 순간부터 import가 느려진다. 느린 건 둘째 문제다. 더 큰 문제는 “가져오기만 했는데 뭔가 실행된다”는 불쾌감이다.
이런 코드는 테스트를 어렵게 하고, CLI/서브프로세스에서도 의외의 부작용을 만든다.
그래서 __init__.py는 최대한 가볍게 두는 게 좋다. 내보내기(import) 외에는 웬만하면 하지 않는다.
2) 순환 import가 슬슬 생기는 문제
공개 API에 올릴 이름이 많아지면, 순환 import가 한 번씩 나타난다. 이때 팀이 흔히 하는 선택이 “그럼 그냥 깊은 경로로 import하자”다. 이 선택을 한 번 하면, 공개 API 규칙이 무너진다.
이럴 때는 내보내기 구조를 조정하면 된다. 예를 들어 공통 타입/예외를 한 파일로 모으거나, 실제 구현을 함수 내부로 미루는 방식으로 순환을 풀 수 있다.
아래는 리뷰에서 자주 쓰는 ‘import 사이드이펙트 피하기’ 패턴이다. 전역 import를 늘리는 대신, 필요한 순간에만 가져오게 만든다.
# mypkg/client.py
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .transport import Transport
class Client:
def __init__(self, transport: "Transport"):
self._transport = transport
def request(self, payload: dict) -> dict:
# 런타임에 꼭 필요할 때만 import해서 순환과 초기 비용을 줄인다.
from .parsing import parse_response
return parse_response(self._transport.send(payload))
이건 만능 처방은 아니다. 다만 “공개 API를 유지하려고 깊은 경로 import를 허용하자”는 방향으로 무너지지 않게 해준다.
3) 외부에서 보이는 이름이 늘어난 채로 방치되는 문제
팀이 바쁘면 __init__.py에 이름을 올린 채로 “나중에 정리하자”가 된다. 그런데 나중에는 정리 못 한다. 밖에서 누가 쓰기 시작하면, 그 순간부터는 되돌리기가 어렵다.
그래서 리뷰에서는 이런 질문을 습관처럼 한다.
- “이 이름은 밖에서 써도 되는 건가요, 아니면 지금만 필요하죠?”
- “지금
__init__.py에 올리면, 다음 분기에도 유지할 수 있나요?”
대답이 애매하면 올리지 않는다. 내부에서만 쓰게 하고, 나중에 정말 필요해지면 그때 공개 API로 올린다. ‘나중에 올릴 수 있는 선택’을 남기는 쪽이 팀을 덜 묶는다.
코드리뷰에서 이 규칙을 어떻게 적용하는가
이 규칙이 좋은 이유는 리뷰 코멘트가 짧아지기 때문이다. “취향”이 줄어든다.
리뷰 상황을 한 번 가정해보자.
- PR이 새 모듈을 만들었다.
- 다른 파일에서 그 모듈을 import한다.
- 깊은 경로 import가 등장한다.
이때 리뷰어가 할 일은 디자인 철학을 설파하는 게 아니다. 내 코멘트는 보통 이 한 줄이다: “이 이름, __init__.py로 올릴까요?”
질문 하나면 된다.
“이 이름을 공개 API로 제공할 건가요?”
- 예라면:
__init__.py에 올리고, 이름을 깔끔하게 고정한다. - 아니라면: 깊은 경로 import를 피하고, 내부 호출로 유지한다(혹은 현재 모듈 내부에서만 사용).
그리고 이 질문이 통과되면, 나머지 리뷰는 본래의 기능 리뷰로 돌아간다. 이게 리뷰의 생산성이다.
Glyph의 글이 리뷰의 목적을 “상대방을 고치기”가 아니라 “팀이 더 나은 방향으로 움직이게 하기”로 생각하게 만든다면, 나는 그걸 __init__.py 한 파일에서 가장 빨리 실감한다. (참고자료)
마무리: 공개 API를 고정하면, 패키지 구조를 바꿀 자유가 생긴다
나는 패키지 구조를 ‘정답’으로 생각하지 않는다. 구조는 바뀐다. 팀도 바뀐다.
다만 밖에서 보이는 이름을 아무렇게나 늘리면, 구조를 바꿀 자유가 사라진다. 그때부터는 팀이 코드에 끌려다닌다.
그래서 __init__.py 하나에 집중한다.
외부로 내보낼 이름은 __init__.py로 모으고, 늘릴 때는 신중하게. 깊은 경로 import가 PR에 나오면 “이걸 공개 API로 올릴 건지”만 묻자.
이 세 문장만 지켜도 코드리뷰는 덜 피곤해지고, 리팩터링은 덜 무서워진다.
참고자료
- Glyph Lefkowitz — What Is Code Review For?
- https://blog.glyph.im/2026/03/what-is-code-review-for.html
- Python Morsels — When are classes used in Python?
- https://www.pythonmorsels.com/when-are-classes-used/
- Rodrigo Girão Serrão — TIL #139 – Multiline input in the REPL
- https://mathspp.com/blog/til/multiline-input-in-the-repl
댓글
댓글 쓰기