기본 콘텐츠로 건너뛰기

큰 파이썬 레포에서 첫 PR이 제일 무섭다: CPython이 보여준 ‘지도’ 만들기

thumbnail

큰 파이썬 레포에서 첫 PR이 무서운 이유: 코드를 고치기 전에 ‘길’부터 봐야 한다

처음 큰 레포를 열었을 때, 나는 코드가 아니라 공기가 무거웠다. 파일이 많고, 디렉터리가 많고, 규칙이 많아 보였다. 어디서부터 손대면 되는지 감이 없었다. 그래도 PR 하나는 해야 하니까, 제일 작은 걸 골랐다. 오타 하나, 로그 한 줄, 그 정도.

그런데 이상하게도 그 작은 변경이 나를 하루 종일 잡아먹었다. 내가 바꾼 건 한 줄인데, 테스트는 수백 개가 돌았고, 중간중간 실패가 났고, 다시 돌리면 또 다른 곳이 깨졌다. 그때부터는 코드가 아니라 ‘길’을 찾는 시간이 됐다. 어디서 실행해야 하는지, 어디서 깨진 건지, 내가 바꾼 게 도대체 어디에 영향을 주는지.

오늘 글은 설치 팁이 아니다. uv가 좋다는 얘기로 시작하지 않는다. 큰 파이썬 코드베이스를 처음 건드릴 때 제일 먼저 필요한 건 도구가 아니라, “지금 내가 어디에 서 있는지”를 알게 해주는 지도다.

본문에는 링크를 넣지 않고, 읽을거리는 맨 아래 참고자료로만 모았다.


그날 10분: 테스트가 터졌는데, 어디가 문제인지도 몰랐다

첫 PR을 올리기 전에 로컬에서 테스트를 돌렸다. ‘전체 테스트’라는 말이 있다면, 나는 그걸 그대로 믿는 편이었다. 그런데 큰 레포에선 그 믿음이 바로 부메랑이 된다. 전체 테스트는 오래 걸리고, 오래 걸리는 동안 사람은 집중을 잃는다. 집중을 잃으면 로그를 대충 읽고, 대충 읽으면 더 대충 고친다.

그날도 그랬다. 실패가 뜨길래 해당 테스트 파일을 열었는데, 테스트가 하는 일이 내가 바꾼 코드랑 전혀 상관없어 보였다. 그래서 다른 테스트로 건너갔다. 또 상관없어 보였다. “내가 뭘 건드린 거지?”라는 질문이 한 시간 뒤에야 제대로 떠올랐다.

그때부터 나는 전체 테스트를 포기했다. 포기라는 말이 좀 거칠지만, 큰 레포에서 포기는 종종 ‘전략’이 된다. 대신 최소한의 확인을 하나 정했다. 이 한 줄을 돌리고 나서야 다음으로 넘어가기로.

pytest -q -m smoke

이게 품질을 보장해주진 않는다. 하지만 이 한 줄이 있으면, 그날의 작업은 최소한 “내가 무엇을 확인했는지”를 말할 수 있게 된다. 큰 레포에서 제일 무서운 건 실패가 아니라, 실패가 어디서부터 시작됐는지 모르는 상태다.

그날 내가 진짜로 얻은 건 테스트 통과가 아니었다. 스택트레이스를 읽는 속도였다.

실패가 나면 보통 사람은 제일 마지막 줄부터 본다. 큰 레포에선 그 습관이 위험하다. 마지막 줄은 친절한데, 친절한 만큼 정보를 숨긴다. “어떤 값이 왜 여기까지 왔는지”가 빠져 있기 때문이다. 그래서 나는 실패가 나면 스택트레이스를 거꾸로 올려 읽기 시작했다. 어디서 입력이 들어왔고, 어디서 변형됐고, 어디서 형태가 바뀌었는지.

이게 바로 지도다. 코드를 이해하는 게 아니라, 흐름을 이해하는 것.


내가 제일 먼저 본 파일: ‘여긴 건드리지 말자’가 생기는 순간

테스트가 터지는 것보다 더 어려웠던 건, 내가 뭘 모르고 있는지도 모르는 상태였다. 그래서 그날은 코드를 더 보지 않고, 레포의 바깥부터 훑었다. 빌드 설정, 도구 설정, CI 관련 파일들, 그런 것들.

여기서 한 가지가 생겼다. 금지구역이 아니라 ‘보류구역’.

나는 첫 PR에서 이 구역은 건드리지 않겠다고 마음먹었다. 이유는 간단했다. 지금은 수정 능력이 아니라, 실수했을 때의 피해를 줄이는 능력이 필요했기 때문이다. 작은 변경으로 끝내는 데 집중하려면, 퍼지는 변경을 피해야 했다.

내가 보류구역으로 잡은 건 대개 이런 것들이었다. 빌드 스크립트, 테스트 러너 설정, CI 워크플로, 린터 설정, 그리고 “여기서 쓰고 저기서도 쓰는” 공용 유틸. 이런 파일은 한 줄 바꾸면 좋아 보이지만, 그 한 줄을 검증하는 비용이 크다. 초보에게 그 비용은 ‘공포’가 된다.

반대로 처음 건드리기 좋은 곳도 있다. 기능 경로의 끝, 아주 얇은 래퍼, 로그 한 줄. 변경을 작게 만들 수 있는 자리. 그 자리를 찾는 게 레포를 읽는 첫 번째 동기다.

이게 지도 만들기의 첫 장면이었다. “안 건드려도 되는 곳”이 생기자, 이상하게도 “건드려야 하는 곳”이 좁아졌다. 그리고 좁아진 만큼 읽을 수 있게 됐다.

CPython 같은 코드베이스가 수십 년 동안 쌓였다는 이야기를 들으면(참고자료), 우리는 규모를 떠올린다. 나는 다른 걸 떠올린다. 쌓인 시간만큼 ‘길’이 생겼다는 것. 길이 많으면, 초보는 전체를 이해할 수 없다. 대신 길 하나를 따라가야 한다. 그 길을 따라가기 위해 먼저 필요한 건, 따라가지 말아야 할 길을 고르는 감각이다.

내가 고른 첫 길은 항상 같은 질문으로 시작한다. “입력이 어디서 들어오지?” 그리고 그 입력이 “상태를 바꾸는 순간”은 어디인지.

이 두 점만 연결하면, 중간이 복잡해도 길이 생긴다. 반대로 중간부터 파면, 길이 아니라 방이 늘어난다. 클래스가 나오고, 헬퍼가 나오고, 설정이 나오고, 어느새 처음의 입력은 잊힌다.

그래서 나는 첫날에는 코드를 이해하려 하지 않고, 흐름을 눈으로만 따라간다. 파일을 열고, 함수 이름을 보고, import를 따라가고, 다시 파일로 돌아오고. 이렇게 한 바퀴만 돌면 “여기에서만 바뀌는 값”과 “여기 바꾸면 다 흔들리는 값”이 구분되기 시작한다.

이 구분이 생기면 읽는 순서도 바뀐다. 문서를 찾기 전에 코드로 들어가고, 코드를 다 읽기 전에 경로를 좁힌다. 큰 레포에서 지도를 만든다는 건 결국 이 순서 바꾸기다.


리뷰에서 딱 한 줄: “이거 어디까지 영향 가나요?”

PR을 올리고 나서 받은 첫 코멘트는 코드 스타일이 아니었다.

“이 변경, 어디까지 영향 가나요?”

그 한 줄이 내 하루를 정리해줬다. 내가 하고 싶었던 것도 그거였으니까.

나는 순간 당황했다. ‘오타 하나’였는데, 왜 영향 반경을 묻지? 그런데 생각해보면 당연했다. 리뷰어는 내 의도를 모른다. 리뷰어가 보는 건 diff가 아니라 리스크다. 큰 레포에서 리스크는 대개 코드가 아니라 경로에서 나온다. 한 줄이 다른 팀의 배치를 멈출 수도 있고, 한 줄이 디버깅을 두 배로 늘릴 수도 있다.

그래서 그날 나는 코드 설명 대신 “이 변경이 닿는 길”을 설명했다. 어디에서 이 값이 만들어지고, 어디에서 소비되는지. 그걸 찾기 위해 내가 읽었던 파일 이름 몇 개를 PR 본문에 적었다. 그 순간부터 리뷰는 ‘심문’이 아니라 ‘합의’가 됐다.

큰 레포에서 첫 PR을 망치는 건 대개 코드 실력이 아니다. 영향 반경을 모르는 상태로 바꾸는 것이다. 영향 반경을 모르면, 리뷰어는 겁을 먹고, 겁을 먹으면 PR은 커진다. PR이 커지면 테스트가 더 터지고, 테스트가 더 터지면 사람은 더 조급해진다. 그 조급함이 다시 영향을 키운다.

그래서 그날 이후로 나는 PR을 올릴 때 ‘설명’ 대신 ‘범위’를 남기기로 했다. 어떤 경로를 건드렸고, 어떤 경로는 일부러 피했는지. 이건 문서 작성이 아니라, 다음 대화를 쉽게 만드는 장치다.

여기서 중요한 건 정답을 맞히는 게 아니라, 예측을 공유하는 거다. “아마 이 테스트는 영향 받을 거고, 저 배치도 한번 봐야 할 것 같다” 같은 문장. 그 예측이 틀려도 괜찮다. 틀린 예측이 쌓이면 지도가 정확해진다. 반대로 예측을 아예 하지 않으면, 팀은 늘 ‘사고 난 뒤’에만 배운다.

그리고 이건 로컬 개발 루프를 빠르게 만든다. 딱히 컴퓨터가 빨라지는 건 아니다. 사람이 빨라진다. 내가 지금 뭘 했고, 뭘 안 했는지 말할 수 있으면, 다음 확인이 빨라지고, 다음 수정이 작아지고, 작은 수정은 다시 빠르게 끝난다.


uv는 ‘추천’이 아니라 싸움을 줄이는 장치다

여기까지 오면 도구가 보인다. 이때 uv 같은 도구는(참고자료) “더 빠르다”보다 “같은 환경으로 말 안 싸우게 한다” 쪽에서 가치가 생긴다.

큰 레포에서 팀이 제일 많이 시간을 쓰는 대화는 의외로 기능이 아니다. “너는 어떤 버전으로 돌렸어?” “내 로컬에선 되는데?” 같은 말이다. 환경이 파편화되면, 버그인지 환경인지부터 싸워야 한다.

나는 uv를 도구 추천으로 설명하지 않는다. 그냥 ‘한 경로’를 고정하는 데만 쓴다. 이 레포는 이 방식으로 실행한다, 라는 문장을 팀이 공유하게 만드는 용도. 그 문장이 생기면, 사람은 코드로 돌아갈 수 있다.

이건 속도의 문제가 아니다. 싸움의 문제다. 환경이 다르면, 버그가 생겼을 때부터 대화가 갈라진다. “네 로컬이 이상한 거 아냐?” “CI가 이상한 거 아냐?”로 시작하면, 원인 분석은 이미 늦다. 같은 바닥에서 출발하면, 팀은 바로 ‘길’로 들어갈 수 있다.

큰 레포에서 내가 진짜로 원했던 건 ‘빠른 설치’가 아니라 ‘같은 언어’였다. 테스트를 최소한으로 붙잡을 수 있고, 내가 건드린 범위를 내 말로 설명할 수 있고, 동료와 같은 환경에서 같은 결과를 볼 수 있으면 그날의 PR은 결국 앞으로 굴러간다. 멋있는 기술보다 이런 기본이 먼저였다.


참고자료

  • Python Insider — CPython: 36 Years of Source Code
    • https://blog.python.org/2026/03/cpython-codebase-growth/
  • GitHub Releases — astral-sh/uv 0.10.9
    • https://github.com/astral-sh/uv/releases/tag/0.10.9

댓글

이 블로그의 인기 게시물

Django에서 트랜잭션 관리하기

Django에서 트랜잭션 관리하기 안녕하세요! 오늘은 Django에서 데이터베이스 트랜잭션을 효과적으로 관리하는 방법에 대해 알아보겠습니다. 1. 트랜잭션의 중요성 트랜잭션은 데이터베이스의 일관성과 무결성을 보장하는 중요한 개념입니다. Django에서는 여러 가지 방법으로 트랜잭션을 관리할 수 있습니다. 1.1 기본 개념 원자성(Atomicity) : 트랜잭션은 모두 실행되거나 모두 실행되지 않아야 합니다. 일관성(Consistency) : 트랜잭션 전후로 데이터베이스의 일관성이 유지되어야 합니다. 격리성(Isolation) : 동시에 실행되는 트랜잭션들이 서로 영향을 주지 않아야 합니다. 지속성(Durability) : 완료된 트랜잭션의 결과는 영구적으로 저장되어야 합니다. 2. Django의 트랜잭션 관리 2.1 기본 설정 # settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'mydatabase', 'USER': 'myuser', 'PASSWORD': 'mypassword', 'HOST': 'localhost', 'PORT': '5432', 'ATOMIC_REQUESTS': True, # 모든 뷰를 트랜잭션으로 래핑 } } 2.2 데코레이터 사용 from django.db import transaction @transaction.atomic def create_order(user, items): order = Order.objects.create(user=...

AWS S3 + CloudFront로 정적 파일 서빙 완전 가이드

AWS S3 + CloudFront로 정적 파일 서빙 완전 가이드 안녕하세요! 오늘은 AWS S3와 CloudFront를 사용하여 정적 파일을 효율적으로 서빙하는 방법에 대해 알아보겠습니다. 왜 S3와 CloudFront를 사용할까요? 높은 가용성 : AWS의 글로벌 인프라를 활용 빠른 전송 속도 : CloudFront의 CDN 기능으로 전 세계 사용자에게 빠른 전송 비용 효율성 : 사용한 만큼만 지불 보안 : AWS의 보안 기능 활용 확장성 : 트래픽 증가에 자동 대응 1. S3 버킷 설정 1.1 버킷 생성 및 설정 import boto3 def create_s3_bucket(): s3 = boto3.client('s3') # 버킷 생성 bucket_name = 'your-static-files-bucket' s3.create_bucket( Bucket=bucket_name, CreateBucketConfiguration={ 'LocationConstraint': 'ap-northeast-2' } ) # 버킷 정책 설정 bucket_policy = { "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObje...

RDS에서 Django 앱 성능을 높이는 데이터베이스 설정 팁

RDS에서 Django 앱 성능을 높이는 데이터베이스 설정 팁 안녕하세요! 오늘은 AWS RDS를 사용하는 Django 애플리케이션의 성능을 최적화하는 방법에 대해 알아보겠습니다. 1. RDS 인스턴스 최적화 1.1 인스턴스 타입 선택 # RDS 인스턴스 크기 조정 import boto3 def resize_rds_instance(): rds = boto3.client('rds') response = rds.modify_db_instance( DBInstanceIdentifier='your-db', DBInstanceClass='db.t3.large', # 워크로드에 맞는 인스턴스 타입 선택 ApplyImmediately=True ) return response['DBInstance'] 1.2 파라미터 그룹 설정 def create_parameter_group(): rds = boto3.client('rds') # PostgreSQL 파라미터 그룹 생성 response = rds.create_db_parameter_group( DBParameterGroupName='django-optimized', DBParameterGroupFamily='postgres13', Description='Optimized parameters for Django applications' ) # 성능 관련 파라미터 설정 parameters = [ { 'ParameterName': 'shared_buffers', 'ParameterValue': '2GB...