iOS & Swift

Swift 어댑터 패턴(Adapter Pattern), 레거시 API를 프로토콜로 감싸는 법

레거시 API 코드, 지우지도 못하고 그대로 쓰지도 못해서 답답했던 적 있으시죠?

이석우iOS Developer5분 읽기
Swift 어댑터 패턴(Adapter Pattern), 레거시 API를 프로토콜로 감싸는 법 대표 이미지

레거시 API 코드, 지우지도 못하고 그대로 쓰지도 못해서 답답했던 적 있으시죠?

오래된 네트워크 모듈을 새 화면에 붙일 때 누구나 부딪히는 벽이죠.

결론부터 말씀드리면, 레거시 API를 프로토콜(interface)로 감싸고 그 사이에 어댑터를 두는 게 가장 깔끔한 해법입니다.

어댑터 패턴은 서로 안 맞는 두 인터페이스 사이에 “변환기”를 끼워 넣는 방식이에요.

오늘은 Swift에서 이걸 어떻게 적용하는지, 제가 직접 겪은 흐름 그대로 정리해볼게요.

Swift 어댑터 패턴, 레거시와 새 코드 사이에 변환기를 둡니다
Swift 어댑터 패턴, 레거시와 새 코드 사이에 변환기를 둡니다

이 글에서 얻어갈 세 가지

바쁘신 분들을 위해 먼저 정리했어요.

  1. 새 코드가 원하는 형태를 프로토콜로 먼저 정의한다
  2. 레거시 API를 그 프로토콜에 맞게 변환하는 어댑터 타입을 만든다
  3. 화면·뷰모델은 레거시가 아니라 프로토콜에만 의존하게 한다

이 세 가지만 지켜도 나중에 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 하나부터 한번 감싸보세요.

함께 보면 좋은 글