오늘은 Swift 에서의 Class와 Protocol 그리고 덤으로 약간의 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 가 바로 그것입니다. 그게 뭐냐면 공통적인 부분을 잔뜩 가지고 있는 상위 클래스!
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 프로토콜을 채택했지만 관련 함수들을 정의하지 않으면 아래와 같은 에러메세지를 만나게 될거예요.
🔅[UIKit] UITableView어쩌구프로토콜들의 채택
다시 UIKit 도 한번 살펴볼까요? UIKit 프레임워크에도 Protocol 개념이 사용되어 있는데요, 아래처럼 "MemoListViewController" 를 메인으로 하고, UIViewController를 상속받는다고 할때, UITableViewDataSource, UITableViewDelegate 는 프로토콜로 채택하는 구조를 가질 수 있어요. 그러니까, "기본적으로 UIViewController 라는 클래스 조건을 쓸건데, 테이블 정의에 필요한 프로토콜들도 필요하니까 채택할래!" 라는 의미가 되겠네요.
방금 채택한 UITableViewDataSource 의 내부를 들여다보면, 이건 만들어야 해! 하는 함수들이 명시되어 있어요(구현X). 그런데 하나의 프로토콜이 가지는 기능이 매우 많을텐데, 그걸 다 준수해야 할까요? 기본적으로 프로토콜에 명시된 내용들은 필수적으로 구현을 해주어야 하지만, optional 이 붙은 요소는 코드 구조 상 필요에 따라 구현하면 된답니다.
3. Extension: 클래스 쪼개기
그런데 아까 위에서 보여드린 클래스, 상속과 프로토콜이 덕지덕지 붙은 클래스, 조금 거슬리지 않으셨나요...?
위처럼 정의된 클래스와 함수들을 구현하면 이 함수가 상속받아와서 쓰고 있는 함수인지... 어느 프로토콜에서 데려온 함수인지... 등등 헷갈리겠죠? 코드 작성 시에 구분해서 잘 적어둔다 하더라도 주석이 난무하거나 바로바로 원하는 부분에 찾아가기 힘들수도 있겠어요. 무엇보다도 그냥 깔끔하지 않아서 마음에 안들어요 ㅠㅠ
이럴 때 사용할 수 있는 것이 바로 클래스 Extension(확장) 인데요, 위 예시로 쉽게 말하면 클래스를 쪼개서 쓴다고 생각하면 됩니다!
보통 클래스, 구조체, 열거형, 프로토콜 에 새로운 기능을 추가할 요량으로 사용할 수 있어요. 이렇게 되면 하나의 블럭(extension)의 책임과 범위가 정해지기 때문에 가독성💎을 높임과 동시에 유지보수성을 높일 수 있어요. 가장 매력적인 사실로는, 기존 코드에 접근하지 않고도 기능을 확장할 수 있습니다! 잘 돌아가는 메인 코드 만지기 너무 싫을때가 있죠... 말하자면 이 확장 기능을 통해서 기능을 모듈화하고, 유연하게 관리할 수 있어요.
방금 보여드린 클래스를 익스텐션으로 정리해보았습니다! 각 블록별로 하는 일과 내용들이 명확해졌죠? extension 을 이용하면 protocol 들을 깔끔하게 정리할 수 있으니, 여러 기능을 갖는 프로토콜을 채택하는 클래스를 작성할때는 꼭 이용해보도록 해요!
'기초탄탄 > Swift' 카테고리의 다른 글
GCD (Grand Central Dispatch) 를 그려보기 (3) | 2025.01.20 |
---|---|
클래스와 구조체, 아직도 난 잘 알지 못한다. (2) | 2025.01.15 |
연산프로퍼티 와 UserDefault 로 간편한 CRUD (0) | 2025.01.02 |
생성자(Initializer) : 슈붕으로 드릴까요 팥붕으로 드릴까요 (1) | 2024.11.14 |