본문 바로가기
개린이 이야기

타입으로서 프로토콜에 관하여

by iOS 개린이 2023. 6. 5.

타입으로서 프로토콜

일급객체 프로토콜

-Swift에서 프로토콜은 일급 객체다.

일급 객체란 특정 타입이 다음과 같은 특성을 가지고 있을 때를 말한다.

 

1) 변수나 데이터 구조에 저장 가능

2) 함수의 매개변수로 전달 가능

3) 함수의 반환값으로 사용 가능

4) 런타임에 생성 가능

 

따라서 프로토콜을 변수, 상수, 배열, 딕셔너리 등의 값으로 사용하거나,

함수의 매개변수나 반환값으로 사용할 수 있다는 것을 의미한다. 

프로토콜의 일급 객체 특성은 프로토콜을 유연하게 사용할 수 있도록 한다.

 

프로토콜을 변수나 상수로 사용하기

protocol Describable {
    func describe() -> String
}

class Dog: Describable {
    func describe() -> String {
        return "나는 강아지야."
    }
}

class Cat: Describable {
    func describe() -> String {
        return "나는 고양이야."
    }
}

// 프로토콜 타입의 변수에 클래스의 인스턴스 할당
var describable: Describable = Dog()

// 함수 호출
print(describable.describe())  //"나는 강아지야."

// 다른 클래스의 인스턴스 할당
describable = Cat()

// 함수 호출
print(describable.describe()) //"나는 고양이야."

 

'Descirbable' 프로토콜은 변수 'describable' 의 타입으로 선언할 수 있다.

따라서 'Descirbable' 프로토콜을 준수하는 타입은 모두 해당 변수에 적용할 수 있다.

정확히는 'Descirbable' 프로토콜을 준수하는 어떤 타입의 인스턴스도 해당 변수에 할당할 수 있지만,

실제로 할당되는 것은 'Dog', 'Cat' 클래스 인스턴스가 'Descirbable' 프로토콜 타입으로 타입 캐스팅된 것이다.

 

 

프로토콜 타입캐스팅

protocol Describable {
    func describe() -> String
}

class Dog: Describable {
    func describe() -> String {
        return "나는 강아지야."
    }
    
    func bark() {
        print("멍멍!")
    }
}

var describable: Describable = Dog() //타입 캐스팅

describable.describe() //"나는 강아지야."

describable.bark()     //컴파일 에러 발생, Describable 프로토콜에는 bark() 메소드가 없습니다

 

변수 'describable' 에 실제로 할당되는 것은 'Dog' 클래스의 인스턴스가 아닌, 'Descirbable' 프로토콜이다.

따라서 'Dog' 클래스에 'bark()' 메서드는 'Descirbable' 프로토콜에 정의되어있지 않았기 때문에 해당 메서드를 호출하려고 하면, 컴파일 에러가 발생한다.

 

프로토콜을 통해 인스턴스를 생성할 수 없기 때문에

해당 프로토콜을 준수하고 있는 타입의 인스턴스를 사용하고,

이 과정에서 해당 프로토콜로 타입 캐스팅을 진행하여 할당한다.

 

함수의 매개변수로 전달되는 프로토콜

protocol Describable {
    func describe() -> String
}

class Dog: Describable {
    func describe() -> String {
        return "나는 강아지야."
    }
}

class Cat: Describable {
    func describe() -> String {
        return "나는 고양이야."
    }
}

func printDescription(animal: Describable) {
    print(animal.describe())
}

let myDog = Dog()
let myCat = Cat()

printDescription(animal: myDog)  //"나는 강아지야."
printDescription(animal: myCat)  //"나는 고양이야."

 

 

프로토콜을 타입으로 사용함으로써 얻는 이점

-먼저 프로토콜을 타입으로 사용한다는 것은

프로토콜 자체를 하나의 타입으로 취급하여 변수나 상수의 타입, 함수의 매개변수 또는 반환 타입으로 사용할 수 있다는 뜻이다.

프로토콜을 타입으로 사용함으로써 얻는 가장 큰 이점 중 하나는 유연성이다.

 

"유연성은 무엇을 뜻하죠?"

 

프로토콜은 특정 프로퍼티나 메서드의 구현을 요구하는 청사진이다.

이 프로토콜을 타입으로 사용하면, 해당 프로토콜을 채택하는 어떤 타입이든 상관 없이 해당 프로토콜의 메소드나 프로퍼티에 접근할 수 있다.

 

위의 예제를 다시 가져 오겠습니다.

protocol Describable {
    func describe() -> String
}

class Dog: Describable {
    func describe() -> String {
        return "나는 강아지야."
    }
}

class Cat: Describable {
    func describe() -> String {
        return "나는 고양이야."
    }
}

func printDescription(animal: Describable) {
    print(animal.describe())
}

let myDog = Dog()
let myCat = Cat()

printDescription(animal: myDog)  //"나는 강아지야."
printDescription(animal: myCat)  //"나는 고양이야."

 

위 코드에서 'printDescription()' 함수는 'Descirbable' 프로토콜을 준수하는 모든 타입의 인스턴스가 할당이 가능하다.

따라서 'printDescription()' 함수는 어느 한 타입의 인스턴스에 종속되지 않고, 

"Dog" 와 "Cat" 서로 다른 타입을 동일하게 처리할 수 있다.

이것이 바로 프로토콜을 타입으로 사용함으로써 얻을 수 있는 유연성이다.

 

또 다른 이점 

1. 코드 재사용성

유연성 예제에서 'printDescription()' 함수는 'Descirbable' 프로토콜을 준수하는 어떤 타입도 받아들일 수 있었다.

이렇게 특정 메서드나 함수가 특정 타입에 종속되지 않고, 프로토콜을 채택하는 모든 타입을 처리할 수 있기 때문에

코드 중복을 줄이고, 재사용성을 높일 수 있다.

 

2. 추상화

프로토콜은 구체적인 타입의 세부 사항보다는 프로토콜이 요구하는 동작에 집중할 수 있게 된다.

이것은 복잡한 시스템을 좀 더 단순하게 만들어, 이해하기 쉽게 만들어준다.

 

추상화는 복잡한 시스템을 단순화하는 기법 중 하나로, 세부적인 구현보다 필요한 기능만을 제공한다.

예를 들어, 어떤 네트워크 요청을 처리하는 함수를 작성하려고 한다.

이 함수가 정확하게 어떤 방식으로 네트워크 요청을 처리하는지 알 필요 없이,

우리는 이 함수에게 요청을 전달하고 그 결과를 받아올 수 있다. 이것이 추상화의 원리다.

 

이것을 프로토콜과 연관지어 볼 수 있다. 

예제에서 'Descirbable' 프로토콜에는 'descibe()' 메서드를 정의했다. 

여기서 'descirbe()' 메서드가 내부적으로 어떻게 동작하는지는 중요하지 않고, 

'Descirbable' 프로토콜을 채택하는 타입은 descirbe()' 메서드를 호출한다.

이렇게 복잡한 내부 구현을 숨기고 필요한 동작만을 제공하는 것이 프로토콜의 추상화이다.

 

프로토콜의 추상화는 구체적인 동작의 세부 사항보다는 요구하는 동작에 집중할 수 있게 되어, 코드의 가독성을 높여줄 수 있다.

 

3. 의존성 역전

의존성 역전 원칙은 객체 지향 설계 원칙 중 하나로, 상위 모듈이 하위 모듈에 의존하지 않고, 둘 다 추상화에 의존(프로토콜 등)하도록 설계하는 것을 말한다.

즉, 자세한 구현 사항에 대한 의존성을 줄이고, 프로토콜 등의 추상 타입에 의존하도록 만드는 것이다.

의존성 역전 원칙은 구성 요소 결합도를 낮추고, 변경에 더 유연하게 대응할 수 있게 된다.

 

특정 함수나 메소드가 특정 클래스나 구조체에 의존하게 되면, 그 함수나 메소드는 해당 클래스나 구조체의 인스턴스에 한정적으로 동작한다.

예제에서도 보았듯이 프로토콜 타입을 사용한 함수는 특정 타입 인스턴스에 한정적으로 동작하지 않고, 유연한 작동을 할 수 있었다.

또한 클래스나 구조체는 프로토콜에 비해 쉽게 변경될 수 있다. 이럴 때마다 해당 인스턴스에 의존하는 코드도 함께 수정해야 한다. 반면에 프로토콜에 의존하면 특정 타입의 변경에 의한 영향을 최소화 할 수 있다.

 

이렇게 프로토콜을 이용하여 의존성 역전 원칙을 적용하면, 코드를 더 유연하고 확장 가능하게 만들 수 있다.

예를 들어, 나중에 새로운 클래스를 생성하여 해당 프로토콜을 채택하게 되면, 기존의 함수를 수정하지 않고도 새 클래스의 인스턴스를 파라미터로 받을 수 있게 된다.

 

따라서, 프로토콜을 타입으로 사용하면 의존성 역전 원칙을 적용할 수 있으며, 안정적이고 유연한 코드를 만들 수 있다.

 

프로토콜과 의존성 역전 원칙 예시 코드

class DatabaseService {
    func fetch() -> [String] {
        return ["Data1", "Data2"]
    }
}

 

"DatabaseService" 라는 클래스를 정의했다. 

이 클래스는 데이터베이스에서 데이터를 조회하는 기능을 제공하고 있다.

하지만 이렇게 설계하면 "DatabaseService" 클래스는 특정 데이터 베이스 시스템에 의존하게 된다.

만약 데이터 베이스 시스템이 변경되면 "DatabaseService" 클래스도 그에 따라 변경되어야 하며, 유지보수가 어려워질 수 있다.

 

 

프로토콜을 활용한 추상화

protocol Database {
    func fetch() -> [String]
}

//CoreData 데이터베이스에서 데이터를 조회하고 반환
class CoreDataService: Database {

    func fetch() -> [String] {
        return ["Data1", "Data2"]
    }
}

//Realm 데이터베이스에서 데이터를 조회하고 반환
class RealmDataService: Database {

    func fetch() -> [String] {
        return ["Data3", "Data4"]
    }
}

 

'Database' 프로토콜을 정의하고, 각 데이터베이스 시스템에 맞게 데이터를 조회하는 작업을 수행할 수 있도록 했다.

이제 다시 "DatabaseService" 클래스를 사용해보자.

 

 

class DatabaseService {
    var database: Database
    
    init(database: Database) {
        self.database = database
    }
    
    func fetch() -> [String] {
        return self.database.fetch()
    }
}

let coreDataService = CoreDataService()
let realmDataService = RealmDataService()

let databaseService1 = DatabaseService(database: coreDataService)
print(databaseService1.fetch())  //["Data1", "Data2"]

let databaseService2 = DatabaseService(database: realmDataService)
print(databaseService2.fetch())  //["Data3", "Data4"]

 

위 코드는 기존의 "DatabaseService" 클래스와는 다르게

어떤 종류의 데이터베이스 서비스가 들어오든 유연하게 동작할 수 있게 되었다.

또한 데이터베이스 시스템이 변경도더라도 "DatabaseService" 클래스는 변경할 필요가 없게 되었다.

 

이렇게 프로토콜을 통해 의존성을 줄이면 유연한 코드를 만들 수 있다.

 

 

 

 

Reference:

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols/

https://babbab2.tistory.com/176

https://suvera.tistory.com/29