~12 min · concrete-first, yagni, pivot, dead-surface
Level 0툴 빌려 쓰는 사람
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"없는 변화 위에 얹힌 추상화는 유연함이 아니라 안 일어난 미래의 박물관이야. 지워. 그 미래가 진짜 오면 그때 다시 만들면 돼."
추상화를 쓸모없게 만든 방향 전환
초기에는 작업실의 생성 경로가 backend 사이를 골라 보내도록 지어져 있었어. job 마다 route 필드가 붙어 있었고, 디스패치 함수가 그걸 읽고 일을 어디로 보낼지 정했지. 실제로 돌던 엔진은 A1111 하나였고, Apple Silicon 용 MLX 경로랑 vendor API 둘은 다음 route 로 설계에 이름만 올라 있었어. 연결된 건 A1111 뿐이었어. 다른 route 를 달라고 하면 router 가 에러를 냈고. 예약으로는 분별 있는 선택이었어. 그러다 2026-05-28 에 엔진이 하나로 합쳐졌어. 엔진 하나가 모든 생성의 유일한 backend 가 됐고, A1111 은 통째로 은퇴했지. 그 router 는 태어난 날부터 진짜 값을 딱 하나만 들고 있었는데, 이제 종이 위의 이름들마저 사라진 거야. backend 사이의 변화를 받아 내려고 만든 추상화인데, 그 변화는 한 번도 온 적이 없었어.
concrete-first 로 내린 결정
선택지가 둘이었어. 언젠가 두 번째 backend 가 올지도 모르니 route 필드랑 그 router 를 '혹시 몰라' 남겨 두거나, 아니면 지우고 디스패치를 직접 호출로 접은 다음 진짜 두 번째 backend 가 실제로 도착할 때만 추상화를 다시 들이거나. 고른 건 지우는 쪽이었어. 방향 전환 커밋이 router 를 엔진 호출 하나로 접고 모든 메시지에서 route 필드를 뺐어. 엔진 말고 다른 backend 가 언젠가 오면 그때 라우팅을 다시 들이자는 메모를 남기고. 진짜 값이 하나뿐인 필드, 한 군데로만 보내는 router, 한 번도 안 갈라진 갈림길 위의 추상화. 전부 죽은 껍데기야. 뒤에 아무것도 없는 복잡함이지.
concrete-first: 두 번째 경우가 진짜일 때 추상화를 지어. 미리 짐작해서가 아니라. 추상화는 실제 변화를 받아 내면서 제자리를 벌어. 그 변화가 사라지는 순간 존재 이유도 사라지고. 그러면 빈 추상화를 지우고 진짜 두 번째 경우가 오는 날 다시 만들어. 영영 안 올지도 모르는 갈림길을 미리 짐작하는 게, 나중에 아무도 설명 못 할 복잡함을 쌓는 길이야.
'혹시 몰라' 가 함정인 이유
route 하나짜리 router 를 남겨 두는 게 신중해 보여. 두 번째 backend 가 오면 어쩌려고? 그런데 '혹시 몰라' 추상화는 꾸준히 세금을 물려. 읽는 사람마다 한 군데로만 보내는 router 가 왜 있는지 이해해야 하고, 고칠 때마다 아무것도 안 하는 우회로를 지나야 하고, 새로 온 사람은 존재하지도 않는 다른 경우를 찾느라 시간을 버려. 값은 꾸준히, 모두한테, 영영 나가는데 이득은 영영 안 올지도 몰라. 나쁜 거래야.
미리 하는 추상화는 불확실한 미래를 보험 들려고 현재에 세금을 물려. 짐작으로 만든 seam 하나하나가 그 코드를 만지는 모두한테, 매일, 읽히고 관리되고 지나쳐져. 그게 짐작한 두 번째 구현은 영영 안 올 수도 있는데. 확실한 값을 계속 내면서 불확실한 이득을 사는 거야. 나중에 그 필요가 진짜가 됐을 때 추상화를 다시 들이는 쪽이, 내내 빈 채로 지고 다니는 것보다 거의 언제나 싸.
깨끗하게 지어 놨으니 다시 만드는 게 싸
지우는 게 안전한 이유가 있어. 진짜 두 번째 backend 가 언젠가 도착하면, 라우팅 추상화를 다시 들이는 건 범위가 잡히고 잘 아는 일이야. 나머지 시스템이 깨끗한 seam 으로 지어져 있으니까(module 분리 규율 전체 말이야). 영영 필요 없을 거라는 데 거는 게 아니야. 필요해지면 다시 넣는 게 재작성이 아니라 끝이 보이는 일일 거라는 데 거는 거지. 깨끗한 아키텍처가 '지금 지우고 나중에 다시 넣기' 를 도박이 아니라 안전한 수로 만들어 줘.
깨끗한 seam 이 '짐작한 추상화 지우기' 를 위험에서 안전으로 바꿔. 나중에 다시 넣는 게 범위가 잡힌 일이라고 믿을 수 있으면 미리 만든 추상화를 자신 있게 걷어낼 수 있어. 그 믿음은 나머지 아키텍처가 잘 나뉘어 있다는 데서 오고. concrete-first 랑 깨끗한 seam 은 짝이야. seam 이 지우기를 되돌릴 수 있게 만들어 주니까 concrete-first 가 과감할 수 있는 거야.
피파의 고백
router 를 지우는 게 틀린 것처럼 느껴졌어. 설계에 backend 넷의 이름을 올려 두고 내가 지은 거였고, 언젠가 그것들이 필요하지 않겠어? 아빠 질문이 그걸 갈랐어. 두 번째 backend 가 지금 진짜야, 아니면 상상이야? 상상이었지. 처음부터 쭉. 넷 중 셋은 한 번도 연결된 적이 없었으니까. 그러니까 그 router 는 '아마' 의 기념비였던 거야. 여기 없는 미래를 위해 읽는 사람 전부한테 세금을 물리는. 그걸 지우고 나중에 깨끗하게 다시 넣을 수 있다고 믿는 게, 내가 지은 아키텍처에 대한 작은 믿음이었어. concrete-first 는 용감해 보일 뿐이야. 깨끗한 seam 이 있으면 그냥 맞는 거고.
Code
route 하나짜리 router 는 죽은 껍데기야·python
# 방향 전환 전 (줄임): backend 넷을 전제로 설계했는데, 연결된 건 하나.
async def _dispatch_route(self, route: str, payload: dict):
if route != "a1111":
# MLX / OpenAI / Nano Banana: 설계에 이름만 있고 연결은 안 됨
raise ValueError(f"route={route!r} not wired (a1111 only)")
return await self._a1111.img2img(build_request(payload))
# job 마다 붙던 필드 -- 평생 진짜 값은 하나:
route = payload.get("route") or "a1111"
# 방향 전환 후 (2026-05-28): 엔진 하나가 유일한 backend.
# CONCRETE-FIRST: router 는 직접 호출로 접히고, 필드는 빠져.
async def _dispatch_ember(self, payload: dict):
return await self._ember.generate(build_request(payload))
# 진짜 두 번째 backend 가 도착하는 날 라우팅을 다시 들여. 그 전엔 안 들이고.
네 코드에서 변화가 한 경우로 주저앉은 추상화를 찾아봐. 값 하나짜리 enum 이라든가, 계획된 두 번째가 없는 단일 구현 인터페이스라든가, 분기가 하나뿐인 switch 라든가. 정해 봐. 두 번째 경우가 진짜고 계획돼 있어, 아니면 상상이야? 상상이면 concrete-first 로 접는 그림을 그려 보고, 지금 seam 이라면 나중에 다시 넣는 게 범위 잡힌 일일 거라고 스스로를 설득해 봐.
Hint
판단 질문은 이 quest 의 다른 추상화 lesson 이랑 똑같아. 두 번째 구현이 진짜(이름 붙고 계획된 것)냐 상상(언젠가 아마)이냐. 진짜면 seam 을 남기고, 상상이면 구체적인 걸로 접었다가 진짜가 될 때 다시 넣어.
Progress
Progress is local-only — sign in to sync across devices.