ios dev kangwook.

iOS) Protocol에 대한 고찰 본문

iOS

iOS) Protocol에 대한 고찰

kangwook 2024. 5. 17. 16:27

일을 하다보면, 그러니까 점점 비대해지는 이 프로젝트를 보고 있자면 가끔씩 갑갑한 마음이 들 때가 있다.

그럴 때 마다 아 좀 더 유연하게 코드를 짤 수 없을까 라는 생각이 들기 마련이고 이럴 때 활용할 수 있는게 바로 프로토콜이 되겠다.

나도 완전히 능숙하게 사용하지는 못하지만, 최대한 protocol을 활용하려고 노력 중이고, 왜 강력한 도구인지 소개하려고 한다.


일단 Swift에서 기본적으로 제공하는 프로토콜들을 한 번 살펴보자

Equatable

Equatable 프로토콜은 두 인스턴스를 비교할 수 있도록 한다. == 연산자를 사용해서 두 객체가 같은지 비교할 때 사용한다. 직접 정의한 Classsk Struct에 Equatable 프로토콜을 추가하면, 해당 타입의 인스턴스들을 비교하는 로직을 구현할 수 있다.

예를 들어 User라는 구조체를 정의하고 Equatable 프로토콜을 추가한다면 두 Person 인스턴스의 name과 age를 비교하여 동일한 사람인지 판별할 수 있다.

struct Person: Equatable {
    let name: String
    let age: Int
    
    static func == (lhs: Person, rhs: Person) -> Bool {
        return lhs.name == rhs.name && lhs.age == rhs.age
    }
}

보통은 id값을 두어서 이거로 비교하는 방법을 주로 사용하는 것 같다.(내가 개발하는 프로젝트의 경우 idx로 사용한다...)

Comparable

Comparable 프로토콜은 인스턴스들을 순서대로 정렬할 수 있도록 한다. <, >, <=, >= 연산자를 사용해서 객체의 순서를 비교할 때 사용한다. Comparable 프로토콜을 구현하면, 해당 타입의 인스턴스들을 정렬하는 로직을 구현할 수 있다.

예를 들어 Task라는 구조체를 정의하고 Comparable 프로토콜을 추가한다면, priority를 기준으로 작업의 우선순위를 비교하여 정렬할 수  있다.

struct Task: Comparable {
    let title: String
    let priority: Int

    static func < (lhs: Task, rhs: Task) -> Bool {
        return lhs.priority < rhs.priority
    }
}

Hashable

Hashable 프로토콜은 인스턴스를 hash 가능하도록한다. 여기서 hash란 뭐냐. 객체를 고유하게 식별할 수 있게하는 정수 값이라고 보면 된다. Set이나 Dictionary와 같은 컬렉션 타입에서 객체를 효율적으로 저장하고 검색하는데에 사용한다.

Hashable 프로토콜을 구현하면 객체를 해싱하는 로직을 구현할 수 있다.

예를 들어, Product 라는 구조체를 정의하고 Hashable 프로토콜을 추가한다면, id 프로퍼티를 기반으로 해시값을 생성하여, Set을 사용하여 중복된 상품을 제거할 수 있다.

struct Product: Hashable {
    let id: Int
    let name: String

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

Codable

많이 사용하기도 하고, 나 또한 사용하고 있는 Codable 프로토콜은 인스턴스를 JSON이나 XML등으로 encoding / decoding 할 수 있도록 한다. 데이터를 저장하거나 네트워크를 통해 전송할 때 꽤나 유용하게 사용하고, 이 Codable을 구현하면 특정 데이터 형식으로 변환하는 로직을 구현할 수 있다.

예를 들어, Message라는 구조체를 정의하고 Codable 프로토콜을 추가한다면, JSON형식으로 메시지를 인코딩하여 서버로 전송하거나 JSON 데이터를 디코딩하여 Message 객체를 생성할 수 있다.

struct Message: Codable {
    let sender: String
    let content: String
}

자 그럼 이런 기본적인 프로토콜들은 알겠다 이거다. 그럼 내가 프로토콜을 만들어야 될 상황은 어떤 상황일까?

바로 Delegate 패턴을 들 수 있겠다.

Delegate 패턴은 특정 이벤트가 발생했을 때, 다른 객체에게 작업을 위임하는 데 사용한다.

쉽게 설명하기 위해 예를 들자면, UITableView는 스스로 모든 것을 처리하기보다는, 다른 객체에게 "테이블 뷰에서 이런 이벤트가 발생했는데, 어떻게 처리할래?" 라고 묻는 방식을 사용한다. 이 때, "어떻게 처리할래?"라는 질문에 답하는 녀석이 바로 UITableViewDelegate 프로토콜을 준수하는 객체다.

예를 들어 UITableView에서 특정 행을 선택했을 때 어떤 작업을 수행해야할까? 라는 질문에 답하기 위해 UITableViewDelegate프로토콜은 tableView(_:didSelectRowAt:) 메서드를 제공한다.

protocol UITableViewDelegate: NSObjectProtocol {
    // ... (다른 메서드들)

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
}

UITableView는 특정 행이 선택되면 delegate 객체의 tableView(_:didSelectRowAt:) 메서드를 호출한다. 이 메서드를 구현하는 쪽에서는 선택된 행의 정보를 이용해서 원하는 동작을 수행할 수 있다.

class MyViewController: UIViewController, UITableViewDelegate {

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // 선택된 행의 데이터를 가져와서 처리한다.
        let selectedData = data[indexPath.row]
        // 예를 들어, 선택된 데이터를 사용하여 다른 뷰 컨트롤러로 이동한다.
        let detailViewController = DetailViewController(data: selectedData)
        navigationController?.pushViewController(detailViewController, animated: true)
    }
}

사실 이 Delegate 부분은 쉽게 이해를 시키기 위해 UITableView의 예를 든거지만, 직접 자신의 커스텀뷰에 대한 동작을 Delegate로 만들어서 다른 객체에 위임하는 식으로 Delegate 패턴을 이용한 프로토콜을 만들 수 있다. 

처음에는 생각보다 뭔소리야? 라는 생각이 들 수 있지만, 계속 사용하고 이해하다보면 굉장히 익숙해지는게 프로토콜을 이용한 delegate패턴이라고 볼 수 있겠다.


자 그럼 한 번 간단한 프로토콜을 만들어 보도록 하자.

프로젝트를 하다보면, 새로고침 버튼을 누른다던가, 위로 스크롤해서 새로고침을 한다던가, 여러가지 방법으로 refresh를 해야할 경우가 생긴다. 그런데 모든 뷰나 뷰 컨트롤러에서 필요한 것은 아니다.

그래서 필요한 부분에서 적절한 동작을 할 수 있게 Refreshable이라는 프로토콜을 만들도록 해보자

이 프로토콜은 UIViewController 뿐만 아니라, View를 포함한 다양한 곳에 적용될 수 있다는 점이 핵심이다.

예를 들어, 데이터를 표시하는 DataViewController와 DataView가 있다고 가정해보자.

class DataViewController: UIViewController, Refreshable {
    // ...

    func refresh() {
        // 데이터를 다시 불러와서 뷰를 업데이트 한다.
    }
}

class DataView: UIView, Refreshable {
    // ...

    func refresh() {
        // 데이터를 다시 불러와서 뷰를 업데이트 한다.
    }
}

Refreshable 프로토콜을 통해 DataViewController와 DataView가 refresh 기능을 지원한다는 것을 명확하게 알 수 있다.

이 프로토콜을 활용하는 다른 객체들은 DataViewController와 DataView의 구체적인 타입을 알 필요 없이, refresh() 메서드를 호출하여 데이터를 새로 고칠 수 있다.

이를 보면 알 수 있듯이, Refreshable 프로토콜은 코드의 의존성을 줄이고, 유연성을 높이는데 기여한다고 할 수 있겠다.

조금 더 응용해보자면, extension을 통해 기본적인 구현을 제공하는 거다.

UIView와 UIViewController는 명확하게 다른 class이기 때문에, 각각 뷰를 다시 그리는 방법도 다르다. 따라서 그에 대한 기본적인 구현을 작성해보는 거다.

extension Refreshable where Self: UIViewController {
    func refresh() {
        for child in children {
            (child as? Refreshable)?.refresh()
        }
    }
}

extension Refreshable where Self: UIView {
    func refresh() {
        setNeedsDisplay()
    }
}

뭐.. 그냥 이런식으로 각각 기본적인 구현을 protocol의 extension에서 할 수 있다는 얘기다

정말 정말 간단하게 보여주기 위해서 위와 같이 한거고, 이를 응용해서 더욱 더 확장해 나가면 된다.

여기서 더 확장해 나가면 예를 들어 NetworkRefreshable: Refreshable이라는 protocol을 채택하는 protocol을 하나 더 생성해서 더욱 구체화 시킬 수도 있고, Delegate와 조합하여 더욱 상세한 동작을 할 수 있게 만들 수도 있겠다.


이와 같이, 프로토콜은 특정 기능을 구현하도록 강제하는 도구인 와중에 코드를 유연하게 만들어 재사용성을 높이는 Swift의 강력한 장점이라고 할 수 있겠다.

이러한 프로토콜을 잘 활용하면 Network 레이어의 동작이라던지, 각종 Delegate의 작동 방식에 대해서 더욱 더 잘 이해하고 활용할 수 있을 거 같다. (물론 나도 아직 멀었다)

이 글을 보고 조금 더 프로토콜이랑 친해질 수 있는 계기가 되길 바라며... 이만 가보겠슴다

빠이염

Comments