[iOS-Swift] Protocol과 Generic의 차이점은 뭘까?

ornwoo·2025년 2월 16일
0

iOS

목록 보기
4/4
post-thumbnail

안녕하세요.
iOS 개발자 비모 🤖 입니다.

오늘은 최근에 기술 면접에서 받았던 질문과 그에 대한 고민을 공유해보고자 합니다.

사전 과제를 진행하면서 서드파티 라이브러리를 사용하지 않고, 직접 구현하여 네트워크 레이어를 구성하려 했습니다.
그 과정에서 Moya 라이브러리를 직접 구현해보는 도전을 하게 되었습니다.



🚀 Moya란..?

Moya는 Alamofire를 기반으로 한 고수준의 네트워크 라이브러리로, API 요청을 보다 모듈화하고 관리하기 쉽게 도와줍니다.

let provider = MoyaProvider<UserAPI>()

// 사용자 정보 가져오기
provider.request(.getUser(id: 1)) { result in
    switch result {
    case .success(let response):
        do {
            let json = try JSONSerialization.jsonObject(with: response.data, options: [])
            print("✅ 성공: \(json)")
        } catch {
            print("⚠️ JSON 변환 실패")
        }
    case .failure(let error):
        print("❌ 네트워크 오류: \(error.localizedDescription)")
    }
}

위와 같이 API 호출을 간편하게 만들기 위해 Moya의 구조를 참고하여 직접 구현해보았습니다.
그러나 면접 과정에서 제가 작성한 NetworkProvider 클래스에 대한 질문을 받았습니다.

👨🏻‍🦱 : “왜 NetworkProvider에서 제네릭을 사용했나요? 프로토콜로 만들면 안 되나요?”

이 질문에 대해 저는 명확한 답변을 하지 못했고,
면접이 끝난 후 깊이 고민해보며 제네릭과 프로토콜의 차이를 공부하게 되었습니다.



💡 NetworkProvider를 제네릭으로 설계한 이유

//
// MARK: - NetworkProvider
//

class NetworkProvider<T: TargetType> { // << 이부분
    
    /// 네트워크 요청을 수행합니다.
    ///
    /// - Parameters:
    ///   - target: 요청 정보를 포함한 `TargetType` 객체.
    /// - Returns: `AnyPublisher<Data, Error>`로 반환된 네트워크 응답 데이터.
    func request(_ target: T) -> AnyPublisher<Data, Error> {
        ... 생략
    }
}

당시 저는 Moya의 구조를 참고하여 여러 API 요청을 TargetType으로 나누어 관리하고자 했습니다.
예를 들어, UserAPI, PostAPI 등의 다양한 API 요청을 유연하게 처리하려면, 제네릭을 사용하면 타입을 명확하게 정의하고 확장하기 용이하다고 생각했기 때문입니다.

즉, NetworkProvider가 다양한 API 요청을 처리할 수 있도록 유연하게 설계하기 위해 제네릭을 적용했습니다.

하지만 면접관이 질문한 “프로토콜로 만들면 안 되나요?”라는 부분에서, 저는 제네릭과 프로토콜의 성능 차이에 대해 깊이 고민하지 않았다는 걸 깨달았습니다.



🤔 면접관이 질문한 이유

단순히 “제네릭을 적용하는 것이 적절했는가?“를 묻는 것이 아니라,

“제네릭과 프로토콜을 사용할 때의 차이점과 성능 영향을 이해하고 있는가?”

를 확인하려는 의도가 있었을 가능성이 큽니다.

그렇다면 제네릭 vs 프로토콜의 차이는 무엇일까요?



✅ 제네릭 vs 프로토콜: 어떤 차이가 있을까?

📌 제네릭(Generic)

  • 컴파일 타임에 타입이 확정되며, 런타임 오버헤드가 없음
  • 런타임에 타입 캐스팅이 필요하지 않으므로 성능이 더 우수
  • 사용 예: 네트워크 요청을 여러 타입으로 확장할 때
class NetworkProvider<T: TargetType> {
    func request(_ target: T) -> AnyPublisher<Data, Error> { ... }
}

✅ 장점

  • T가 제네릭으로 정의되어 있어 컴파일 타임에 구체적인 타입이 결정됨
  • 즉, NetworkProvider<UserAPI>, NetworkProvider<PostAPI> 와 같이 각각의 타입별로 컴파일 시점에 최적화됨
  • 불필요한 타입 캐스팅이 없으므로 성능이 유리

⚠️ 단점

  • 새로운 API 타입마다 클래스가 별도로 생성되므로 코드 크기가 증가할 수 있음

📌 프로토콜(Protocol)

  • 런타임에 다형성(polymorphism)이 결정됨
  • 동적 디스패치(VTable)를 사용하므로, 런타임에서 성능 오버헤드가 발생할 가능성이 있음
  • 사용 예: 공통 인터페이스를 제공하여 여러 구현체를 유연하게 다룰 때
protocol NetworkService {
    func request(target: TargetType) -> AnyPublisher<Data, Error>
}

class NetworkProvider: NetworkService {
    func request(target: TargetType) -> AnyPublisher<Data, Error> { 
        // 네트워크 요청 처리
    }
}

✅ 장점

  • 유연성과 확장성이 뛰어남
  • 네트워크 레이어를 추상화할 때, 프로토콜을 사용하면 각 API가 동일한 인터페이스를 따르도록 강제할 수 있음

⚠️ 단점

  • VTable을 사용하여 동적 디스패치를 수행하므로, 런타임 오버헤드가 발생할 수 있음

📌 즉, 성능을 최적화하려면 가능한 경우에는 제네릭을 사용하고, 확장성과 유지보수를 고려해야 하는 경우 프로토콜을 활용하는 것이 좋음!

Moya 같은 네트워크 라이브러리는 다양한 API를 다루기 때문에 제네릭을 활용하는 것이 성능적으로 더 효율적!



다시 면접 질문에 대한 답변을 정리하자면…

👨🏻‍🦱 :

“왜 제네릭을 사용했나요? 프로토콜로 만들면 안 되나요?”

🙆🏻 :

네, 프로토콜을 사용할 수도 있지만, 저는 API 호출을 여러 개 관리해야 했기 때문에 제네릭을 적용했습니다.

이렇게 하면 API마다 타입을 명확하게 정의할 수 있어 가독성이 높아지고, 런타임 성능 오버헤드 없이 빠르게 동작할 수 있습니다.

반면 프로토콜을 사용하면 다형성을 제공할 수 있지만, 런타임에서 동적 디스패치가 일어나므로 성능적으로는 제네릭이 더 적합하다고 판단했습니다.

라고 대답할 것 같습니다.



배운 점

면접을 통해 단순히 Moya를 구현하는 것이 아니라,
제네릭과 프로토콜의 차이를 깊이 고민할 필요성을 깨달았습니다.

이후 WWDC 2024 Swift Performance 세션을 보고,
제네릭과 프로토콜이 컴파일 및 런타임에서 어떻게 다르게 동작하는지 공부하게 되었고,

앞으로는 기능 구현뿐만 아니라, 성능과 유지보수 측면까지 고려하여 개발하는 습관을 가지려 합니다.

읽어주셔서 감사합니다!
비모 🤖였습니다. 😊

profile
🧑🏻‍💻 정신과 시간의 방 🧘🏻

0개의 댓글