기본 콘텐츠로 건너뛰기

배포가 자꾸 사람을 괴롭힌다면: publish를 ‘플러그인 가능한 동작’으로 바꾼 날

thumbnail

배포가 자꾸 사람을 괴롭힌다면: publish를 ‘플러그인 가능한 동작’으로 바꾼 날

해커톤 마지막 날 오후였다. 누군가 노트북을 돌려 보여주면서 말했다.

“앱은 돌아가요. 근데 배포만 하면 끝에서 항상 막혀요. 우리도 웹으로 올리는 publish 하나만… 딱 하나만 넣어주면 안 돼요?”

그 방에는 두 종류의 사람이 있었다.

한쪽은 바로 고개를 끄덕였다. “좋아, 커맨드 하나 추가하자.” 다른 쪽은 얼굴이 굳었다. “그거, 커맨드 하나로 끝나는 일이 아니야.”

나는 그 사이에서, 슬쩍 겁이 났다. 코드를 고치는 일은 익숙한데 배포는 늘 낯설었다. 특히 파이썬 프로젝트에서 배포는 더 그랬다. 어떤 팀은 SFTP로 올리고, 어떤 팀은 대시보드에서 클릭하고, 어떤 팀은 누군가의 로컬 스크립트를 빌려 쓴다. 그 스크립트는 빠르지만, 늘 한 사람의 기억에 의존한다.

그날 기능요청은 “웹 배포”였지만, 실제로 우리를 괴롭힌 건 다른 질문이었다.

‘publish’라는 동작을 어디까지 책임지게 할 건가?

문서에 배포 방법을 적어두면 된다고 생각했는데, 문서는 늘 늙었다. 호스팅 화면이 바뀌고, 인증 방식이 바뀌고, 팀원이 바뀌고, 무엇보다 “배포를 잘 아는 사람”이 바뀐다. 배포가 문서에 붙어 있는 순간, 배포는 기술이 아니라 기억력 시험이 된다.

이 글은 며칠 전 읽은 한 사례에서 시작한다. BeeWare Briefcase가 PythonAnywhere를 publish 타깃으로 붙인 과정과, 그 과정에서 briefcase publish web이 어떻게 논의되는지를 보면(참고자료), 이 문제가 “특정 호스팅의 사용법”이 아니라 구조의 문제라는 게 드러난다. 그리고 구조를 바꾸지 않으면, 팀은 배포를 할수록 더 느려진다.

(링크는 맨 아래에만 모았다.)


“그럼 PythonAnywhere는 누가 알아야 하죠?”라는 질문

처음엔 다들 단순하게 생각한다.

“PythonAnywhere API로 업로드하면 되지.”

그런데 곧 질문이 생긴다.

누가 PythonAnywhere를 알아야 하지?

배포 담당자만 알면 되는가, 아니면 모든 개발자가 알아야 하는가. 기능을 만드는 사람도 배포를 이해해야 하는가. 이해해야 한다면 어디까지인가.

이 질문은 되게 실무적이다. 팀이 커질수록 더 그렇다.

만약 publish가 “호스팅별 기능”으로 박혀 있으면, 호스팅이 늘 때마다 팀은 지식도 늘려야 한다. 그 지식은 코드리뷰에 스며들고, 설치 스크립트에 스며들고, 결국 로컬 환경에까지 스며든다.

그날 밤에도 비슷한 일이 벌어졌다.

누군가는 “core에 PythonAnywhere 코드를 넣자”고 했다. 빨리 끝내고 싶어서. 누군가는 “절대 안 된다”고 했다. 이유는 간단했다. PythonAnywhere를 쓰지 않는 사람도 그 의존성(그리고 인증 흐름)을 떠안게 되니까.

여기서 플러그인 분리는 깔끔한 아키텍처 취향이 아니라, 팀의 시간 문제로 내려온다.

  • 배포 채널 하나 추가하려고 core가 무거워지면, 설치가 느려진다.
  • 설치가 느려지면, 새로 들어온 사람이 제일 먼저 좌절한다.
  • 좌절하면 결국 “배포는 저 사람만”이 된다.

그 순간부터 배포는 다시 기억력 시험이 된다.

PythonAnywhere 쪽 글이 흥미로운 지점도 거기다(참고자료). 단순히 “새 타깃이 생겼다”가 아니라, publish 타깃이 늘어날 때 무엇을 core가 책임지고 무엇을 외부로 밀어내야 하는지의 감각이 보인다.


‘선택 설치’가 되지 않으면 배포는 팀을 갈라놓는다

우리는 결국 한 가지 원칙을 세웠다.

배포 채널은 “코어에 포함되는 기능”이 아니라, 필요할 때 설치해서 붙이는 확장으로 둔다.

그때부터는 “배포를 아는 사람”을 찾기보다, “이 채널을 설치했나”를 먼저 확인하게 됐다.

이 말은 “설치 옵션을 친절하게 만들자”가 아니다. 배포가 팀을 갈라놓지 않게 하자는 말이다.

실무에서 배포는 되게 이상한 위치에 있다. 모두가 중요하다고 말하지만, 모두가 매일 만지진 않는다.

그래서 배포용 의존성이 core에 들어오면 이런 일이 생긴다.

  • 어느 날 배포 라이브러리 버전이 올라가며 다른 의존성과 충돌한다.
  • 배포를 하지도 않는 개발자가 그 충돌을 해결하느라 시간을 쓴다.
  • 그러다 누군가 말한다. “배포 관련은 건드리지 말자.”

이 흐름이 만들어지면, 배포는 점점 특정 사람의 영역이 된다. 그리고 그 사람의 휴가가 팀의 배포 일정이 된다.

Briefcase 논의가 흥미로운 건, publish 타깃이 ‘웹’처럼 보이지만 실제로는 “이 타깃을 어떤 방식으로 끼워 넣을지”가 핵심이라는 점이다(참고자료). 끼워 넣는 방식이 정해져 있으면, 그 다음 타깃은 덜 무섭다. 반대로 방식이 없으면, 다음 타깃은 항상 첫 타깃처럼 어렵다.

그날 내가 얻은 결론은 문장 하나로는 잘 안 담긴다. 다만 감각은 선명했다. “배포가 된다/안 된다”를 사람의 기억에 맡기지 말고, 레포의 구조(코어/채널)로 바꿔야 한다는 것. 그래야 다음 해커톤에서도, 다음 인수인계에서도, 같은 실수를 반복하지 않는다.

우리가 실제로 한 건 ‘이름 붙이기’였다.

publish는 “이미 만들어진 출력물을 특정 채널로 보내는 동작”이고, 채널은 플러그인으로 붙인다.

그 동작의 모양을 하나로 고정하면, 팀은 “PythonAnywhere를 어떻게 쓰는지”를 모든 사람이 알 필요가 없어진다. 필요한 사람만 설치하고, 필요한 사람만 인증을 다루고, 대신 팀 전체는 같은 동사를 공유한다.

내가 첫 PR에서 실제로 한 건 거창하지 않았다. 문서에 배포 절차를 한 페이지 더 추가하는 대신, publish가 실패하면 어떤 메시지가 떠야 하는지만 먼저 정리했다. 그리고 “업데이트”가 아니라 “처음 만들기”로만 안전하게 실행되는 경로를 하나 남겼다.


안전 기본값이 없으면 publish는 ‘성공한 실패’를 만든다

해커톤 다음 날, 누군가가 이렇게 말했다.

“배포 성공했어요.”

그런데 앱은 안 떴다. 아니, 더 정확히는 어떤 사람에게는 떴고 어떤 사람에게는 안 떴다. 원인을 찾다 보니, 이전 배포를 덮어써야 하는데 반쯤만 덮어쓴 상태였다. 로그는 성공처럼 보였고, 대시보드는 업로드된 것처럼 보였고, 사람은 ‘완료’라고 생각했다.

이게 publish에서 제일 무서운 실패다.

실패는 빨리 드러난다. 하지만 성공처럼 보이는 실패는 늦게 드러난다. 보통은 사용자가 먼저 알려준다.

그래서 우리는 플러그인 구조를 논의하면서도, 자꾸 안전 이야기로 돌아왔다.

  • 기본값은 덮어쓰지 말자
  • 무엇을 바꿀 건지 먼저 보여주자
  • “처음 만들기”와 “업데이트”를 의식적으로 구분하게 하자

Briefcase 쪽 논의에서 --create 같은 플래그가 자주 등장하는 이유도 결국 같은 결이다(참고자료). 누군가의 기존 배포를 덮어쓰는 순간, 그건 기능이 아니라 사고가 된다. 그래서 기본값을 안전하게 두지 않으면, 팀은 배포를 무서워하고 결국 배포를 피한다.

여기서 플러그인이 주는 가치는 “타깃이 늘어난다”가 아니라, 안전 규칙을 core로 끌어올릴 수 있다는 점이다.

채널마다 각자 안전을 구현하면, 팀은 또 문서를 쓴다. “이 채널은 덮어써요”, “저 채널은 덮어쓰기 안 돼요.” 문서는 늙고, 배포는 다시 기억력 시험이 된다.

그래서 인터페이스의 모양에 안전 기본값을 박아두면, 채널을 붙일 때마다 같은 위험을 반복해서 설계하지 않아도 된다.


그날 이후 우리가 적게 싸우게 된 이유(짧은 코드)

결국 팀에서 가장 자주 싸우는 건 “호스팅이 뭐냐”가 아니었다.

“지금 배포가 뭘 하려는 거냐”였다.

그래서 배포 채널을 플러그인으로 만든다는 건, 합의를 코드로 옮기는 일이 된다.

이런 합의가 문서로도 바로 떨어진다. 설치 방법, 토큰 준비, pyproject에 둘 설정이 한 페이지에 모이면, 팀은 “누가 아느냐”가 아니라 “지금은 어느 단계냐”로 대화하게 된다. 이 프로젝트에서 publish는 이런 플러그인 이름 아래로 들어온다, 정도만 고정해도 효과가 있었다.

pyproject.toml (예시)
[project.entry-points."mytool.publish"]
pythonanywhere = "mytool_pythonanywhere:PythonAnywhereChannel"

이 짧은 조각이 멋있어서가 아니다.

이렇게 등록되는 순간, publish는 “core가 다 아는 기능”이 아니라 “필요한 채널을 붙이는 동작”이 된다. 의존성은 채널 쪽으로 밀려나고, core는 단단해진다. 그리고 팀은 ‘배포를 잘 아는 사람’을 찾기 전에, ‘이 채널을 붙였는지’를 먼저 확인하게 된다. 싸움의 대상이 사람에서 구조로 이동한다.


이 얘기가 배포에서만 끝나지 않는 이유

이 패턴은 배포에서만 반복되지 않는다.

팀이 뭔가를 ‘붙일’ 때마다 똑같은 질문이 돌아온다. “이건 레포 어디에 들어가야 하지?”, “기본값은 안전한가?”, “실패하면 어디에 어떤 로그가 남지?”

그 질문에 답이 없으면, 기능이 늘어날수록 성공처럼 보이는 실패가 늘어난다. 그래서 publish 채널을 플러그인으로 분리한다는 결정은 단순히 배포 편의가 아니라, 팀이 설계를 대화하는 방식 자체를 바꾸는 사건이었다.


참고자료

  • PythonAnywhere — How PythonAnywhere Became a Publish Target for BeeWare Apps
    • https://blog.pythonanywhere.com/223/
  • pythonanywhere-briefcase-plugin (docs)
    • https://briefcase.pythonanywhere.com/
  • BeeWare Briefcase (GitHub Discussion) — Implement briefcase publish web for PythonAnywhere
    • https://github.com/beeware/briefcase/discussions/2678

댓글

이 블로그의 인기 게시물

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...