2017.12.21 레츠 스위프트의 유튜브 영상을 참고 하였습니다.
값 타입은 참조 타입과는 다르게 값을 할당할 때 스택에 값 전체가 저장이 된다. Heap
을 쓰지 않으며, Reference Counting
필요가 x
Stack
인지 Heap
인지Static
인지 Dynamic
인지 (먼저 말하자면, 왼쪽에 있는 요소들이 성능에 좋은 영향을 끼침)
thread safe
해야 한다는 점이 가장 큰 문제 -> lock
등의 synchronization
동작은 성능에 큰 저하요소이다.이는 정말 자주 실행이 된다, 변수를 copy 할 때도 일어남. 그러나 이것의 가장 큰 문제 또한 thread safety
때문이다. -> count를 Atomic
하게 늘리고 줄여야 할 필요가 있음
class MyClass {}
func foo(c: MyClass) {}
do {
let c0: MyClass = MyClass() // retain(c0) -> Ref + 1
var c1: MyClass = c0 // retain(c1) -> Ref + 1
foo(c0) // retain(c) -> Ref + 1 ... release(c) -> Ref - 1
c1 = nil
// release(c1) -> Ref - 1 -> release(c0) -> Ref - 1
// Ref = 0 이 된다.
}
위 코드에서 참조 카운팅은 3번이 일어나고, 수행이 끝나거나, 할당이 nil 이 되는 경우 카운팅은 감소해서 0으로 줄여진다. retain, release
메소드가 자동으로 수행이 되는데 이를 ARC(Automatic Reference Counting)이라고 부른다. (이를, 옛날에는 직접 쳤다는,,? ㄷ ㄷ)
컴파일 시점에 메소드의 실제 코드 위치를 안다면, 실행중 찾는 과정 없이 바로 해당 코드주소로 점프가 가능하다. -> 이는 컴파일러의 최적화, 메소드 인라이닝이 가능하다.
컴파일 시점에서 메소드 호출 부분에 메소드 내용을 붙여 넣음(컴파일러가 효과가 있다고 판단하는 경우만) 이를 수행하면 Call Stack
오버헤드가 감소하고 이는 CPU iCache나 레지스터를 효율적으로 쓸 가능성이 높아진다.
컴파일 시점에서 확인 할 수 없는 경우이다. Reference
시맨틱스에서의 다형성 -> Compiler가 판단하기 힘든 경우 Dynamic
으로 일어난다 수행과정은 다음과 같다.
vtable (Virtual Dispatch Table) : Swift에서 클래스마다 유지하는 것, 이는 함수 포인터들의 배열로 표현되며, 하위 클래스가 메소드를 호출할 때 이 배열을 참조하여 실제 호출할 함수를 결정한다.
요점은 실제 Type
을 컴파일 시점에 알수가 없다는 점이다. 때문에 컴파일러의 최적화를 못하는것이 문제이다. -> 이를 final, private
등을 사용하여, 해당 메소드 프로퍼티 등은 상속되지 않음을 나타내어 Static
하게 처리를 하게끔 버릇을 들여야한다.
먼저 클래스를 살펴보면
그 다음 구조체는
String은 Value Semantics 이지만, 내부 storage로 class type을 가지고 있다. copy 시 해당 프로퍼티에 Reference Counting을 수행함. Array, Dictionary도 마찬가지
값의 제한이 가능하다면 enum
등의 값 타입으로 변경하고, 다수의 클래스를 하나의 클래스로 몰아주면 좋다.
프로토콜 타입에서 성능을 살펴보면
프로토콜 내부에서 함수를 찾고, 워드 단위로 데이터를 받고, VWT, PWT는 저번에 작성한 내용이 있으므로 생략 하겠습니다.
작은 사이즈의 프로토콜 타입은
큰 사이즈의 프로토콜 타입은
3워드가 넘어가는 프로토콜에서는 힙 할당이 자주 이루어 지는데 이는 성능에 큰 악영향을 끼친다. 이를 개선하기 위해서는 Indirect Storage
를 사용하거나, class 타입의 간접 저장소로 이동하는 방법이 있다.
힙 할당을 개선하기 위해 나온 방법 중 하나로, Indirect Storage with Copy-on-Write
가 존재한다. 이는 sharing
을 하다가, write
가 들어오면 copy
를 하는 메커니즘이다.
class LineStorage { var x1, y1, x2, y2: Double}
struct Line: Drawble {
var storage: LineStorage
init() { stroage = LineStorage(Point(), Point())}
func draw() {...}
mutating func move() {
if !isUniquelyReferenceNonObjc(&storage) {
storage = LineStorage(storage)
}
storage.start = ...
}
}
위 코드는 reference count
가 unique
하지 않다면 Sharing
을 막기 위해 새로운 LineStorage
인스턴스를 생성(copy) 하도록 하는 예제이다.
Indirect Storage를 사용한 큰 사이즈 프로토콜 타입에서는
프로토콜 타입에서 메소드 구현 방식에 따른 차이에 대해서 알아보겠습니다.
하위 클래스들이 메소드들을 구현하고 있음이 반드시 보장이 됩니다. 구현을 하지 않았더라도 디폴트 메소드를 사용하면 됩니다. 따라서 Witness Table
을 이용한 Dynamic Dispatch
가 이루어 집니다.
protocol SomeProtocol {
func action() -> Int
}
extension SomeProtocol {
func action() -> Int { 4 }
}
class SomeClass: SomeProtocol {
func action() -> Int { 2 }
}
class DerivedClass: SomeClass {
override func action() -> Int { 3 }
}
var c: SomeProtocol = SomeClass()
print(c.action()) // 2
c = DerivedClass()
print(c.action()) // 3
본체에 선언하지 않고 extension
으로 추가한 메소드들은 Witness Table
을 이용할 수 없습니다. 따라서 Static Dispatch
가 적용 됩니다.
protocol SomeProtocol {}
extension SomeProtocol {
func action() -> Int { 4 }
}
class SomeClass: SomeProtocol {
func action() -> Int { 2 }
}
class DerivedClass: SomeClass {
override func action() -> Int { 3 }
}
var c: SomeProtocol = SomeClass()
print(c.action()) // 4
c = DerivedClass()
print(c.action()) // 4
각 테스트의 ‘단위’를 얼만큼으로 잡을 것인가는 테스트의 속도 vs 효과성 트레이드오프를 파악하여 정해야 한다.
단위를 매우 작게 잡으면 속도는 빠르지만 실세계와 테스트 환경의 괴리가 크므로 효과성이 낮다. 단위를 크게 잡을수록 속도는 느리지만 효과성이 높다.
테스트 대상을 최대로 크게 잡은게 UI Test고, 가장 작게 잡은건 sut가 객체 하나짜리인 단위 테스트라고 볼 수 있다.
모바일 앱에서 테스트가 필요한 여러 기능 중 중요도가 높은건 사용자의 인터랙션과 복잡하게 얽힌 플로우다
이는 RIBs로 치면 뷰, 인터랙터, 라우터를 전부 실 객체로 테스트할 수 있는 것. 더 나아가 인터랙터가 호출하는 서비스 계층도 실 객체를 쓸 수도 있다.
모멘티 프로젝트는 서비스 계층이 의존하고 있는 플랫폼 계층(네트워킹, 데이터 저장, 엔진 등)이 한 단계 더 있어서 실 서비스 객체까지 테스트할 수 있다.
Hammer를 사용하면 테스트의 인풋은 실세계와 매우 유사한 유저 터치이며 모킹된 플랫폼 계층에서 최종 행위/상태 검증을 할 수 있다. 신뢰도가 높으면서 유연하고 빠른 테스트를 만들 수 있다.
참고
https://jcsoohwancho.github.io/2019-11-01-Swift%EC%9D%98-Dispatch-%EA%B7%9C%EC%B9%99/