레거시 API 코드, 지우지도 못하고 그대로 쓰지도 못해서 답답했던 적 있으시죠?
오래된 네트워크 모듈을 새 화면에 붙일 때 누구나 부딪히는 벽이죠.
결론부터 말씀드리면, 레거시 API를 프로토콜(interface)로 감싸고 그 사이에 어댑터를 두는 게 가장 깔끔한 해법입니다.
어댑터 패턴은 서로 안 맞는 두 인터페이스 사이에 “변환기”를 끼워 넣는 방식이에요.
오늘은 Swift에서 이걸 어떻게 적용하는지, 제가 직접 겪은 흐름 그대로 정리해볼게요.
이 글에서 얻어갈 세 가지
바쁘신 분들을 위해 먼저 정리했어요.
- 새 코드가 원하는 형태를 프로토콜로 먼저 정의한다
- 레거시 API를 그 프로토콜에 맞게 변환하는 어댑터 타입을 만든다
- 화면·뷰모델은 레거시가 아니라 프로토콜에만 의존하게 한다
이 세 가지만 지켜도 나중에 API를 통째로 교체할 때 수정 범위가 확 줄어듭니다.
저는 이 구조로 바꾼 뒤에 테스트 코드 작성이 훨씬 편해졌어요.
Swift 어댑터 패턴이 왜 필요할까요?
레거시 API는 보통 우리가 원하는 모양이 아니에요.
콜백 기반이거나, 파라미터가 지저분하거나, 반환 타입이 애매하죠.
새 코드가 낡은 API의 사정에 맞춰 휘어지면, 그 낡은 API가 사라지는 날 코드 전체가 흔들립니다.
그래서 중간에 어댑터를 둡니다.
예를 들어 예전 모듈이 이렇게 생겼다고 해볼게요.
// 손대기 애매한 레거시 API (콜백 기반)
class LegacyUserAPI {
func fetch(id: Int,
done: @escaping (NSDictionary?) -> Void) {
// 오래된 네트워크 호출...
}
}
NSDictionary를 그대로 화면까지 끌고 가면, 나중에 이 API를 바꿀 때 화면 코드까지 다 뜯어야 해요.
딱 봐도 붙이고 싶지 않은 모양이죠.
레거시 API를 프로토콜로 감싸는 법 (3단계)
실제로 감싸는 과정은 생각보다 단순합니다.
1단계, 원하는 모양을 프로토콜로 정의합니다.
새 코드가 “이렇게 불렀으면 좋겠다” 하는 형태를 먼저 적어요.
// 새 코드가 원하는 깔끔한 인터페이스
protocol UserRepository {
func user(id: Int) async throws -> User
}
콜백 대신 async/await, NSDictionary 대신 User 타입으로 바꿨어요.
2단계, 어댑터가 레거시를 이 프로토콜에 맞춥니다.
지저분한 변환은 전부 어댑터 안에 가둡니다.
// 레거시를 새 프로토콜로 변환하는 어댑터
struct LegacyUserAdapter: UserRepository {
let legacy = LegacyUserAPI()
func user(id: Int) async throws -> User {
try await withCheckedThrowingContinuation { cont in
legacy.fetch(id: id) { dict in
cont.resume(returning: User(dict))
}
}
}
}
콜백을 async로 바꾸는 withCheckedThrowingContinuation이 여기서 핵심 역할을 해요.
3단계, 화면은 프로토콜에만 의존합니다.
뷰모델은 LegacyUserAPI를 몰라도 됩니다. UserRepository만 알면 돼요.
이렇게 하면 레거시 코드는 어댑터 딱 한 곳에서만 등장하게 됩니다.
어댑터를 쓰면 뭐가 달라질까? (직접 호출과 비교)
직접 호출과 어댑터 방식을 표로 비교해봤어요.
| 항목 | 레거시 직접 호출 | 어댑터로 감싸기 |
|---|---|---|
| API 교체 시 수정 범위 | 화면 전체 | 어댑터 한 곳 |
| 단위 테스트 | 어려움 | 목(mock)으로 쉽게 |
| 새 코드 가독성 | 낮음 | 높음 |
| 초기 작업량 | 적음 | 조금 늘어남 |
초기 작업량이 아주 살짝 늘어나는 건 사실이에요.
하지만 저는 이 비용이 전혀 아깝지 않았습니다.
테스트할 때 UserRepository를 구현한 가짜 객체 하나만 넣어주면 네트워크 없이도 화면 로직을 검증할 수 있거든요.
실제 API가 아직 안 나온 상황에서도 개발을 먼저 시작할 수 있었어요.
자주 묻는 질문 (Q&A)
Q. 어댑터를 struct로 만들까요, class로 만들까요?
상태가 없다면 struct로 충분해요.
레거시 객체를 계속 붙들고 있어야 하거나 참조 공유가 필요하면 class를 씁니다.
Q. 어댑터 패턴이랑 파사드 패턴은 뭐가 다른가요?
어댑터는 “인터페이스를 맞추는” 게 목적이에요.
파사드는 여러 복잡한 걸 “하나로 단순하게 보여주는” 게 목적이라 방향이 조금 달라요.
Q. 프로토콜 이름은 어떻게 짓나요?
레거시 이름을 따라가지 말고, 새 코드 관점에서 원하는 역할로 지으세요.
LegacyUserAPI가 아니라 UserRepository처럼요.
레거시를 억지로 지우려 하지 마시고, 프로토콜과 어댑터로 조용히 격리하는 것부터 시작해보세요.
딱 한 곳에만 낡은 코드를 가둬두면 그다음 리팩터링이 한결 편해집니다. 여러분 프로젝트에서도 제일 지저분한 API 하나부터 한번 감싸보세요.
