OOP에서의 클래스 특성
- 캡슐화: 관련 데이터와 동작들을 모아볼 수 있다.
- 접근제어: 코드 외부와 내부를 구분짓는 벽을 세워 외부에서의 접근에 제한을 둘 수 있다. 이를 통해 정보에 대한 불변성을 유지할 수 있다.
- 추상화: 추상화를 통해 관련시킬 수 있는 정보들을 모아둔 클래스를 만들 수 있다.
- NameSpace: 소프트웨어가 커지면서 발생할 수 있는 충돌을 대비하는데 도움을 주는 네임스페이스를 제공, 여기서 네임스페이스는 내부 식별자에 사용될 수 있는 유효 범위를 제공하는 선언적 영역
- Expressive Syntax: 클래스를 통해 프로퍼티, 메서드등의 흐름으로 이해하기 쉬운 구문을 작성할 수 있다.
- Extensibility(확장성): 작성된 코드에 언제든지 기능을 추가할 수 있다.
위 특성을 struct와 enum을 통해 할 수 있다?
클래스는 자동화된 공유를 한다.
- 어떤 A가 B에게 괜찮아 보이는 정보를 줄 때 B는 그 정보를 받고 끝난다.
- A가 데이터를 바꿔버리면, B가 그 데이터를 다시 열람할 때 문제가 생긴다 (공유된 자원)
- Dispatch Queue로 어떤 작업을 할 때 쓰레드가 Mutable State를 공유하기에 race condition에 빠질 수 있음
- 불변성을 보호하기 위해 nslock을 추가 -> lock이 코드를 느리게 만들고 데드락 현상을 초래할 수 있음
클래스는 오직 하나의 슈퍼 클래스만 가질 수 있다.
여러 추상화를 모델링 하는 경우에는?
- 클래스의 상속은 단일 상속이다. 서브 클래스가 상속을 하는 순간 슈퍼클래스만큼 크기가 늘어난다.
- 나중에 확장이나 클래스를 정의하는 순간에 슈퍼클래스를 지정해야함.
- 슈퍼클래스가 저장 프로퍼티를 가진다면, 그것을 전부 수용해야한다.
- 속성이 저장되어 있으니 무조건 초기화해줘야한다.
// precedes의 코드블럭 안에 들어갈 수 있는 것은? fatalError밖에 없다.
class Ordered {
func precedes(other: Ordered) -> Bool { fatalError("Implement me!") }
}
class Number: Ordered {
var value: Double = 0
override func precedes(other: Ordered) -> Bool {
return value < other.value // ERROR: Value of type 'Ordered' has no member 'value'
}
}
// Ordered를 상속받는것이 문자열이라면?
class Label: Ordered { var text: String = "" }
func binarySearch(sortedKeys: [Ordered], forKey k: Ordered) -> Int {
var lo = 0, hi = sortedKeys.count
while hi > lo {
let mid = lo + (hi - lo) / 2
if sortedKeys[mid].precedes(other: k) {
lo = mid + 1
} else { hi = mid }
}
return lo
}
Number클래스는 Ordered를 상속받는다.
비교 메서드를 오버라이드하여도 작동하지 않는다.
- 파라미터 other가 값 프로퍼티를 갖는지 알 수 없다.
이 문제를 해결하기 위해서 Number라는 타입으로 다운 캐스트해야한다.
class Number: Ordered {
var value: Double = 0
override func precedes(other: Ordered) -> Bool {
return value < (other as! Number).value
}
}
위 처럼 다운 캐스트 구조 (as! subclass)는 타입들간의 관계에서 유실이 발생했다는 뜻이다.
이런 문제가 발생하는 이유는 클래스를 통해서는 자신의 타입과 다른 타입간의 중요한 관계를 표현할 수 없기 때문이다.
이런 경우는 보통 클래스들로 추상화를 표현할 때 발생
이런 문제를 해결하기 위한 추상화 구조는 어떤 것들이 필요할까?
- 값 타입 지원 (클래스도)
- 정적 타입관계 지원
- monolithic(의존성이 강한)이지 않을 것
- 원본 코드에 접근할 수 없을 것
이런 특징들이 왜 Cocoa에서 델리게이트 패턴을 사용하는지에 대한 이유가 될 수 있다.
- 무조건 슈퍼클래스의 프로퍼티를 받아들여야함
- 초기화 작업이 버거워짐 (저장 프로퍼티들을 상속받으면 무조건 초기화해야하므로)
- 슈퍼클래스의 불변성을 침범하면 안됨
위 Ordered 코드를 프로토콜로 바꿔보면 오류가 발생합니다.
protocol Ordered {
func precedes(other: Ordered) -> Bool { fatalError("Implement me!") } // ERROR: Protocol methods must not have bodies
}
프로토콜이기 때문에 메서드를 직접 구현할 수 없습니다.
직접 구현을 제거하면 dynamic runtime check를 static check로 바꿀 수 있습니다.
- Dynamic test: 프로그램의 실제 동작을 확인함으로써 원하는 동작을 정상적으로 수행하는지 검토합니다. (빌드 이후에 수행)
- Static test: 오류를 유발할 가능성이 있는 코드를 사전에 검출하는 것 (빌드 이전에 수행)
→ 메서드가 모두 구현되어 있어 실행시 체크를 해보는것이 아니라 필요한 메서드를 명세만 해놓음으로써 정적체크로 변경한다.
→ 이것이 꽤 괜찮은 방법인지는 한번 생각해보는 것도 좋을 것 같다.
protocol Ordered {
func precedes(other: Ordered) -> Bool
}
// protocol을 채택하므로 오버라이딩을 할 필요가 없어지고, 오버라이딩을 안하면 class를 사용할 이유가 없다.
// class -> struct로 변경
struct Number: Ordered {
var value: Double = 0
func precedes(other: Ordered) -> Bool {
return self.value < (other as! Number).value
}
}
다운캐스팅을 없애기 위해 other 파라미터를 Number로 변경할 수 있지만, 수정하면 protocol의 요구사항을 충족하지 않기 때문에 protocol의 precedes 파라미터도 아래와 같이 변경해주어야합니다.
protocol Ordered {
func precedes(other: Number) -> Bool
}
struct Number: Ordered {
var value: Double = 0
func precedes(other: Number) -> Bool {
return self.value < other.value
}
}
위 코드로 수정하면 작동하는데는 문제가 없지만, 프로토콜을 준수하는 어떤 객체더라도 Number타입으로 받아야하는 문제가 발생합니다.(다른 타입도 프로토콜을 따르도록 설계한다면 문제)
그래서 다른 타입도 프로토콜을 따르도록 설계한다면 아래와 같이 수정할 수 있습니다.
protocol Ordered {
func precedes(other: Self) -> Bool
}
struct Number: Ordered {
var value: Double = 0
func precedes(other: Number) -> Bool {
return self.value < other.value
}
}
'iOS' 카테고리의 다른 글
Reactor Kit View를 채택하는 VC에 bind(reactor:) 메서드가 실행되지 않음 (0) | 2025.06.20 |
---|---|
Github Action으로 CI 구성하기 (1) | 2025.06.04 |
[트러블 슈팅] DiffableDataSource 컬렉션 뷰에 다양한 데이터모델을 사용하기 (0) | 2025.05.02 |
[트러블 슈팅] CoreData의 Entity Attribute의 타입을 변경한 경우 (0) | 2025.04.23 |
메모리 사용량 확인을 위한 vmmap (1) | 2025.03.18 |