기초탄탄/Swift

Class 와 Protocol 그리고 Extension (feat.UIKit)

노랑✨ 2024. 10. 28. 13:42

오늘은 Swift 에서의 ClassProtocol 그리고 덤으로 약간의 Extension에 대해서 정리해보려고 합니다. 그런데 이제 요즘 공부하는 UIKit도 조금 곁들인...

 

먼저 Class 와 Protocol 에 대해서 알아보려고 하는데요, 저는 정말 처음에 이 두 개념이 너무 헷갈렸답니다. 둘 다 다른 곳에 있는 함수(와 함수가 될 것)을 가져와서 다른 클래스에서 사용하게 하는 것인데, 이 두 개념이 왜 필요한지를 마음깊이 이해하지 못해서 계속 어색했어요. 결론적으로는 두 개념의 표현법, "Class를 상속받는다", "Protocol을 채택한다" 를 되뇌이면서(?) 사용법을 이해하게 되었습니다.

둘 다 코드의 구조화와 가독성을 위해 다른 곳에 명시된 내용을 가지고 오는 역할을 하지만, 표현에서부터 사용법이 다르다는 것을 눈치챌 수 있는데요, 각각의 예시를 통해 조금 더 알아보도록 하겠습니다!

1. Class: 실제 데이터와 행동을 담은 틀 (보통 붕어빵 틀에 많이 비유하죠?)

Class는 데이터와 행동을 한데 모아 하나의 객체로 만드는 틀이라고 생각할 수 있어요. 이동수단으로 예시를 들어보겠습니다.

이동수단(Vehicle) 클래스는 모든 이동수단의 공통 속성인 최대 속도(maxSpeed)와 현재 속도(currentSpeed)를 가지고 있고, 모든 이동수단이 공통적으로 가지는 기능인 이동(move())을 가지고 있다고 해봅시다.

// 이동수단(상위 클래스)
class Vehicle {
    var maxSpeed: Int
    var currentSpeed: Int = 0
    
    init(maxSpeed: Int) {
        self.maxSpeed = maxSpeed
    }
    func move() {
        print("The vehicle is moving at \(currentSpeed) km/h.")
    }
}

 

그럼 이제 이동수단 중 하나인 비행기 클래스를 만들어보겠다, 라고 한다면 비행기(Airplane) 클래스는 이동수단(Vehicle)을 "상속"받아 공통적인 부분(최대 속도, 현재 속도, 이동)은 재사용하면서, 비행기만의 고유 속성인 고도(altitude)와, 고유 함수인 이륙(takeOff)과 착륙(land) 기능을 추가해서 사용합니다.

// 비행기(하위 클래스) 이동수단의 속성과 메소드를 상속받아서 자기 것처럼 사용합니다
class Airplane: Vehicle {
    var altitude: Int = 0
    
    init(maxSpeed: Int, altitude: Int) {
        self.altitude = altitude
        super.init(maxSpeed: maxSpeed)
    }
    
    override func move() {
        print("✈️ The airplane is flying at \(currentSpeed) km/h at an altitude of \(altitude) meters.")
    }
    
    func takeOff() {
        altitude = 10000
        print("✈️ The airplane has taken off and is now at \(altitude) meters.")
    }
    
    func land() {
        altitude = 0
        print("✈️ The airplane has landed.")
    }
}

재미(?)있는 것은, 상위 클래스(Vehicle)에 정의되어 있는 함수는 하위 클래스(Airplane)에서는 별도로 명시하지 않아도 자동으로 가지고 있다고 여겨진다는 것입니다. 부유한 집 자녀들은 부모님 재산을 당연하게 상속받아 가진다 같은... 뭐 그런 비슷한 개념일까요...?(눈물) 만약 상위 클래스에 이미 정의되어 있는 함수를 조금 다르게 사용하고 싶다! 라고 한다면 위의 예시처럼 수정하고 싶은 함수에 override 를 적어주는 것으로 덮어쓰기를 할 수 있어요. "원래 있던거 말고 이걸로 진행해줘~" 라고 말하는 것이에요.

 

🔅[UIKit] UIViewController 의 상속

UIKit 프레임워크에서도 이런 상속개념을 사용하고 있는데요, 화면을 작성할 때 나의 메인 ViewController가 상속받는 UIViewController 가 바로 그것입니다. 그게 뭐냐면 공통적인 부분을 잔뜩 가지고 있는 상위 클래스!

Main으로 쓸 Class가 UIViewController를 상속받고 있다

UIKit은 화면 관리를 위한 프레임워크인 만큼, 주요 컨트롤러인 UIViewController에는 기본 함수들이 잔뜩 내장되어 있는데요, 그 중에 가장 일반적인 것이 뷰를 디바이스 메모리에 올리는 viewDidLoad() 입니다.

일단 다른 어려워보이는 것들은 못본체하세요

UIViewController 를 상속받는 것으로도 viewDidLoad() 를 사용할 수 있지만, 내가 별도로 만든 ViewController가 기본 컨트롤러가 되어 있고, viewDidLoad() 시점에 무언가 다른 동작을 하게 하고 싶다고 한다면 이때 override 를 사용하는거죠.

여기서는 타이틀과 배경색 그리고 서브뷰를 넣어보았습니다

이처럼 상속을 이용해서 상위 클래스에 존재하는 함수를 다양하게 수정해서 내 마음대로 사용할 수가 있습니다.

 

2. Protocol: 규약(계약)을 정의하는 틀 (나 쓸거면 이건 꼭 지켜야됨)

Class와 달리 Protocol은 말 그대로 구현해야 할 "규칙"만을 정의하는 역할을 합니다. 프로토콜 자체로는 데이터를 저장하거나 특정 동작을 구현하지는 않고, 그저 다른 타입들이 따라야 할 규칙을 제시합니다. "나를 채택하려고? 그럼 조건이 있음!"

아까 만든 예제를 조금 수정해서 프로토콜을 한번 적어보도록 하겠습니다.

 

모든 교통수단은 이동하기 때문에, Movable이라는 프로토콜을 만들어 모든 교통수단이 반드시 이동할 수 있어야 한다는 규칙을 정의해봅니다. currentSpeed 속성을 통해 현재 속도를 가지고 있어야 한다는 것과, move() 함수를 통해 이동할 수 있어야 한다는 것을 명시해 두어요. (이동하게 한다며... 이정도는 갖고 있어야 된다)

protocol Movable {
    var currentSpeed: Int { get set }
    func move()
}

 

이제 Vehicle 클래스가 Movable 프로토콜을 채택(준수)하도록 만들어 봅니다. Vehicle 클래스와 이를 상속받는 모든 하위 클래스는 Movable 프로토콜에 정의된 규칙을 따라야 하므로 currentSpeed 속성과 move() 메서드를 반드시 구현해야 합니다.

class Vehicle: Movable {
    var maxSpeed: Int
    var currentSpeed: Int = 0

    init(maxSpeed: Int) {
        self.maxSpeed = maxSpeed
    }

    func move() {
        print("The vehicle is moving at \(currentSpeed) km/h.")
    }
}

 

아까 만든 예시 중 "비행기"는 이동뿐만 아니라 이착륙도 할 수 있어야 하겠죠. 해당 조건을 명시하는 Flyable 프로토콜도 만들어 봅니다.

protocol Flyable {
    func takeOff()
    func land()
}

 

이제 비행기 클래스는 Movable, Flyable 프로토콜을 채택하여, 이동은 물론, 이륙과 착륙 기능을 구현합니다.

class Airplane: Movable, Flyable {
    var currentSpeed: Int = 0
    var altitude: Int = 0
    var maxSpeed: Int

    init(maxSpeed: Int) {
        self.maxSpeed = maxSpeed
    }

    func move() {
        print("✈️ The airplane is flying at \(currentSpeed) km/h at an altitude of \(altitude) meters.")
    }
    
    func takeOff() {
        altitude = 10000
        print("✈️ The airplane has taken off and is now at \(altitude) meters.")
    }
    
    func land() {
        altitude = 0
        print("✈️ The airplane has landed.")
    }
}

 

아까 말했다시피, Protocol 은 규칙이기 때문에, 정의된 함수를 구현하지 않으면(규칙을 따르지 않으면) 에러가 발생합니다. Flyable 프로토콜을 채택했지만 관련 함수들을 정의하지 않으면 아래와 같은 에러메세지를 만나게 될거예요.

지금 정의한 클래스로는 Flyable 할 수 없어!

 

🔅[UIKit] UITableView어쩌구프로토콜들의 채택

다시 UIKit 도 한번 살펴볼까요? UIKit 프레임워크에도 Protocol 개념이 사용되어 있는데요, 아래처럼 "MemoListViewController" 를 메인으로 하고, UIViewController를 상속받는다고 할때, UITableViewDataSource, UITableViewDelegate 는 프로토콜로 채택하는 구조를 가질 수 있어요. 그러니까, "기본적으로 UIViewController 라는 클래스 조건을 쓸건데, 테이블 정의에 필요한 프로토콜들도 필요하니까 채택할래!" 라는 의미가 되겠네요.

메인 ViewController에 여러 특성을 부여하는 모습

방금 채택한 UITableViewDataSource 의 내부를 들여다보면, 이건 만들어야 해! 하는 함수들이 명시되어 있어요(구현X). 그런데 하나의 프로토콜이 가지는 기능이 매우 많을텐데, 그걸 다 준수해야 할까요? 기본적으로 프로토콜에 명시된 내용들은 필수적으로 구현을 해주어야 하지만, optional 이 붙은 요소는 코드 구조 상 필요에 따라 구현하면 된답니다.

optional 함수는 구현하지 않아도 에러가 나타나지 않아요

3. Extension: 클래스 쪼개기

그런데 아까 위에서 보여드린 클래스, 상속과 프로토콜이 덕지덕지 붙은 클래스, 조금 거슬리지 않으셨나요...?

윽 너무 길어...! Xcode 뚫고 나간다!

위처럼 정의된 클래스와 함수들을 구현하면 이 함수가 상속받아와서 쓰고 있는 함수인지... 어느 프로토콜에서 데려온 함수인지... 등등 헷갈리겠죠? 코드 작성 시에 구분해서 잘 적어둔다 하더라도 주석이 난무하거나 바로바로 원하는 부분에 찾아가기 힘들수도 있겠어요. 무엇보다도 그냥 깔끔하지 않아서 마음에 안들어요 ㅠㅠ

이럴 때 사용할 수 있는 것이 바로 클래스 Extension(확장) 인데요, 위 예시로 쉽게 말하면 클래스를 쪼개서 쓴다고 생각하면 됩니다!

보통 클래스, 구조체, 열거형, 프로토콜 에 새로운 기능을 추가할 요량으로 사용할 수 있어요. 이렇게 되면 하나의 블럭(extension)의 책임과 범위가 정해지기 때문에 가독성💎을 높임과 동시에 유지보수성을 높일 수 있어요. 가장 매력적인 사실로는, 기존 코드에 접근하지 않고도 기능을 확장할 수 있습니다! 잘 돌아가는 메인 코드 만지기 너무 싫을때가 있죠... 말하자면 이 확장 기능을 통해서 기능을 모듈화하고, 유연하게 관리할 수 있어요.

편-안

방금 보여드린 클래스를 익스텐션으로 정리해보았습니다! 각 블록별로 하는 일과 내용들이 명확해졌죠? extension 을 이용하면 protocol 들을 깔끔하게 정리할 수 있으니, 여러 기능을 갖는 프로토콜을 채택하는 클래스를 작성할때는 꼭 이용해보도록 해요!