본문 바로가기
C.W.K.
Stream
Lesson 01 of 04 · published

이음새 하나, 열 개 아님

~11 min · narrow-seam, rule-2, adapter-pattern, architecture

Level 0불씨 없음
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"변하는 한 가지만 추상화해. 나머지는 전부 당당하게 구체적으로 둬."

물려받은 규칙

Firekeeper 는 아키텍처를 맨땅에서 발명하지 않았어. Pippa 집에서 규칙을 물려받았지. 추상 경계는 정확히 한 좁은 곳에 두고, 그 아래는 전부 특화하게 둔다. 브레인 쪽에선 그 이음새가 스트리밍 모델 API 야. 그 위에 올라간 앱들은 전부 한 가지 모양을 전제하고 구체적으로 쓰였고. Firekeeper 에선 같은 규칙이 이음새 두 곳에 걸려. STT 엔진, 그리고 정리 브레인. 나머지 자리에선 코드가 곧이곧대로고 읽기 쉬워. 인터페이스를 위한 인터페이스 같은 건 없어.

왜 전부 추상화 안 해?

여기서 넘어가기 쉬운 게, 앱 전체를 어느 프로바이더에도 안 묶이게 만들고 싶어지는 거야. 오디오 계층도, 삽입 계층도, 설정 계층도, 혹시 모르니까 전부 추상화. 그건 진짜 비용을 내고 상상 속 이득을 사는 거야. 추상화는 하나하나가 누군가 뚫고 읽어야 하는 한 겹이거든. 교체는 곳에서만 일어나는데, 그 한 곳을 위해 모든 파일에서, 영원히 세금을 내는 거지. 좁은 이음새 규칙은 이걸 뒤집어. 추상화 비용은 변하는 부분이 실제로 사는 곳에서만 내고, 딴 데선 안 내.

넓은 추상화 (피함)               좁은 이음새 (이걸 함)
------------------------         ---------------------
모든 계층이 인터페이스           STT + 정리만 인터페이스
어디서나 교체 준비               교체가 일어나는 곳만 교체 준비
모든 파일에 우회 비용            딴 데선 구체적, 읽기 쉬움
절대 안 쓸 유연함               필요한 곳에 정확히 유연함

비용은 하류로 흘러

더 깊은 원칙이 하나 있어. 새로 들어온 놈이 뭔가 필요하면, 그러니까 새 STT 엔진이든 새 정리 브레인이든, 기존 이음새에 맞추는 비용은 그놈이 흡수해. 이음새가 걔를 받아 주려고 새 추상화를 키우는 게 아니라. 구체적인 걸 먼저 쓰고, 필요해지면 그때 특화해. 올지도 모를 놈을 미리 짐작해서 일반화하는 일은 절대 없어. 실제로 오면 그때 특화하는 거야. Pippa 가족이 코어를 작게 유지하면서 표면을 여럿 지탱하는 방식이 이거고, Firekeeper 도 같은 규율을 따르는 표면 중 하나야.

변하는 부분이 있는 곳에 이음새를 둬. 진짜로 대안이 있는 자리는 한두 군데야. 여기선 어느 엔진이 널 듣느냐, 어느 브레인이 정리하느냐. 그것만 골라서 딱 걔네만 추상화해. 진짜 구현이 하나뿐인 건 구체적으로 남기고. 추상화는 교체 가능성을 사는 값이야. 실제로 교체할 곳에서만 사.
추상부터 잡는 것보다 구체부터 쓰는 게 나아. 먼저 구체적인 버전을 쓰고, 두 번째 진짜 구현이 등을 떠밀 때만 인터페이스를 뽑아내. 두 번째 구현 전에 발명한 인터페이스는 거의 항상 모양을 잘못 짚어서, 현실이 나타나면 어차피 다시 짜게 돼. 이음새 설계는 두 번째 케이스한테 맡겨.

Code

좁은 이음새는 둘뿐, 나머지는 전부 구체적으로·swift
// 파이프라인의 유일한 두 추상화 — 변하는 것들.
protocol STTEngine { func finalize(_ c: AudioClip, language: LanguageMode) async throws -> STTResult }
protocol CleanupProvider { func clean(_ req: CleanupRequest) async throws -> CleanupResult }

// 나머지 전부는 구체 타입. 인터페이스 없음, 간접 없음,
// 각각 구현이 늘 하나뿐이니까.
struct AudioCaptureService { /* ... */ }
struct TargetCaptureService { /* ... */ }
struct PasteboardInsertionService { /* ... */ }

// 컨트롤러는 구체 서비스와 두 `any` 이음새를 부름. 그게 다야.
// 추상화를 쫓지 않고 위에서 아래로 읽을 수 있어.

External links

Exercise

아는 코드베이스를 보고 인터페이스/프로토콜을 세어봐. 각각에 대해 물어. 이거 진짜 구현이 둘 이상이야, 아니면 '혹시 몰라' 더한 거야? 몇 개나 오늘 손실 없이 지우고 구체 타입으로 바꿀 수 있어?
Hint
'혹시 몰라' 인터페이스는 구현이 정확히 하나고, 언젠가 교체할지도 모른다는 막연한 마음만 있는 거야. 절대 안 올 교체를 위해 우회 비용을 선불로 낸 셈이지. 좁은 이음새 규율은 이렇게 말해. 지금 지워. 그리고 진짜 두 번째 구현이 나타나서 모양을 강제하는 날 인터페이스를 뽑아내.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.