본문 바로가기
Programming/Swift

[iOS Swift5] 객체지향 프로그래밍 기초 클래스 프로토콜 익스텐션

by 개발자 염상진 2024. 1. 9.

Swift는 객체 지향 프로그래밍 언어(Object Oriented Programming)입니다. 그만큼 객체를 사용한 프로그래밍을 지원하고 있다는 말인데요, 객체는 재사용가능한 독립 기능 모듈로 클래스 구조체의 형태로 지원되고 있습니다. 이번 포스팅에서는 Swift에서 바로 사용가능한 클래스의 기본 개념에 대해 알아보고자 합니다. 

 

 

 

클래스는 왜 사용하나요?

먼저 클래스를 사용하는 이유에 대해 알아보겠습니다. 

#1 캡슐화

OOP에서 클래스는 멤버를 하나로 묶어 캡슐화 할 수 있습니다. 이는 데이터를 보호하고 유지보수하는데 용이하다는 장점을 가지고 있습니다. 또한 접근제어자를 통해서 클래스 멤버에 접근할 수 있는 안전장치를 걸 수 있기 때문에 안전하게 데이터를 사용할 수 있습니다. 

#2 추상화

클래스를 사용함으로써 코드의 전체적인 윤곽을 잡기에 유리합니다. 복잡성을 최대한 줄이고 코드 재사용성을 높이는 효과를 가집니다. 

#3 다형성

같은 이름의 클래스라고 할지라도 다양한 기능의 메서드를 구현할 수 있으므로 다양한 코드 사용이 가능해집니다. 

#4 코드 재사용성

인스턴스를 생성해서 클래스의 구조를 사용할 수 있기 때문에 다양한 곳에서 코드를 재사용할 수 있고 클래스 본체만 수정하면 여러곳에서 코드가 자동으로 적용되기 때문에 유지보수성이 높습니다. 

 

 

 

 

클래스 선언하는 방법

Swift에서 클래스를 선언하는 방법은 다음과 같습니다. 클래스 내부에서 선언되는 변수는 프로퍼티(property), 함수는 메서드(method) 그리고 프로퍼티와 메서드를 통합해서 멤버라고 지칭하고 있습니다. 

class 클래스 이름 : 상속받을 부모 클래스 {
	클래스 프로퍼티(변수)
	초기화 메서드(함수)
    메서드(함수)
}

 

실제 사용하는 코드를 살펴보겠습니다. Car 라는 클래스에  brand, model, year이라는 프로퍼티가 담기게 되고 init, displayInfo() 메서드가 포함되어 있습니다. 

class Car {
    var brand: String
    var model: String
    var year: Int
    
    init(brand: String, model: String, year: Int) {
        self.brand = brand
        self.model = model
        self.year = year
    }
    
    deinit {
    
    }
    
    func displayInfo() {
        print("Car: \(brand) \(model), Year: \(year)")
    }
}

이 클래스를 사용하기 위해서는 객체(instance)를 생성해서 사용할 수 있습니다. 인스턴스를 생성할 때는 클래스 프로퍼티를 초기화 할 수 있는 인자를 함께 전달해서 생성합니다. 인스턴스 멤버에 접근하기 위해서는 (.)을 사용해서 사용할 수 있습니다. 

let myCar = Car(brand: "Toyota", model: "Corolla", year: 2022)
print(myCar.brand) // "Toyota"
myCar.displayInfo() // Car: Toyota Corolla, Year: 2022

 

 

클래스 인스턴스가 생성되면 메모리 상에 데이터가 올라가게 됩니다. Swift에서는 메모리 할당 작업을 ARC(Automatic Reference Counting)에서 자동으로 진행되는데요, 인스턴스에 대한 참조코드가 없어지면 메모리에서 제거되게 됩니다. 이 때 인스턴스가 소멸될 때 돌아가야 할 코드가 있다면 deinit{} 블럭에서 수행할 수 있습니다. 

 

프로퍼티 2가지 타입

클래스 내부에서 데이터를 저장하는 프로퍼티는 저장 프로퍼티(stored property)와 연산 프로퍼티(computed property)로 구분할 수 있습니다. 저장 프로퍼티는 상수나 변수에 저장된 값입니다. 반면 연산 프로퍼티는 값을 설정하거나 호출하는 시점에 계산이 이뤄지는 프로퍼티입니다. 

위의 Car 예제에서 details 라는 연산 프로퍼티를 추가했습니다. 연산 프로퍼티는 getter와 setter를 생성합니다. getters는 암묵적으로 생성되기도 하고 명시적으로 생성해줄 수 있습니다. get은 생략 가능합니다. 

class Car {
    var brand: String
    var model: String
    var year: Int
    
    init(brand: String, model: String, year: Int) {
        self.brand = brand
        self.model = model
        self.year = year
    }
    
    deinit {
    
    }
    
    // computed property
    var details: String {
        get {
            return "\(year) \(brand) \(model)"
        }
        set(newValue) {
            let components = newValue.split(separator: " ")
            if components.count >= 3 {
                year = Int(components[0]) ?? year
                brand = String(components[1])
                model = String(components[2])
            }
        }
    }
    
    func displayInfo() {
        print("Car: \(brand) \(model), Year: \(year)")
    }
}

myCar.details = "2019 Honda Civic"

// Accessing the updated details after setting values
print("Updated Car Details: \(myCar.details)") // Output: Updated Car Details: 2019 Honda Civic

코드 마지막 줄에서 details를 setter로 값을 설정하고 getter로 값을 가져올 수 있습니다. 

 

프로퍼티 초기화

클래스 내부에 선언된 프로퍼티를 초기화 하기 위해서는 초기화 함수에서 로직을 구성할 수 있습니다. 

init(brand: String, model: String, year: Int) {
    self.brand = brand
    self.model = model
    self.year = year
}

 

위와 같은 방식은 새로운 인스턴스가 생성될 때 마다 초기화 하게 됩니다. 만약 인스턴스가 최초 한번 생성될 때만 프로퍼티를 초기화 해야 하는 경우 lazy 키워드를 사용할 수 있습니다. 예를 들어 위의 Car 클래스에서 brand 프로퍼티는 단 한번만 초기화 되어야 한다고 한다면 아래와 같이 구성할 수 있습니다.

lazy var brand: String = {
    // Simulating a complex or resource-intensive initialization
    print("Initializing brand")
    return "Toyota"
}()

init(model: String, year: Int) {
    self.model = model
    self.year = year
}

 

 

 

 

Swift 프로토콜

클래스는 개발자가 원하는대로 프로퍼티와 메서드를 구성할 수 있습니다. 하지만 프로그래밍을 하다보면 iOS SDK를 사용하는 경우, 클래스를 사용하기 위해서는 특정 규칙을 따라야 하는 경우가 있습니다. 이런 클래스가 따라야 하는 규칙을 정의해놓은 집합을 프로토콜(protocol)이라고 합니다. 프로토콜을 준수(conform)한다고 합니다. 

Car class가 만약 아래와 같은 Vehicle 프로토콜을 준수해야 한다고 한다면 모든 멤버들을 다 채워야 컴파일 에러를 피할 수 있습니다. 만약 하나라도 프로토콜을 준수하지 못하게 되면 컴파일 에러가 발생합니다.

protocol Vehicle {
    var brand: String { get }
    var model: String { get }
    var year: Int { get }
    
    func displayInfo()
}

Vehicle 프로토콜은 3개의 프로퍼티와 1개의 메서드를 구현할 것을 '강제'하고 있습니다. 

class Car: Vehicle {
    var brand: String
    var model: String
    var year: Int
    
    init(brand: String, model: String, year: Int) {
        self.brand = brand
        self.model = model
        self.year = year
    }
    
    deinit {
    
    }
    
    // computed property
    var details: String {
        get {
            return "\(year) \(brand) \(model)"
        }
        set(newValue) {
            let components = newValue.split(separator: " ")
            if components.count >= 3 {
                year = Int(components[0]) ?? year
                brand = String(components[1])
                model = String(components[2])
            }
        }
    }
    
    func displayInfo() {
        print("Car: \(brand) \(model), Year: \(year)")
    }
}

 

프로토콜을 이해하면 불투명 반환타입(opaque return type)을 이해할 수 있습니다. SwiftUI로 View를 구성하다 보면 some 키워드를 사용한 구조체를 쉽게 볼 수 있는데요, 이는 특정 반환 타입을 강제하지는 않지만 해당 프로토콜을 준수하는 모든 타입이 반환값이 될 수 있다는 의미입니다.

// basic SwiftUI View Struct
struct ContentView: View {
    var body: some View {
        Text("Hello SwiftUI")
            .font(.title)
            .foregroundColor(.blue)
    }
}

 

아래 코드를 보면 createCar() 함수는 Vehicle 프로토콜을 준수하는 어떤 타입이라도 반환값이 될 수 있다는 의미입니다. 우리는 Vehicle 프로토콜을 준수하는 Car 클래스를 만들었기 때문에 createCar() 함수의 반환값에 Car 인스턴스가 올 수 있게 됩니다. 

func createCar() -> some Vehicle {
    return Car(brand: "Toyota", model: "Corolla", year: 2022)
}

 

이처럼 SwiftUI 디자인 프레임워크는 재사용 가능한 빌딩 블록을 가지고 거대한 뷰 선언부를 작게 리팩토링하면서 앱을 만들도록 설계되었습니다. 결과적으로는 매우 유연한 상호호환성을 가지게 됩니다. 

 

 

 

 

클래스 상속

클래스 상속은 부모 클래스의 특성을 그대로 사용하면서 자신만의 기능을 추가할 수 있는 개념입니다. Swift에서는 클래스를 통해서 계층 구조를 만들 수 있는데요, 최상위 클래스를 루트 클래스(Root Class), 상속을 받는 클래스는 하위 클래스(subclass), 상속하는 클래스를 상위 클래스(super class)로 지칭합니다. 

Swift의 특징은 다중상속을 지원하지 않고 단 하나의 상위 클래스만 둘 수 있다는 점입니다. 

Car 클래스를 상속받는 ElectricCar 클래스를 생성해보겠습니다. Car 클래스의 멤버를 그대로 사용하면서도 displayInfo() 메서드는 오버라이딩을 통해 추가적인 기능을 더할 수 있습니다. 상위 클래스의 메서드는 super 키워드를 사용해서 접근/사용 가능합니다. 

class ElectricCar: Car {
    var batteryCapacity: Double
    
    init(brand: String, model: String, year: Int, batteryCapacity: Double) {
        self.batteryCapacity = batteryCapacity
        super.init(brand: brand, model: model, year: year)
    }
    // Overriding the displayInfo method from the superclass
    override func displayInfo() {
    	super.displayInfo()
        print("Electric Car: \(brand) \(model), Year: \(year), Battery: \(batteryCapacity) kWh")
    }
}

하위 클래스에서 오버라이딩 하기 위해서는 특정 규칙을 따라야 합니다.

  • 상위 클래스의 메서드 매개변수 타입및 개수가 동일해야 한다.
  • 상위 클래스의 메서드 반환타입이 일치해야 한다. 

하위 클래스에서 초기화 작업을 할 때  또한 상위클래스의 초기화 작업을 동시에 진행할 수 있는데요, 이 때도 super 키워드를 통해 상위클래스 초기화를 할 수 있습니다. 이 때는 반드시 하위 클래스 초기화가 완료된 시점에 호출되어야 합니다. 

 

클래스 익스텐션

Swift SDK를 사용하다 보면 추가적인 기능을 더해서 사용해야 하는 경우가 있습니다. 이 경우 Extension을 사용할 수 있습니다. SDK의 원본 소스코드에 접근 권한이 있든 없든 상관없이 기능을 추가해서 사용할 수 있습니다. 

extension Int {
    var isEven: Bool {
        return self % 2 == 0
    }
}

// Using the extension on Int
let number1 = 10
if number1.isEven {
    print("\(number1) is even")
} else {
    print("\(number1) is odd")
}

let number2 = 7
if number2.isEven {
    print("\(number2) is even")
} else {
    print("\(number2) is odd")
}

예를 들어 일반 iOS 앱 개발자들이 Int 클래스를 관리하지는 않지만 isEven이라는 함수를 추가해서 용이하게 사용할 수 있습니다. 

 

🚀️ 도움이 되셨다면 구독좋아요 부탁드립니다 👍️

 

 

 

[iOS Swift5] 함수, 메서드 그리고 클로져 기본 개념

프로그래밍 영역에서 함수는 반복적인 작업을 대폭 줄여주는 장점이 있습니다. 애플의 스위프트 언어 또한 함수를 제공하고 있는데요, 이번 포스팅에서는 Swift의 함수에 대해 알아보도록 하겠

about-tech.tistory.com

 

 

[iOS Swift5] if else, guard 제어 흐름 관리하기

Swift5 제어흐름 관리하기 안녕하세요 About Tech 입니다. 이번 포스팅에서는 Swfit 언어에서 제어흐름을 어떻게 관리할 수 있는지 알아보고자 합니다. 제어흐름은 조건식이라고도 불리는 if 문이나 gu

about-tech.tistory.com

 

 

[iOS Swift5] 상수와 변수 데이터 타입 옵셔널, 옵셔널 바인딩, 강제언래핑

Swift5 상수와 변수 프로그래밍 언어에서 데이터를 표현하는 방법은 다양합니다. iOS 8 이전까지는 Objective-C로 개발했지만 스위프트 언어가 등장하고 나서는 대부분 흐름이 스위프트로 새로운 기

about-tech.tistory.com

 

댓글