Part 2 — VPS 한 대를 네 명이 나눠 씁니다 — 역할별 권한·홈·포트 격리 설계
16GB 맥북의 한계를 느껴 빌린 96GB·18코어 서버를 개발자 Jieun·Sungsuk, 디자이너 Juhee, 마케터 Jina가 함께 쓰게 됐습니다. 역할에 따라 권한과 프로젝트 홈, 포트를 나누고 운영 서버는 공용 터널 서비스 뒤에 격리했습니다.
목차
들어가며
1부에서 맥북·서버·운영으로 나눈 3단 구조를 정리했습니다. 이번엔 그 가운데 서버를 네 명이 실제로 나눠 쓰면서, 사람마다 다른 역할과 권한을 한 장비 안에서 어떻게 나눴는지를 적습니다.
저는 개발자이고 Juhee는 디자이너입니다. 같은 서버에 접속한다고 해서 같은 권한과 작업공간을 줄 이유는 없었습니다. 개발자에게는 셸과 코딩 에이전트가 필요하지만, 디자이너에게 운영 서버의 키나 다른 사람의 폴더까지 열어줄 필요는 없습니다.
기준은 간단했습니다. 사람의 계정·홈·포트는 나누고, 운영으로 가는 공유 자원은 사람 계정에서 떼어냅니다. 프로젝트는 각자의 홈 아래에 두고 dev 포트도 계정별로 배정합니다. 모두가 써야 하는 운영 API 터널은 제한된 서비스 계정으로만 붙입니다.
세션을 여러 개 열면 dev 서버가 메모리를 계속 점유할 수 있다는 건 처음부터 예상했습니다. 그래서 수치는 문제를 발견하려고 잰 것이 아니라 실제 규모와 우선순위를 확인하려고 잰 것입니다. 작업 폴더와 dev 서버의 비용을 나눠 본 뒤, 프로세스 회수까지 포함해서 계정·홈·포트 경계를 설계했습니다.
1. 역할이 다르면 권한·홈·포트도 달라야 합니다
공용 서버를 쓴다는 건 모두가 같은 사람이 된다는 뜻이 아닙니다. 자원은 공유하되, 그 자원에 접근하는 이유와 책임은 분리해야 했습니다.
| 역할 | 필요한 것 | 열지 않는 것 | 작업 위치 |
|---|---|---|---|
| 개발자 (Jieun·Sungsuk) | SSH 셸, 코딩 에이전트, dev 서버 실행, 자기 브랜치 작업 | 운영 서버 자격증명, 다른 사람의 홈과 프로세스 | 각자 홈 아래 레포·worktree |
| 디자이너 (Juhee) | 화면 확인, 필요한 범위의 개발 작업, 자기 dev 포트 | 운영 키, 운영 DB 직접 접근, 다른 사람의 작업공간 | 자기 홈 아래 프로젝트 |
| 마케터 (Jina) | 필요한 화면 확인과 콘텐츠 업무 범위 | 운영 키, 셸·에이전트, 다른 사람의 작업공간 | 자기 홈 아래 프로젝트 |
| 공유 터널 서비스 | 운영 API로 가는 단일 포워딩 | 셸, sudo, 에이전트, 임의의 내부 포트 | wdot-tunnel 전용 홈 |
Juhee와 Jina에게는 서버 전체를 여는 대신 도쿄 VPS 접속 안내와 각자의 전용 키를 전달했습니다. 처음 한 번 SSH 설정을 만들면 이후에는 정해진 별칭으로 접속합니다. 접속 방법은 단순하게 만들되 운영 권한과 다른 사람의 작업공간은 열지 않았습니다.
개발자·디자이너·마케터의 차이는 신뢰의 문제가 아니라 필요한 표면의 차이입니다. 화면을 봐야 한다고 운영 서버에 로그인할 필요는 없고, 에이전트를 돌린다고 운영 DB 자격증명을 홈에 둘 필요도 없습니다.
운영 서버는 더 바깥으로 밀었습니다. 사람의 셸이 운영 키를 직접 들고 있지 않고, nologin 계정의 systemd 서비스가 허용된 목적지 하나로만 터널을 엽니다. 사람은 공용 서버의 127.0.0.1:8787만 봅니다.
사람이 늘어도 추가할 것은 계정·홈·포트 슬롯입니다. 다른 사람의 키나 운영 권한을 섞지 않아도 됩니다.
2. 무엇을 아껴야 하는지부터 정했습니다
지금 당장 서버가 부족한 것은 아니었습니다. 96GB와 18코어면 네 명이 쓰기에도 여유가 있고, 나중에 e2e 환경을 올릴 공간도 남아 있습니다. 하지만 켜 둔 프로세스를 계속 쌓으면 새 작업을 시작할 때마다 무엇을 꺼야 하는지 확인해야 합니다. 그래서 처음부터 놀고 있는 자원을 그때그때 정리하는 것을 설계에 넣었습니다.
디스크·CPU·메모리 중 여러 세션과 e2e 환경을 제한할 자원이 무엇인지 확인하고, 여유를 유지하려면 무엇을 회수해야 하는지 우선순위를 정했습니다.
| 항목 | 값 |
|---|---|
| 디스크 | 581GB 중 7GB 사용 (2%) |
| 메모리 | 96GB 중 3GB 사용 |
| 부하 | 18코어에 load 0.2 |
당장은 전혀 힘든 상태가 아니었습니다. 단위당 비용은 이랬습니다.
| 무엇 | 크기 |
|---|---|
| 작업 폴더 하나 | 82MB (디스크) |
.git | 112MB — 공유됩니다. 복사되지 않습니다 |
node_modules | pnpm 전역 스토어에서 하드링크 |
| 코딩 에이전트 세션 | 약 350MB (메모리) |
| dev 서버 하나 | 1,259MB → 3,759MB (4시간, 메모리) |
작업 폴더는 쌉니다. .git은 공유되고 node_modules는 하드링크라 실제 증가분은 소스 코드뿐입니다. 반면 e2e 테스트 컨테이너와 작업량이 늘어나면 지금의 여유가 빠르게 줄 수 있습니다.
비싼 건 켜 둔 dev 서버입니다. 하나가 에이전트 세션 열 개보다 무겁고, 켜 둘수록 자랍니다.
폴더 자체보다 켜 둔 프로세스의 비용이 더 큰 상황이었습니다. 걱정할 것은 폴더 개수가 아니라 켜 둔 프로세스였습니다.
그래서 폴더보다 프로세스를 더 챙기고, 놀고 있는 프로세스는 회수하는 쪽으로 설계를 정했습니다.
3. 남의 화면을 보지 않게 포트를 나눴습니다
여러 사람이 한 서버에서 작업할 때 가장 먼저 없애야 했던 건 성능 문제가 아니라 착각이었습니다. 내 서버를 띄웠다고 생각했는데 실제로는 다른 사람의 화면을 보고 있는 상황은, 코드가 잘못된 것보다 원인을 찾기 어렵고 작업 자체를 믿을 수 없게 만듭니다. 그래서 포트 충돌을 경고하는 데 그치지 않고, 계정마다 포트를 미리 배정하고 남의 포트라면 기동을 거부하도록 했습니다.
next dev는 포트가 점유돼 있으면 에러를 내지 않습니다. 말없이 다음 번호로 옮겨서 뜹니다.
그러면 이런 일이 생깁니다. 화면을 띄웠다고 생각하고 localhost:3000을 열었는데 그건 남이 띄운 화면입니다. 내가 방금 고친 코드는 3001에 있습니다. 본인은 모릅니다.
문서에 “3001로 밀리니 주의”라고 적혀 있었지만 경고는 사고를 막지 못했습니다.
계정별로 번호를 배정했습니다
| 슬롯 | 계정 | admin | web | hub |
|---|---|---|---|---|
| 0 | A | 3000 | 4321 | 4331 |
| 1 | B | 3100 | 4421 | 4431 |
| 2 | C | 3200 | 4521 | 4531 |
| 그 밖 | (미배정) | 3900 | 5221 | 5231 |
포트를 아는 자리는 스크립트 한 곳뿐입니다. 문서에도, 다른 코드에도 번호를 적지 않았습니다. 나중에 바꿀 때 한 곳만 고치면 됩니다.
슬롯 0이 3000인 건 취향이 아닙니다. 구글 OAuth 콜백이 등록된 포트라 실제 로그인이 되는 유일한 자리였습니다. 나중에 다른 포트도 등록해서 풀었습니다.
기동을 스크립트 하나로 묶었습니다
scripts/local-up.sh순서대로 이걸 합니다.
- 내 포트가 떠 있는지 봅니다. 내 것이면 주소만 안내하고, 남의 것이면 멈춥니다.
- 공유 API 터널을 확인합니다.
.env.local과node_modules를 확인합니다.- 백그라운드로 띄우고
curl로 확인한 뒤 주소를 안내합니다.
핵심은 1번인데, 판정 방법이 재미있었습니다. ss -ltnp는 내 프로세스에만 users:(("next-server",pid=...))를 붙입니다. 그래서 “떠 있는데 내 것이 아니다”를 그것으로 가릅니다.
listening() { ss -ltn | grep -q "127\.0\.0\.1:$1 "; }mine() { ss -ltnp | grep ":$1 " | grep -q 'users:'; }4. 운영 접근은 사람의 계정에서 떼어냈습니다
개발 서버는 네 명이 함께 써도 되지만, 운영으로 이어지는 통로까지 사람에게 나눠주면 안 된다고 봤습니다. 누가 출근했는지, 어느 노트북이 켜져 있는지에 따라 운영 화면이 달라지는 구조는 공유 자원이 아니라 개인의 생활에 매달린 장애물이 되기 때문입니다. 그래서 운영 터널은 사람의 세션이 아니라 제한된 서비스가 소유하도록 바꿨습니다.
포트는 나눠야 했는데, 운영 api로 가는 SSH 터널은 반대로 공유해야 했습니다. 같은 장비의 루프백이라 하나를 네 명이 같이 쓰면 되고, 사람 수만큼 띄우면 두 번째부터 ExitOnForwardFailure로 실패합니다.
처음부터 지금 구조였던 것은 아닙니다. 8월에는 디자이너가 자기 맥북에서 admin을 띄우고, 제한된 SSH 키로 운영 서버의 API 포트에만 닿는 구조였습니다. 그때의 목표는 “디자이너 로컬 BFF가 API를 볼 수 있게 하되, 셸과 다른 내부 포트는 열지 않는다”였습니다. 9월에 개발 서버를 네 명이 함께 쓰게 되면서 목표가 바뀌었습니다. 개인 노트북마다 터널을 들고 있는 방식이 아니라, 공유 VPS 안의 공용 서비스가 터널 하나를 책임져야 했습니다.
“공유한다”까지는 금방 정했습니다. 그런데 그 하나를 누가 무는지를 확인해보니 이랬습니다.
$ ss -ltnp | grep :8787127.0.0.1:8787 users:(("sshd",pid=78803)) ← ssh 클라이언트가 아닙니다sshd가 물고 있었습니다. 서버가 나가서 연결한 게 아니라, 누군가 들어와서 밀어넣고 있었습니다. 한 사람의 맥북이 ssh -R로 역방향 포워딩을 걸어둔 상태였습니다.
8월에 정했던 “제 노트북이 중계에 개입하지 않아야 한다”는 기준이 공유 VPS로 옮기는 과정에서는 지켜지지 않고 있었습니다.
잘못이 아니라 우회였습니다
운영 키가 공용 장비에 없었습니다. 기존에는 키를 가진 사람은 한 명뿐이었고, 그래서 자기 노트북에서 역방향으로 터널을 밀어 네 명이 쓰게 만든 것입니다. 키를 나눠 갖지 않으면서 넷이 쓰게 하는 합리적인 우회였습니다.
다만 이렇게 됩니다.
| 위험 | 내용 |
|---|---|
| 단일 실패점이 노트북 | 그 사람이 자거나 사무실을 벗어나면 네 명 전원 다운 |
| 재접속 없음 | 끊기면 그 사람이 수동으로 다시 띄워야 합니다 |
| 재부팅 복구 없음 | 장비를 재부팅하면 아무도 못 씁니다 |
| 안내가 막다른 길 | 스크립트가 “키로 띄워라”는데 그 키가 장비에 없습니다 |
마지막 문제가 가장 컸습니다. 터널이 끊겼을 때 스크립트가 찍어주는 지시가 no such identity로 끝납니다. 가장 급한 순간에 안내가 실행 불가능했습니다.
(예전에 만든 터널 설치 스크립트도 launchd 기반 macOS 전용이었습니다. 디자이너 맥북용이라 리눅스에서는 애초에 안 도는데, 문서가 그걸 실행하라고 안내하고 있었습니다. 아무도 실행해본 적이 없어서 몰랐습니다.)
이 시점에서 /local 절차도 정리했습니다. dev 서버를 켜기 전에 계정별 포트가 비어 있는지, 공용 API 터널이 응답하는지, .env.local과 node_modules가 준비됐는지 확인하게 했습니다. 단순히 next dev만 실행하면 터널 장애가 화면 코드 오류처럼 보이거나, Next.js가 빈 포트를 찾아 밀려 뜨는 상황이 생깁니다. 그래서 3절의 기동 스크립트가 포트 확인과 터널 확인을 먼저 수행하도록 묶었습니다.
바꾼 것은 한 군데뿐입니다
전체 경로에서 8787을 누가 무는가, 이 하나만 바꿨습니다.
[전] 기계 4대 내 맥북 공용 서버 A의 맥북 운영 서버 ─────── ───────── ──────── ───────── 브라우저 ──▶ next dev │ API_BASE=127.0.0.1:8787 ▼ :8787 ◀── sshd ── ssh -R 8787 ──▶ :8987
[후] 기계 3대 내 맥북 공용 서버 운영 서버 ─────── ───────── ───────── 브라우저 ──▶ next dev │ API_BASE=127.0.0.1:8787 ▼ :8787 ── wdot-api-tunnel (systemd) ──▶ :8987가운데 노트북 칸이 사라졌습니다.
계정을 새로 만들었습니다
wdot-tunnel uid 999 · nologin · sudo 없음 · wheel 없음기존 admin 계정에 두려다 그만뒀습니다. uid 1000에 sudo와 wheel이 둘 다 있고 사람이 실제로 쓰는 계정이라, 운영 키를 거기 두면 목적과 반대로 갑니다. 로그인 자체가 불가능한 계정을 새로 만드는 게 맞았습니다.
키 자체도 “운영 서버에 로그인할 수 있는 키”가 아니라 “정해진 포트포워딩만 허용하는 키”로 만들었습니다. 여기서 중요한 구분은 인증과 인가입니다. 공개키 인증은 “이 키를 가진 주체가 맞는가”를 확인합니다. authorized_keys의 옵션은 그다음에 “인증된 키가 무엇을 할 수 있는가”를 좁힙니다.
SSH 연결 안에서도 셸을 여는 session 채널과 로컬 포워딩을 여는 direct-tcpip 채널은 별도로 동작합니다. 그래서 session 채널을 막아도 포워딩 채널만 허용할 수 있습니다.
restrict,port-forwarding,permitopen="127.0.0.1:8987",command="/bin/false" ssh-ed25519 AAAA... wdot-api-tunnel각 옵션의 의미는 이렇습니다.
| 옵션 | 역할 |
|---|---|
restrict | pty, X11, agent forwarding, 포트포워딩, user-rc 등 기본 기능을 끕니다 |
port-forwarding | 꺼진 기능 중 포워딩만 다시 허용합니다 |
permitopen | -L 로컬 포워딩의 목적지를 운영 서버의 127.0.0.1:8987 하나로 제한합니다 |
command="/bin/false" | session 채널로 명령이 들어와도 셸 대신 실패하는 명령만 실행합니다 |
OpenSSH의 permitopen 설명에 따르면 이 옵션은 로컬 포워딩에서 클라이언트가 연결할 수 있는 목적지를 제한합니다. 예를 들어 -L 8787:127.0.0.1:8987은 허용되지만, 같은 키로 -L 8787:127.0.0.1:5432처럼 DB 포트에 닿는 터널을 만들 수는 없습니다. 다만 이것만으로 모든 종류의 -R 역방향 포워딩까지 같은 방식으로 제한된다고 말할 수는 없습니다. 여기서 설명하는 설정의 범위는 이 서비스가 사용하는 로컬 포워딩 목적지와 셸·명령 실행의 제한입니다.
127.0.0.1:8987을 목적지로 고정하려면 운영 서버 쪽에도 안정된 호스트 포트가 필요했습니다. API 컨테이너의 내부 IP는 배포 때 바뀔 수 있으므로 172.x.x.x:8787 같은 주소를 permitopen에 박을 수 없습니다. 그래서 운영 서버의 루프백에만 8987을 열고, 그 포트가 API 컨테이너의 8787로 이어지게 했습니다. 인터넷에 새 문을 연 것이 아니라, 서버 자기 자신에게만 보이는 고정 주소를 만든 것입니다.
운영 서버의 Compose 설정은 다음과 같습니다. 127.0.0.1 바인딩을 유지해야 호스트 외부 인터페이스로 공개되지 않습니다.
ports: ["127.0.0.1:8987:8787"]-L 8787:127.0.0.1:8987에서 목적지 127.0.0.1은 SSH 접속을 받는 운영 서버 기준입니다. 목적지가 고정되면서 컨테이너 IP를 조회하던 준비 스크립트도 제거할 수 있었습니다. 공유 VPS의 wdot-tunnel은 SSH 클라이언트를 실행하는 서비스 계정이고, 위 authorized_keys 제한은 운영 서버가 접속 키에 적용하는 규칙입니다. 두 서버의 역할을 구분해야 합니다.
이 제한은 DB 직접 접속을 막는 장치입니다. API 연결 자체가 안전하다는 뜻은 아닙니다. API 안에 저장·발행·삭제 같은 기능이 있으면 그 HTTP 권한은 여전히 운영 데이터에 영향을 줄 수 있습니다. 그래서 DB 자격증명과 셸 권한은 분리하되, API에서 가능한 쓰기 작업은 별도의 화면 권한과 확인 절차로 다뤄야 합니다.
[Service]User=wdot-tunnelExecStart=/usr/bin/ssh -N -o ExitOnForwardFailure=yes \ -o ServerAliveInterval=30 -o ServerAliveCountMax=3 \ -o StrictHostKeyChecking=yes \ -o UserKnownHostsFile=/home/wdot-tunnel/.ssh/known_hosts \ -i /home/wdot-tunnel/.ssh/<키> \ -L 8787:127.0.0.1:8987 ubuntu@<운영서버>Restart=alwaysRestartSec=5
[Install]WantedBy=multi-user.target이렇게 바꾸면 터널의 소유자가 사람에서 서비스로 바뀝니다. 재부팅 뒤에도 systemd가 다시 올리고, 중간에 끊기면 재시작합니다. 키를 회수할 때도 사람에게 연락해 노트북 설정을 지우게 하는 대신, 운영 서버의 authorized_keys에서 해당 줄을 삭제하면 됩니다. 이 조치는 새 인증을 막습니다. 이미 맺어진 연결을 즉시 끊는 것은 별도의 세션 종료나 서비스 중지가 필요합니다.
8월의 작업 기록에서는 제한 키의 셸·명령 실행 차단을 실제로 확인했습니다. 허용 목적지는 permitopen 설정으로 고정했고, 9월의 공유 서비스에는 Restart=always로 재시작 동작을 구성했습니다. 이 기록을 모든 SSH 포워딩 유형이나 장애 복구 시나리오까지 검증했다는 뜻으로 확대하지는 않습니다.
별도 미리보기 환경을 만드는 선택지도 있었습니다. 브랜치마다 URL을 만들어 주면 협업자는 자기 노트북에서 터널을 다룰 필요가 없습니다. 다만 당시에는 코드를 고치면서 바로 확인하는 개발 흐름이 더 중요했고, 공용 VPS가 이미 그 역할을 맡고 있었습니다. 그래서 미리보기 환경은 나중의 배포 편의 문제로 두고, 이번 글에서는 공유 개발 서버의 권한과 터널 소유권만 정리했습니다.
5. 작업을 섞지 않기 위해 폴더를 브랜치에 묶었습니다
사람별로 폴더를 나누는 이유는 정리하기 편해서만이 아닙니다. 같은 폴더에서 브랜치를 바꿔가며 여러 세션이 작업하면 한 세션의 상태가 다른 세션을 오염시킵니다. 그래서 작업 하나를 브랜치 하나와 폴더 하나에 묶고, 각자의 홈 안에서만 그 상태를 관리하게 했습니다.
세션을 여러 개 열고 동시에 작업하면 브랜치 갈아타기가 깨집니다. git worktree로 작업 하나 = 브랜치 하나 = 폴더 하나로 만들었습니다.
scripts/wt.sh feat/기능이름# → .worktrees/YYYY-MM-DD/feat-기능이름/규칙은 날짜와 브랜치 이름, 이 하나입니다. 날짜로 묶은 이유는 둘입니다. 찾기(“어제 뭐 했더라”)와 버리기(오래된 폴더가 눈에 띕니다).
자리는 레포 안 .worktrees/입니다. 이미 .gitignore에 “로컬 격리 작업공간”으로 등록돼 있던 자리를 그대로 썼습니다.
작업 폴더 안에서 스크립트를 실행해도 폴더가 한 군데 모여야 합니다. 그래서 현재 위치가 아니라 본체를 직접 묻습니다.
REPO="$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")"--git-common-dir는 worktree 안에서도 **본체의 .git**을 가리킵니다.
6. 지워도 되는지는 날짜가 아니라 작업의 상태로 판단합니다
자동 정리에서 가장 중요한 결정은 “얼마나 오래됐는가”가 아니라 “정말 끝난 작업인가”였습니다. 오래된 폴더를 지우는 것은 간단하지만, 오래 열어둔 작업이 아직 필요할 수 있고 오늘 만든 작업도 이미 끝났을 수 있습니다. 그래서 날짜는 찾기 쉽게 묶는 기준으로만 쓰고, 삭제 여부는 작업의 저장·push·PR 상태로 판단했습니다.
scripts/wt-gc.sh # 미리보기 — 아무것도 지우지 않습니다scripts/wt-gc.sh --apply # 실제 정리기본이 미리보기입니다. 목록만 보고 싶을 때 부담이 없어야 실제로 쓰게 됩니다.
넷을 순서대로 보고 하나라도 걸리면 손대지 않습니다.
① 저장 안 한 게 있나? → 있으면 유지② push 안 한 커밋이 있나? → 있으면 유지③ PR이 아직 열려 있나? → 열려 있으면 유지④ PR이 머지 없이 닫혔나? → 유지 + "확인" 표시──────────────────────────────────────────── 전부 아니고 PR이 머지됨 → 지웁니다날짜는 기준이 아닙니다. 3개월 전 폴더인데 PR이 열려 있으면 영원히 안 지우고, 오늘 만든 폴더인데 머지됐으면 한 시간 안에 지웁니다. “오래된 것”이 아니라 **“끝난 것”**을 지웁니다.
①에는 한 번도 add한 적 없는 새 파일도 걸립니다. 메모하려고 만든 파일 하나 때문에 폴더가 살아남습니다. 그게 맞다고 봤습니다.
순서에도 이유가 있습니다. ①②는 로컬만 보면 되니 빠르고 ③④는 GitHub에 물어야 해서 느립니다. 그리고 잃을 게 큰 순서입니다. ①에 걸리는 건 커밋도 안 된 작업이라 복구 방법이 없습니다.
PR 상태를 권위로 삼은 이유
squash로 머지하면 브랜치의 커밋들이 하나로 합쳐져 main에 들어갑니다. 원래 커밋은 main 어디에도 없습니다. 그래서 git에게 “이 브랜치 머지됐어?”라고 물으면 **“아니오”**라고 답합니다. 실제로는 머지됐는데도요.
git 말만 믿으면 머지된 폴더를 하나도 못 지웁니다. 그래서 GitHub에 PR 상태를 직접 묻습니다.
같은 이유로 브랜치 삭제도 git branch -d(안전)가 아니라 -D(강제)를 써야 합니다. 소문자 -d는 “머지 안 된 브랜치는 못 지운다”며 항상 거부합니다. git의 안전장치를 끈 게 아니라 더 정확한 안전장치로 바꿔 단 것입니다. ③에서 이미 머지를 확인했기 때문입니다.
dev 서버는 보수적으로 둘로 갈랐습니다
| 상황 | 자동(--apply) | 손으로(--reap) |
|---|---|---|
| 고아 (작업 폴더가 이미 없음) | 끕니다 | 끕니다 |
| 폴더는 살아 있고 오래 조용함 | 안 건드림 (표시만) | 끕니다 |
| 작업 중 | 안 건드림 | 안 건드림 |
고아를 자동에 넣은 이유가 중요합니다. 정리 도구가 폴더를 지우는 순간 그 안에서 돌던 서버가 바로 고아가 됩니다. 이걸 같이 안 치우면 디스크 82MB 아끼려고 메모리 3.7GB를 버려두는 꼴이 됩니다. 정작 새는 곳이 안 막힙니다.
반대로 “폴더는 살아 있는데 조용하다”는 추측입니다. 로그와 .next의 수정 시각으로 판정하는 어림짐작이라 자동에서 뺐습니다. 다만 잃는 게 없습니다. dev 서버는 데이터가 아니라 다시 켜면 되는 것입니다.
크론이 정확히 하는 일
크론은 막연히 오래된 폴더를 지우는 작업이 아닙니다. 각 계정의 권한으로 매시간 scripts/wt-gc.sh --apply를 실행하고, 그 계정의 프로젝트 홈 아래에 있는 .worktrees/YYYY-MM-DD/만 검사합니다. 운영 서버나 다른 사람의 홈은 건드리지 않습니다.
실행 순서는 이렇습니다.
- 스크립트가 놓인 레포의 본체 위치를 찾습니다. 크론의 현재 디렉터리는 홈일 수 있으므로 스크립트 위치를 기준으로 레포를 계산합니다.
- 날짜별 worktree를 하나씩 봅니다.
- 저장하지 않은 변경, push하지 않은 커밋, 아직 열린 PR이 있으면 유지합니다.
- PR이 머지 없이 닫혔으면 삭제하지 않고
확인 필요로 표시합니다. - 위 조건이 모두 없고 PR이 머지된 worktree만 브랜치와 함께 삭제합니다. 기본 명령은 미리보기이고, 크론만
--apply를 사용합니다. - worktree가 이미 사라진 고아 dev 서버가 있으면 그 프로세스도 종료합니다. 반대로 worktree는 살아 있지만 단지 오래 조용한 dev 서버는 추측만으로 끄지 않습니다.
즉 크론이 하는 일은 끝난 작업공간을 매시간 정리하고, 작업공간 없이 남은 dev 프로세스를 회수하는 것입니다. 운영 API 터널을 재시작하거나, 현재 작업 중인 서버를 끄거나, 유휴 시간을 근거로 사람의 작업을 판단하는 일은 하지 않습니다.
마무리하며
| 기준 | 결과 |
|---|---|
| 남의 화면을 보는 일이 없어야 한다 | ✅ 포트 배정 + 남의 포트면 기동 거부 |
| 공유 자원이 사람에 매달리면 안 된다 | ✅ 터널을 nologin 계정 systemd 서비스로 |
| 끝난 작업은 알아서 치워져야 한다 | ✅ PR 머지된 것만, 매시간 |
| 작업물이 사라질 길이 없어야 한다 | ✅ 네 단계 중 하나라도 걸리면 손대지 않음 |
| 정리가 사람 기억에 의존하면 안 된다 | ⚠️ 크론은 걸었지만 유휴 dev 서버는 여전히 손으로 꺼야 합니다 |
마지막 줄은 일부러 남겼습니다. 유휴 여부만으로 dev 서버를 종료하기에는 근거가 약합니다. 방치가 계속 문제가 되면 활동 기준을 더 정교하게 만들 수 있지만, 지금은 96GB 중 3GB만 사용하고 있어 우선순위가 높지 않습니다.
이번 설계에서 가장 큰 이득은 문제가 커진 뒤에 대응할 경계를 미리 정해둔 것입니다. 자원을 측정해 디스크보다 메모리를 먼저 관리해야 한다는 것을 확인했고, 프로세스 회수와 역할별 권한을 함께 설계했습니다. 이후 실제 크론과 로그를 적용하면서 작업이 끝난 경우와 아직 살아 있는 경우를 구분할 수 있었습니다. 미리 전제하고 작은 범위에서 검증했기 때문에, 사람의 기억이나 주의에만 의존하는 자동화를 피할 수 있었습니다.
댓글