솔로 개발자가 팀처럼 일하는 방법: 계획은 유연하게, 연결부만 고정하라
출처: Dev.to
나는 LaraFoundry를 혼자 만든다. 한 사람, 한 브랜치, 한 번에 한 작업. 실제 CRM(Kohana.io)에서 추출한 재사용 가능한 Laravel용 SaaS 엔진을 공개적으로 개발하고 있다.
혼자 작업하면서 나는 대부분의 팀을 곤란하게 만드는 질문, 즉 “두 사람이 동시에 같은 시스템의 두 부분을 작업할 때 어떻게 서로 충돌하지 않게 할까?”에 직면하지 않는다.
그럼에도 불구하고 나는 그 질문을 생각 실험으로 스스로에게 던져 보았다. 그리고 그 답이 내 코드베이스를 보는 방식을 바꾸었다.
큰 프로젝트를 팀에 맡길 때 가장 먼저 떠오르는 직관은 명확하다. “먼저 거대한 문서가 필요해.” 모든 라우트, 파일명, 메서드, 입력값, 반환값, 모든 테스트와 그 어설션, 모든 보안 규칙까지. 전체 시스템을 한눈에 볼 수 있는 거대한 지도라면 두 사람이 절대 충돌하지 않을 테니까.
그것이 어른스러운 방식이라고 생각하지만, 실제로는 그렇지 않다. 업계에서는 이를 Big Design Up Front(초기 대규모 설계)라고 부르며, 팀들은 의도적으로 이를 피한다.
실패하는 두 가지 이유
- 지도는 금방 부패한다. 모듈 E의 모든 메서드를 정의하고 나면, 모듈 A를 구현하면서 그 중 세 메서드가 잘못됐고 두 메서드가 누락됐다는 것을 알게 된다. 결국 문서를 만들었다가 바로 버리는 꼴이 된다.
- 세부 사항이 안전을 보장하지 않는다. 위험한 프로젝트가 계획서에 글자가 많다고 안전해지는 것이 아니다. 초기 단계에서 비용이 많이 드는 가정을 테스트할 때 안전해진다. 메서드 시그니처를 적는 것은 저렴하지만 아무것도 증명하지 못한다. 반면, 테넌시, 결제, 인증을 모두 통과하는 수직 슬라이스를 구현하는 것은 비용이 많이 들지만 모든 것을 증명한다.
따라서 거대한 초기 다이어그램은 도달하지 못한 이상이 아니라, 오히려 생략해야 할 것이다.
팀들은 거대한 지도를 만들지 않는다. 대신 세 가지를 한다.
-
시스템을 단계가 아니라 경계(seam)로 나눈다.
내 경우 단계(A → E)는 시간에 따라 한 사람이 작업한다. 팀은 Auth, Tenancy, Billing, Navigation 같은 모듈 단위로 명확한 경계를 만든다. 각 모듈은 누군가가 소유하고, 작은 공개 계약을 갖으며, 내부는 자유롭게 구현한다. 경계가 깔끔할수록 병렬 작업이 가능해진다. -
구현이 아니라 계약을 고정한다. 이것이 핵심이다. 팀 B가 팀 A를 기다리지 않게 하려면, 양쪽 사이의 인터페이스만 고정하고 나머지는 그대로 둔다. 클래스의 모든 메서드가 아니라, A와 B가 소통하는 하나의 계약만 고정한다. 인터페이스와 DTO만 정의하고, 팀 B는 그 인터페이스를 기준으로 코딩하고, 팀 A는 실제 구현을 담당한다. B는 기다리지 않고, 인터페이스를 모킹해 계속 진행한다.
-
의존성을 텍스트 블록이 아니라 그래프로 관리한다.
작업 E는 계약 C에 의존한다. C가 고정되면(E가 구현되지 않아도) E는 모킹을 이용해 시작할 수 있다. 대부분 “의존성”의 80%는 고정된 계약만 필요하고, 실제 구현은 필요하지 않다. 따라서 거의 모든 작업을 병렬화할 수 있다.
계약을 고정한다는 의미
- 고정 시점부터 다른 사람들은 그 형태 위에 코드를 쌓으며, 아무도 혼자서 조용히 바꾸지 않는다.
- 변경이 금지된 것은 아니다. 고정 전에는 자유롭게 바꿀 수 있다(초안 단계). 고정 후에도 바꿀 수는 있지만, 이제는 다른 사람의 작업을 깨는 것이 된다. 따라서 프로토콜이 필요하다: 변경을 알리고, 의존 관계에 있는 사람들과 합의한 뒤, 버전된 형태로 배포하고, 모두가 마이그레이션하도록 한다.
고정은 편집기 안에서의 수정이 프로토콜이 필요한 이벤트로 전환되는 순간이다. 비용이 급격히 상승함으로써 다른 사람이 안심하고 위에 코드를 쌓을 수 있게 된다.
형태는 고정하고 내부는 자유롭게
interface PaymentGatewayManager
{
// 고정: 이름, 입력, 반환 타입
public function charge(Money $amount, Customer $customer): ChargeResult;
}
- 고정된 부분: 메서드 이름은
charge, 매개변수는Money와Customer, 반환 타입은ChargeResult. - 고정되지 않은 부분: 내부 구현(Stripe, Paddle 등), 사적인 메서드 수 등. 팀 A는 내부를 열 번 바꿔도 팀 B는 전혀 눈치채지 못한다. 계약이 움직이지 않았기 때문이다. 좁은 경계만 고정하고, 그 뒤는 완전 자유롭게 유지한다.
내가 깨달은 점
LaraFoundry를 살펴보니 팀이 존재한다면 살아남을 부분들이 이미 존재하고 있었다.
- 결제 애드온은
EntitlementResolver라는 계약을 통해 코어와 통신한다. - 네비게이션은
MenuProviderInterface를 통해 앱에 연결된다. - 관리자 대시보드는
DashboardWidgetProvider를 통해 위젯을 받아들인다. - 이벤트는 고정된 페이로드를 가진다.
나는 이들을 seam이라고 불렀다. 결제 애드온을 코어 계약만을 이용해 완전히 구현했으며, 그 과정에서 코어는 전혀 변하지 않았다(태그가 그대로 유지됨). 이것이 바로 **고정(freeze)**이 작동하는 모습이다. 애드온은 형태에 기대고, 형태는 유지되었으며, 애드온은 독자적으로 배포되었다.
결국 나는 팀이 병렬로 작업할 때 사용하는 정확한 메커니즘을 우연히 발견했을 뿐, 그 강력한 장점을 혼자서는 필요하지 않았던 것이다.
솔직한 이야기
“첫날부터 완벽하게 계획했다”는 것이 아니다. 내 seam은 처음 필요할 때 단계 안에서 탄생했으며, 미리 고정된 것이 아니다. 오히려 “팀이 어떻게 살아남을까?”라는 질문이 내가 이미 하고 있던 디커플링이 팀이 병렬 작업을 위해 하는 일과 동일하다는 깨달음을 주었다.
거대한 지도를 쓰는 대신 해야 할 일
- 계약을 더 일찍 고정한다. 현재는 seam이 처음 필요해지는 단계와 동시에 등장한다. 팀에서는 먼저 모든 모듈 간 인터페이스와 이벤트 페이로드를 고정하고, 그 뒤에 실제 구현을 진행한다.
- 결정을 기록으로 남긴다. 혼자서는 머릿속과 메모에만 남지만, 팀에서는 작은 버전된 레코드가 레포에 있어야 모두가 왜 테넌시가 fail‑closed인지 질문 없이 알 수 있다.
- 각 의존성을 “계약 차단” 또는 “구현 차단”으로 표시한다. 계약 차단은 고정 순간 해제되고, 구현 차단은 실제 구현이 준비될 때까지 기다린다. 대부분은 전자에 해당한다.
계획 깊이의 차이
- 모듈 경계와 계약: 상세하고 초기 단계에 정의한다. 변경 비용이 크고, 다른 사람을 차단한다.
- 모듈 내부: 스케치 수준, 진행하면서 세부화한다. 변경 비용이 낮고, 누구도 차단하지 않는다.
- 테스트: 실행 가능한 스펙으로 작성한다(문장으로 설명하지 않는다). Pest 테스트는 입력·출력의 공식 기록이다. 그래서 팀은 “어떤 테스트와 반환값을 계획에 적는다”가 아니라 테스트 자체를 작성한다. 내 코어가 Pest 스위트를 갖는 이유가 바로 이것이다—테스트가 계약이며, 테스트에 대한 문단이 아니다.
비유
방의 문을 상세히 설계한다(위치, 너비 등). 그래야 벽이 맞닿는다. 방 안의 가구는 설계하지 않는다. 누가 들어가서 결정한다.
결론
- 추출, 증분, seam이 비복잡 프로젝트의 올바른 형태다.
- 거대한 초기 스키마는 안티패턴이며, 내가 놓친 이상이 아니다.
- 병렬 작업의 비밀은 “모든 것을 적어라”가 아니라 경계의 좁은 계약을 구현이 분기하기 전에 고정하고, 모킹을 활용해 작업한다는 것이다. 내 seam이 바로 그 메커니즘이었다. 팀을 위해 추가할 수 있는 유일한 점은 고정을 더 일찍 하고, **어떤