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

어떤 엔진도 위로 새지 않아

~11 min · stt-adapter, leaky-abstraction, contract, engine

Level 0불씨 없음
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"이음새 위에 'WhisperKit' 이란 단어가 나타나면, 추상화는 이미 실패한 거야."

어댑터의 유일한 일

STT 어댑터가 할 일은 자기 엔진을 인터페이스 처럼 보이게 하는 거야. 그 이상은 없어. WhisperKit 엔 자기만의 디코딩 옵션이랑 모델 enum, 오디오 포맷 요구가 있어. whisper.cpp 는 C API 를 내밀고. SpeechAnalyzer 는 async 결과 스트림을 내밀고. 그중 어느 것도 이음새 위로 새면 안 돼. 어댑터가 자기 엔진의 별난 점을 다 삼키고 공유 계약만 내놔. 클립이랑 언어를 주면 텍스트랑 실제로 감지한 언어를 돌려준다, 그게 전부야. 컨트롤러는 any STTEngine 을 쥐고 있을 뿐 자기가 누구랑 얘기하는지 끝까지 안 배워.

누수 테스트

추상화가 진짜인지 확인하는 구체적인 방법이 있어. 이음새 위 코드를 엔진 이름으로 grep 해 봐. DecodingOptionsWhisperKitConfig, 모델 파일명이 DictationController 에서 나오면 추상화가 새는 거야. 컨트롤러가 모른다고 해 놓고 실은 그 디테일에 기대고 있는 거니까. 깨끗한 어댑터라면 그 단어들이 어댑터 파일 안에서만 나와. 그 위는 전부 중립 계약만 말하고.

누수 (나쁨):
  컨트롤러 -> WhisperKit 의 DecodingOptions 직접 읽음   # 엔진이 위로 샘

깨끗 (좋음):
  컨트롤러 -> STTEngine.finalize(clip, language)          # 중립 계약
  WhisperKitEngine -> DecodingOptions 내부에서 만듦          # 별난 점 흡수

계약은 하류가 필요한 걸 담아

반환 타입은 일부러 작게, 대신 빠짐없이 만들었어. 텍스트, 그리고 엔진이 감지한 언어. 왜 언어까지 실을까. 바로 다음 단계인 정리가 그게 필요하고, 그걸 아는 컴포넌트가 엔진뿐이거든. 어댑터가 언어를 떨구면 정리가 다시 감지하거나 추측해야 해. 엔진이 이미 푼 문제를 다시 푸는 거지. 좋은 계약은 하류가 필요한 걸 정확히 담고 그 이상은 안 담아. 엔진 내부 확신 점수도, 토큰 타이밍도 아니고, 앱 나머지가 굴러가는 데 필요한 두 가지 사실만.

어댑터는 별난 점을 자기가 삼켜. 위로 넘기지 않고. 좋은 어댑터인지 재는 잣대는 엔진의 특이한 점이 그 경계에서 딱 멈추느냐야. 어댑터를 쓰려고 호출자가 엔진을 알아야 한다면, 추상화가 아니라 그냥 지나가는 통로를 지은 거야. 설정도, 포맷도, 에러 모양도 다 흡수해. 밖으로는 중립 계약만.
내가 처음 쓴 WhisperKit 어댑터는 샜어. 컨트롤러가 '지금만' 이라면서 디코딩 옵션을 설정하러 손을 뻗고 있었거든. SpeechAnalyzer 엔진을 붙이기 전까진 별문제 없어 보였어. 붙이는 순간 컨트롤러에 분기가 필요해졌지. 'WhisperKit 이면 이 옵션, SpeechAnalyzer 면 저 옵션.' 그 분기가 누수 때문에 치른 값이었어. 옵션을 각 어댑터 안으로 되돌리니까 분기가 사라졌고, 결합도 같이 사라졌어.

Code

엔진의 별난 점이 어댑터 경계에서 멈춰·swift
final class WhisperKitEngine: STTEngine {
    let id = "whisperkit"
    private let kit: WhisperKit

    func finalize(_ clip: AudioClip, language: LanguageMode) async throws -> STTResult {
        // 모든 WhisperKit-특화 설정을 여기서 만듦, 절대 노출 안 함.
        var opts = DecodingOptions()
        switch language {
        case .korean:  opts.language = "ko"
        case .english: opts.language = "en"
        case .auto:    opts.detectLanguage = true
        }
        let out = try await kit.transcribe(audioArray: clip.samples, decodeOptions: opts)
        return STTResult(text: out.text, detectedLanguage: out.language ?? "en")
    }
}
// 컨트롤러는 STTResult 를 봐. DecodingOptions 는 절대 안 봐.

External links

Exercise

네 코드의 어댑터나 래퍼를 하나 골라서, 그 위 코드를 밑바탕 라이브러리의 타입 이름으로 grep 해 봐. 걸리는 건 전부 누수야. 하나 골라서 밑바탕 라이브러리를 갈아끼우려 할 때 뭐가 깨지는지 적어봐. 그 깨지는 게 정확히 누수가 네게 물리는 값이야.
Hint
새어 나온 타입 이름 하나하나가 호출자가 특정 라이브러리에 몰래 기대고 있는 자리야. 라이브러리를 갈아끼우면 그 자리를 전부 손봐야 하고. 어댑터가 막았어야 할 결합이지. grep 에 걸린 개수가 곧 교체가 실제로 물릴 값이야.

Progress

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

댓글 0

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

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