스위프트 타입 시스템

타입 시스템은 프로그래밍 언어 작성 방식과 프레임워크의 구조를 결정하는 매우 중요한 요소이다.
스위프트를 비롯한 함수 중심 언어 의 타입 시스템은 Objective-C와 코코아 프레임워크에서 사용하는 타입 시스템보다 안전하고 세밀하다.

타입 시스템

스위프트는 JS, Python처럼 Duck Type 시스템은 아니지만 명시적인 시스템이다. Objective-C 처럼 모든 객체가 Dynamic 타입은 아니지만, 프로토콜 타입을 활용하여 Dynamic하게 확장하면서도 Objective-C보다 안전하게 쓸 수 있다.

스위프트 타입

스위프트에는 크게 두 종류의 타입이 있다.

  • 이름있는 타입 : Named Type
  • 이름없는 타입 : Compound Type (int, char 등 Primitive Type)

합쳐진 타입은 Tuple이나 클로저/함수 타입으로 이름이 따로 정해지지 않고, 다른 타입들을 합쳐서 사용하는 타입이다. ex) (Int, (Int) -> (Int))

타입 검사

스위프트는 안전한 타입 언어를 표방한다.
"안전한 타입 언어"

값에 대한 타입을 명확하게 구분해서 사용할 수 있는 언어
컴파일러가 다른 타입으로 선언한 변수에 값을 전달하는 것을 미리 방지

컴파일 동안 안전한 타입 사용을 위해, 타입 검사를 진행한다.

타입 추론

타입 검사는 값에 대한 타입을 다르게 사용할 경우 컴파일 에러를 표시한다.
그렇다고 모든 변수를 선언할 때, 타입을 명시해야하는 것은 아니다. -> 타입 추론 덕분

func foo(x: Double) -> Int {...}
var doubleValue: Double = 3.141592
var unknown = foo(doubleValue)

func bar<T>(x: T) -> T {return x}
var floatValue: Float = -bar(1.414)

코드는 스위프트 타입 추론이 양방향으로 가능하다는 것을 보여준다.

foo() 함수의 타입 정의를 보면 리턴 타입이 Int라는 것을 유추할 수 있다.
따라서 foo() 함수 리턴값을 저장하는 unknown 변수는 Int 타입이다.
bar() 함수는 제네릭 타입으로 타입이 명시되지 않았지만 floatValue 변수의 타입이 Float라서 Float 타입으로 동작한다.

스위프트 타입 검사는 기존의 Objective-C처럼 명시적으로 타입을 선언한 정보를 근거로 타입 정보를 만드는 것도 가능하다.

스위프트 타입 추론은 3단계로 진행된다.

  1. "제약 만들기"
  2. "제약 계산하기"
  3. "제약 판단하기"

-> 스위프트 오픈소스 TypeChecker.rst 문서를 참고

타입 변환(Type Cast)

타입 변환은 종류가 전혀 다른 타입끼리 타입을 바꾸는 것이 아니다.
비슷한 종류끼리만 타입을 바꾸는 것을 의미한다.

"타입의 종류가 같다" -> 수학에서 구조 동일성을 가지는 벡터와 좌표 시스템처럼
데이터 타입의 메모리 구조가 동일하고 다루는 소재가 다른 타입끼리만 타입을 바꿀 수 있다.

ex.1) String 과 Int 는 구조가 다른 타입이기 때문에 타입 변환이 불가능하다.
ex.2) Struct 타입이나 Class 타입에서 상속받은 객체들끼리는 구조 동일성이 유지되기 때문에 타입 변환 가능.
ex.3) 숫자를 표시하는 타입들은 구조가 동일하기 때문에 서로 전환이 가능하다. -> 다만 값에 대한 손실이 발생할 수 있는 경우에는 반드시 타입을 지정해야 한다.

의미 있는 값 vs 의미 있는 레퍼런스

"의미 있는 레퍼런스"

레퍼런스 방식으로 참조하는 대상(대부분 객체 인스턴스)이 중요하다는 것

"의미 있는 값"

값 자체가 중요하다는 것

스위프트는 의미 있는 값으로 쏠려있다.

FP에서는 함수에서 다루는 변수가 레퍼런스가 아니고 불변 변수여야만 부작용이 없다.
따라서 값 자체를 다루는 것이 더 의미 있다.
값 방식은 Reference Count를 하지 않기 때문에 그만큼 병렬 처리나 성능 최적화 측면에서 유리하다.

Objective-C 에서는 클래스 객체를 사용하는 경우에만 의미 있는 레퍼런스를 사용하고, C 언어와 호환하기 위한 내장 타입들은 그대로 C 언어 방식(의미 있는 값)을 사용한다.
C 언어 타입은 컬렉션에 넣지 못하기 때문에, 메모리 관리가 안되고, 타입 변환이 불편해서 객체와 함께 사용하기에 불편하다.

타입별 성능 비교

대표적인 의미 있는 값 방식 구조체 타입과 의미 있는 레퍼런스 방식 클래스 타입, 프로토콜 타입에 대해서 메모리 공간, 참조 계산, 메서드 디스패치 동작을 비교하면 표와 같다.

스택 메모리의 경우, 사용할 때 SP(스택 포인터)를 증가시키고 사용하지 않을 때 감소시키면 된다.
반면에 힙 메모리를 사용할 경우, 비어있는 힙 공간을 찾고 처리를 위한 별도의 데이터 구조가 필요다핟.
여러 스레드에 대한 안정성 확보를 위한 동작이 필요할 경우를 비교하면 힙이 상대적으로 느리다.
프로토콜 타입으로 확장하는 경우에도 세 워드(64bit 기준 24bit)보다 작은 크기 값은 스택만 사용하지만, 그보다 크면 힙 공간을 추가로 사용하기 때문에 느려질 수 있다.

정적 디스패치는 컴파일 시점에 함수의 메모리 주소를 찾아두기 때문에, 런타임에는 해당 주소로 바로 이동한다.
특정 조건에서는 컴파일러가 속도 향상을 위해 인라인에 코드를 그대로 복사하기도 한다.
반면에 동적 디스패치는 런타임에 구현 함수 목록에서 함수 메모리 주소를 찾아 이동해야 한다.

요약

  • 어떤 타입을 사용할 지 결정하고, 타입에 적합한 메모리 관리 방식에 대한 고민이 프로그램 구조에 영향을 준다.
  • 스위프트는 다양한 타입을 지원하기 때문에, 선택의 폭이 넓다. 그것은 개발자의 책임이 큰 것이다.

열거 타입

C 언어나 Objective-C 언어에서 열거타입(enumeration)은 단순히 정수 타입 값을 나열하는 편의를 위한 것이다.
스위프트에서는 문자열 타입도 지정가능하고, 실수 타입도 지정할 수 있다.
뿐만 아니라 모든 값이 있을 필요도 없고, 모두 다 같은 타입이 아니어도 된다.
클래스처럼 함수를 만들 수도 있고 확장도 가능하다.

열거 타입과 프로토콜

열거 타입에 정의한 값은 기본적으로 Hashable 프로토콜을 지원해야 한다.

Hashable

public protocol Hashable: Equatable {
    var hashValue: Int {get}
}

Hashable 프로토콜을 Equatable 프로토콜을 상속받아 만들어져서, 추가적으로 Equatable 프로토콜에 있는 == 비교 함수도 구현해야 한다.

Equatable

public protocol Equatable {
    @warn_unused_result
    func == (lhs: Self, rhs: Self) -> Bool
}

열거 타입에서 일반적으로 동일한 타입 값을 사용하는 경우

enum PenModels{
    case BallPen
    case NamePen
}
  • 열거타입은 내부적으로 분기 처리를 하는데, 위에서부터 순서대로 비교하여 값을 할당한다.
    • 자주 사용하는 case를 가장 위에 두는 것도 하나의 방법?

프로토콜 타입과 증거 테이블

클래스 타입에 대한 상속과 다형성은 가상 함수들을 런타임에 찾는 다이내믹 디스패치 방식을 사용한다.
하지만 다른 타입들은 프로토콜 중심 프로그래밍 방식에 맞춰서 프로토콜 증거 테이블을 사용해서 다형성을 구현한다.
어느 모듈의 특정 타입에 대한 프로토콜 구현 함수 이름을 프로토콜 증거 테이블에서 바로 찾아 호출할 수 있다.

ex)

protocol Drawable{
    func draw()
}

struct Point: Drawable{
    var x, y: Double
    func draw(){...}
}

struct Line: Drawable{
    var x1, y1, x2, y2: Double
    func draw(){...}
}

var drawables: [Drawable]
for d in drawables{
    d.draw()
}
// 출처 : https://zeddios.tistory.com/597

d.draw()는 어떤 draw 메소드를 호출해야 하는가?
PWT를 사용하여 맞는 draw() 메소드 호출하게 함

변수를 포함하는 프로토콜을 컴파일하면 PWT(Protocol Withness Table)와 함께 VWT(Value Withness Table)도 만들어진다.
VWT는 의미있는 값을 가지는 타입에 대한 기본적인 동작을 다루는 생성, 복사, 파괴, 해제 함수들에 대한 참조 테이블이다.
VWT와 PWT 증거 테이블은 그림과 같이 값을 저장하는 저장소 데이터 구조를 참조한다.

값 크기가 버퍼크기보다 작으면 좌측 첫번째 구조처럼 스택공간을 그대로 저장한다.
만약 값 크기가 버퍼 크기보다 크면 좌측 두번째 구조처럼 힙에 큰 데이터 구조를 생성하고, 버퍼에는 힙 공간의 주소를 저장한다.
따라서 프로토콜 타입에서 스택만 사용하는 의미 있는 값을 사용하려면 버퍼보다 작은 데이터 구조를 사용해야 한다.

Equatable 프로토콜

Hashable 프로토콜과 마찬가지로 Hashable 프로토콜이 상속받은 Equatable 프로토콜에 대한 == 비교 함수도 동일하게 만들어진다.
== 비교 함수는 좌우에서 각각 .PenModels 파라미터를 받아서, 좌측 값에 대한 case 비교문 Int 값과 우측 값에 대한 case 비교문 Int 값을 구한다.
그리고 Int 타입의 == 비교함수를 통해서 최종적으로 같은 값인지 판단한다.
-> == 를 사용해서 enum 타입들을 비교할 때, lhs와 rhs 모두 Enum 분기처리를 통해 Int 값을 받아온 후, 가져온 Int 값을 통해 비교한다.

연관 값을 가지는 열거 타입

열거 타입에는 다른 언어에 있는 variants나 unions 형태로 여러 타입에 대한 값이 있을 수 있다.
이런 값을 열거 타입 연관 값이라고 한다.

enum PatientId{
    case socialNumber(String)
    case registeredNumber(Int)
}

var temporaryPatient = PatientId.registeredNumber(1550)

이런 경우는 열거 타입이지만, Hashable이나 Equatable 프로토콜을 구현하는 내부 함수는 만들어지지 않는다.
왜냐하면 case 구문으로 값이 같은지 비교하지 않더라도, 특정한 값을 바로 적용하기 때문이다.

가공 없는 값을 가지는 열거 타입

열거 타입에 특정 타입을 지정해서 가공 없는 값(Raw Value)을 할당하는 방식도 흔히 사용한다.

가공 없는 값을 갖는 열거 타입의 경우는 Grade 타입처럼 열거 타입 생성자가 만들어진다.
가공 없는 값을 전달하면 열거 타입 값들과 비교한다.
열거 타입과 매칭이 되면 값이 들어가고, 매칭이 되지 않으면 null을 할당하기 떄문에 enum.Grade? 타입을 리턴한다.
특이한 점은 스택에 만든 로컬 변수를 비교할 때, == 연산 함수를 사용하지 않고 ~= 연산 함수를 사용한다는 것이다.

** ~= vs == **

~=는 범위 지정이 가능, 패턴 매칭 가능
case문에서 사용되는 것 같음

ex)

switch point{
    case (0, 0): // ~= 연산자를 사용해서 패턴 매칭
        return true
    default: 
        return false
}

요약

다른 언어에서 열거타입은 편의를 위해 상수를 선언하는 타입이었지만, 스위프트에서 열거타입은 패턴 매칭과 함께 확장 가능한 데이터 구조 타입이다.
열거 타입은 구조체 타입과 같이 의미 있는 값타입이다.
Optional, Process, Bit 타입등이 열거 타입의 예 이다.


구조체 타입

스위프트 표준 라이브러리는 대부분 구조체 타입을 기반으로 만들어졌다.
그만큼 구조체 타입은 스위프트에서 가장 핵심적인 타입 중 하나다.
스위프트로 프로그래밍을 한다면 클래스보다 구조체를 사용하는 것이 더 효율적이다.

구조체 타입

Q)구조체는 C언어의 구조체와 Objective-C의 클래스 중 어디에 가까울까?

struct Car {
    let model = "apple"
}

스위프트 구조체 타입은 클래스와 비슷하게 LifeCycle을 가지는 타입이다.
생성자인 init() 초기화 함수가 만들어진다.
그리고 model 변수 속성이 불변이기 때문에, getter 내부적으로 함수가 만들어진다

init() 함수는 구조체를 위한 메모리 박스를 할당한 다음, 내부 변수 타입인 String 타입 초기화 함수를 사용해 "apple"를 지정한다.
그리고 이 값을 model 변수 위치에 저장한다.

가변 변수가 포함된 경우

struct Car{
    var driver = "tree"
}

앞서 살벼본 init() 함수와 별도로 init(driver: String) 함수가 추가된다. -> 자동 생성되는 init에서 let, var인 내부 변수의 차이

driver 변수에 대한 초기값을 지정해서 객체를 초기화 할 수 있는 추가 초기화 함수를 추가해준다.

따라서

let myCar = Car(driver: "tree") 처럼 초기화 값을 넘겨 초기화 가능하다.

그런데 init(driver: String) 구현이 독특하다.
구조체 타입을 초기화하기 위해 내부에서 init() 함수를 부르는 게 아니라, driver 초기 값과 함께 ㄴtruct $Car 명령을 실행하고 반환받은 값을 그대로 리턴한다.
-> 이 부분은 마치 C++ 구조체 초기화 함수처럼 구조체 내부 변수에 대한 초기값을 순서대로 전달해서 구조체 메모리를 초기화하는 방식과 비슷하다.
-> init() 함수를 부르지 않는다는 점을 기억하자

우선 driver 변수에 대한 문자열을 받아 설정하는 setter
첫번째 파라미터 변수는 자체 소유권을 갖는 문자열이고, 두번째 마라미터 변수는 inout으로 선언한 Car 구조체 변수다.
기존 Car 구조체 값이 그대로 전달되지만, 내부에서는 임시로 Car 구조체를 복사하기 위해 박스가 하나 더 만들어진다.
새로 만들어진 박스에 기존 Car 구조체를 복사하고, driver 변수에 첫 번째 파라미터 값을 할당한다.
새 박스의 값을을 두번째 파라미터 구조체인 Car에 복사하고 만들었던 박스를 메모리에서 해제한다.

함께 만들어지는 meterializeForSet() 함수는 var 변수에 대한 초기값을 바로 할당하는 경우가 아닌 경우에 사용한다.(lazy/Computed Property)
-> 개발자가 직접 호출할 수 있는 함수가 아니기에 신경 안써도 됨

구조체 타입 기반의 스위프트 타입

Int, Bool, Set, Ditionary, Array 모두 구조체로 구현됐다.
따라서 Objective-C와 다르게 스위프트는 의미 있는 값 방식으로 동작한다.
Swift 1.x 버전에는 초기 호환성을 위해 Objective-C 런타임 기반으로 동작하는 클래스 타입이 많았다.
지금은 스위프트 런타임으로 코코아 라이브러리의 상당수를 구조체 타입 기반으로 다시 작성했다.
따라서 Objective-C 기반보다 빠르다.

요약

구조체 타입은 성능 향상을 위해 대부분의 경우는 스택에 값을 할당하고 사용한다.
구조체 구조가 동적으로 변하거나 크기가 너무 크다면, 힙 공간을 예외적으로 사용하기도 한다.
힙 공간에 있는 구조체거나 글로벌 구조체의 경우 함수 범위가 벗어나도 해당 구조체를 참조할 수 있다. (참조 방식의 특징)
이런 경우 구조체는 객체에 대한 레퍼런스 방식과 비슷하게 동작하지만, 참조 계산을 사용하지 않아 순환 참조 문제가 발생하지 않는다.

개인적으로 이리저리 일이 있어서 6장 포스팅이 없다

다음주에 꼭 작성할 것을 스스로 약속

코코아 디자인 패턴

디자인 패턴

프로그래밍 과정에서 문제 해결을 위해 반복해서 경험하는 객체와 클래스 관계를 정리한 것

디자인 패턴은 "객체 중심 언어" 가 주목받으면서 자연스럽게 널리 사용되며, 알려졌다.

정보처리기사 시험에도 나오는 "GoF의 디자인 패턴"에서 나온 패턴이나 전략들이 소프트웨어 개발 전반에 영향을 미쳤다.

이 장에서 모든 디자인 패턴을 설명하지는 않는다. 대신 코코아 프레임워크에 반영되어 있는 디자인 패턴들 중 반드시 알아야 하는 핵심 패턴과 객체 사이의 결합성을 줄여주는 패턴에 대해 설명한다.

코코아 프레임워크 핵심 패턴 (3가지)

  1. 메모리 관리를 위한 두단계 초기화 패턴
  2. 객체의 역할에 따라 구분하는 MVC 패턴
  3. 객체가 처리할 메세지를 지연시키는 메세지 셀렉터 패턴

두단계 초기화 패턴

초기화 과정

NSObject에서 상속받은 클래스의 인스턴스가 만들어지기까지는 2단계의 초기화 과정이 이뤄진다.

Pen *aPen = [[Pen alloc] init];

alloc -> 메모리에 올리는 과정 : 힙 공간에 객체 인스턴스 메모리 공간을 할당
init -> 초기화 과정 : 객체 인스턴스 속성이나 내부 객체들을 초기화

다시 말해, 1단계에서는 +(instancetype)alloc 클래스 메서드로 객체 인스턴스 메모리를 할당
2단계에서는 메모리에 할당한 객체 인스턴스의 내부를 -(instancetype)init 인스턴스 메서드로 초기화한다.

당연하듯, 메모리에 객체 인스턴스가 존재하지 않으면 내부 프로퍼티, 객체들을 가르키는 주소또한 없기 때문에 1단계가 없이는 2단계를 진행할 수 없다.

반대로, 1단계는 됐는데, 2단계는 안됐다. -> 사용할 수 있는가?
A : 사용할 수 있을 수도 있다. (항상 사용할 수 있는 것은 아니기 때문)
기본적으로 1단계가 거치면 객체 내부의 모든 값들은 0으로 초기화된다. -> 하지만 init을 통해 2단계를 거치는 것은 권장한다

두 단계의 초기화 과정을 한 단계로 줄이기 위해 Convenience Methods를 클래스 메서드로 제공하기도 한다.

Pen *aPen = [Pen new];

지정 초기화 메서드

초기화 메서드는 메서드가 init- 으로 시작하는 조건만 부합하면, 인자 값에 따라 몇개의 초기화 메서드를 만들어도 된다.

NSString의 경우 -initWithBytes:length:encoding: , -initWithString: ... 등 다양한 초기화 메서드가 존재한다.

코코아에서는 여러 초기화 메서드들 중에서 기준이 되는 지정 초기화 메서드를 둘 것을 권장한다.
ex 1) NSString의 경우 빈 문자열 객체를 만드는 메서드가 지정 초기화 메서드이고, 다른 메서드들은 초기값을 넣을 수 있는 부가적인 보조 초기화 메서드이다.
ex 2) UIView는 -initWithFrame: 메서드가 지정 초기화 메서드이며, 그외의 다른 초기화 메서드는 지원하지 않는다.

이렇듯, 모든 클래스가 다르기 때문에 "클래스 레퍼런스 문서"를 참고할 것을 권장한다.

지정 메서드 초기화를 만들 때 지켜야 할 사항들
  • 상속받은 Sub 클래스에서 지정 초기화 메서드를 구현할 때는 반드시 부모의 지정 초기화 메서드를 호출해야 한다
  • 상속받은 Sub 클래스에서 부모에 없는 새로운 보조 초기화 메서드는 자기의 지정 초기화 메서드를 호출해야 한다
  • Super 클래스의 지정 초기화 메서드에서 반환되는 객체는 self에 할당한다.
  • Super 클래스의 지정 초기화 메서드에서 nil을 반환하면, 인스턴스 내부 변수를 사용하지 않고 nil을 그대로 반환한다.

작성 방법은 2.3절을 참고

MVC 패턴

Model - View - Controller로 이뤄진 패턴
1986년 OOPSLA에서 "A Diagram for Object-Oriented Programs"라는 논문에서의 MVC 패턴의 관계 그림

-> 현재 흔히 아는 MVC 패턴이라기보다는

1. Controller를 통해 사용자의 입력을 확인

2. 모델 데이터를 변경

3. 변경된 모델 데이터를 화면에 표시

하는 순차적인 흐름을 나타내었다

위에서 발전하여 우리가 아는 MVC 패턴 구조가 잡혔다.

또한, 코코아 프레임워크에서는 Model - Controller 관계에서 옵저버 패턴, 컴포지트 패턴을 사용하기도 한다.
옵저버 : Model - Controller 사이에서 NSNotificationCenter
컴포지트 : Controller가 해당 Model을 포함하는 경우 사용

모델 객체

  • 화면을 구성
  • 내부 처리를 위한 데이터를 추상화해서 지정한 자료구조로 표현
  • 데이터 처리 로직을 갖고 있음

앱에서 지속적으로 사용하는 데이터는 모델 객체 내부에 캡슐화되고, FileManager나 DB를 통해 영구적으로 저장하여 사용하기도 한다.

MVC 패턴에서는 Model 객체와 View 객체가 직접적으로 연결되면 안된다.

뷰 객체

  • 주로 UIView 클래스를 상속받아 화면 자체를 그리는 역할
  • 사용자의 입력을 받는다
  • 일반적으로 뷰 객체가 표시하는 정보는 모델 객체가 갖고 있는 데이터 기반 -> 이러한 이유로 서로 의존 관계가 생기면 안됨
  • 하나의 View가 0개 이상의 Model Data와 매칭될 수도 있다.

View 객체는 화면 구성을 위해 Model 객체와 밀접한 관계를 갖고 있지만, 해당 관계를 느슨하게 하는 것이 MVC 패턴의 핵심이다.
그 상호 관계를 느슨하게 연결해주는 역할을 바로 Controller 가 담당한다.
따라서 Controller가 연결해주는 Model 객체에 따라서 View 객체는 얼마든지 재사용이 가능하다.

컨트롤러 객체

Controller는 View와 Model사이에서 사용자 입력과 데이터 변화에 대한 연결을 해주는 중재자 역할을 한다.
V -> C -> M : 사용자의 입력에 따른 새로운 데이터 변화를 확인, 수정, 삽입, 삭제
M -> C -> V : 변경된 데이터를 받아 View에 전달, 화면에 표시, 인터랙션

View 객체의 화면 구조가 복잡하거나 사용자 입력 방식이 다양할수록 Data Model도 상대적으로 커진다.
이어서 Data Model이 커지면 해당 Model 객체를 조작해주는 Controller의 동작이 복잡해진다.

Massive

  • MVC 패턴에서 가장 고민거리는 Controller를 구현하는 코드가 복잡하고 길어지는 것이다.

가볍고 재사용성이 높은 Controller 객체를 만들기 위해 다양한 MVC 변형 패턴들을 함께 사용한다 (MVC-C 등)
또한 MVVM 패턴이나 VIPER 등의 다양한 패턴들이 존재한다.

메세지 셀렉터 패턴

다른 객체가 코코아 객체에게 메세지를 보내면 코코아 런타임은 해당 객체 메서드 중, 메세지를 처리한 메서드를 찾아 해당 함수 포인터를 호출한다.

다이내믹 디스패치

  • 런타임이 메세지에 해당하는 객체 메서드에서 찾는 과정 : 런타임 중 찾기
  • 1.4.2 절에서 나옴

런타임에서 메세지를 처리하기 이전에 메서드를 선택, 메서드 바인드를 지연하기 위한 패턴에 대해 알아보자

SEL(셀렉터)와 IMP(구현 포인터)

코코아 객체의 메서드를 찾기 위해서는 SEL과 IMP를 사용해야 한다.
SEL : 메세지를 받을 객체의 메서드 중, 적합한 메서드를 고르는(Select) 역할
셀렉터가 없다면 C++처럼 Compile과정에서 객체 메서드에 대한 함수 포인터를 찾아 미리 고정된 메모리 주소에 바인드해놓아야 한다. -> Static 디스패치

SEL theSelector = @selector(drawSomething);

이처럼 셀렉터를 선언할 때는 SEL 타입을 사용하고, 변수는 @selector() 예약어를 사용한다.
"theSelector는 drawSomething 이라는 이름을 가진 메서드를 골라서 쓸 수 있는 상태"
if) 해당 객체에 메서드가 없다면, 동적으로 바인드되지 않아 에러가 날 수 있다.

참고로 런타임 API 중 method_getName() 함수를 사용하면 @selector()와 동일하게 셀렉터를 찾을 수 있다.

// 객체 메서드 구조체
struct objc_method{
    SEL method_name;
    char * method_types;
    IMP method_imp;
};
typedef objc_method Method;

method_name : aㅔ서드 이름과 파라미터 키워드를 포함하는 메서드 시그니처를 SEL 타입으로 저장
method_types : 파라미터들에 대한 타입을 문자열로 저장
method_imp : IMP 타입으로 메서드 구현 포인터를 저장

런타임 API 중 class_getClassMethod() or class_getInstanceMethod() 함수에 클래스 타입과 SEL 타입을 넘기면 위의 Method 구조체를 얻을 수 있다.

상속 관계에 따라서 다이내믹 디스패치에 의해 셀렉터가 항상 동일하지 않을 수 있다.

셀렉터 실행과 지연 실행

아래 세 줄은 동일한 동작을 하는 코드이다.

[myPen drawSomething]; // 직접
[myPen performSelector:@selector(drawSomething)]; // NSObject의 -persormSelector:를 활용하여 drawSomething 메서드 셀렉터를 찾아 실행
[myPen performSelector:theSelector]; // 미리 찾아놓은 @selector 

But, 1번과 2번은 특이하게도 변형이 가능하다.
코코아에 있는 NSSelectorFromString() 함수를 사용해서 문자열로 메서드 시그니처를 입력해서 셀렉터를 실행할 수 있다.
더 나아가, 스크립트 언어와 연결해서 코코아 객체에 메세지를 보내는 방식도 가능

메세지를 보내면서 일부러 전달 시점을 지연시키는 방법도 가능하다.
NSObject에 -performSelector:(SEL)aSelectorwithObject:(id)anArgument afterDelay:(NSTimeInterval)delay 를 사용하면 된다.
delay만큼 지연되며, 내부적으로는 해당 스레드에 이벤트를 감시하는 RunLoop에 전달되고, RunLoopdㅔ서 지연시간동안 기다린 후에 해당 객체로 메세지를 전달한다.
해당 RunLoop에 이벤트가 쌓여있거나 할 때는 항상 delay에 맞춘다는 보장은 없다.

최근에는 셀렉터 실행 방식보다는 저수준 병렬 처리 라이브러리 (GCD) 방식을 권장한다.

타깃과 액션

셀렉터 패턴을 자주 활용하는 경우는 타깃과 액션을 사용하는 경우이다.
타깃 : 특정 이벤트를 받을 객체
액션 : 이벤트를 받아(발생했을 때) 처리할 메서드

타깃-액션 패턴은 동적으로 변경이 가능하다. -> 재사용이 가능하다.

말했듯, 타깃-액션 패턴도 셀렉터를 사용한다.
셀렉터는 문자열로 지정하는 방식도 가능하기 때문에, 특정 객체의 이벤트 처리를 실행 중 타깃-액션으로 지정해서 연결할 수 있다.

요약

코코아 프레임워크는 다양한 디자인 패턴을 기반으로 개발되어 있다.
뼈대를 이루는 핵심적인 디자인 패턴은 반드시 숙지해야 한다.
모르더라도 개발은 할 수 있지만, 이해한다면 모든 코코아 객체를 다루기 훨씬 쉽다.

불변객체와 가변객체

코코아 프레임워크 객체는 크게 2가지로 분류할 수 있다.

  1. 초기화한 이후에는 내부 데이터를 변경할 수 없는 불변 객체
    • 문자열을 다루는 NSString 클래스는 문자열을 바꿀 수 있는 인터페이스가 없는 불변 객체다.
  2. 반대로, 초기화한 이후에도 내부 데이터를 변경할 수 있는 가변 객체

불변 객체

Immutable 객체들은 다음과 같은 특징을 갖는다.

  • 초기화 이후 객체 내부의 값이나 상태가 변하지 않는다.
  • 불변 속성 때문에 여러 객체에서, 여러 스레드에서 참조해도 안전하다.
    • 멀티 스레드 환경에서 같은 객체에 접근하여도 값/상태가 변경되지 않아, Concurrency 문제가 야기되지 않음
  • 값이 바뀌는 상황을 고민하지 않기 때문에, 설계가 쉽고 구현하기 수월하다.
  • 객체 내부에 모순된 상태가 줄어들어 부작용이 적다.

이런 불변 객체의 특징 때문에 FP에서는 불변 객체를 더 많이 쓴다.
심지어 불변 객체만 사용하라고 권장한다.
불변 객체의 사용은 의도하지 않은 부작용 의 가능성을 낮춘다.

불변 객체만을 사용하는 방식으로 구현하면 객체들 사이의 참조 관계가 비교적 단순해진다.
하지만 값이 다르면 새로운 객체를 만들어야한다는 단점이 있다.

불변 객체 인스턴스 개수가 많아지는 경우를 대비해 메모리를 효율적으로 사용하기 위한 최적화 과정을 두기도 한다.
불변 객체 중에 '정체성' 이 동일한 객체가 이미 존재하는지 확인하고, 중복된 불변 객체를 만들지 않도록 최적화한다.
특히, NSString 클래스 리터럴 문자열은 불변 객체로, 프로세스 메모리 영역에 문자열을 할당해서 중복 생성을 줄인다.

불변 객체 클래스

불변 객체는 초기화 메서드로 객체의 초기값을 지정한 이후에는 객체 상태를 변경할 수 있는 메서드를 제공하지 않는다.
대부분 객체 내부의 인스턴스 변수는 감춰지거나(private) 보이더라도 읽기만 가능하다.

코코아 프레임워크에 불변 객체 클래스가 많지만, 자주 사용하는 클래스는 다음과 같다.

  • 타입별 데이터 구조를 다루는 클래스 : NSNumber, NSValue, NSData
  • 규격에 맞춰 데이터를 다루는 클래스 : NSString, NSDate, NSURL
  • 다른 객체를 참조하는 클래스 : NSArray, NSDictionary
  • 다른 객체를 꾸며주는 클래스 : NSFont, NSColor

이 클래스 중 일부는 동일한 역할을 하면서 데이터를 변경할 수 있는 동등한 수준의 가변 객체 클래스(NSMutableString 등)가 존재하기도 한다.
반면에 가변 객체 없이 값만 저장하는 클래스도 있다.

Ex) NSString이나 NSData는 NSMutableString, NSMutableData가 있지만, NSNumber나 NSColors는 가변 객체 클래스가 없다.
만약 가변 객체 클래스가 존재한다면, 객체를 복사할 때 -> -mutableCopy로 가변 객체를 복사할 수 있는지도 확인해야 한다.

불변 객체 구현하기

고려해야 할 사항

  • 초기화 이후 내부 값이나 상태를 재정의하는 메서드가 없어야한다.
  • 내부 전용 인스턴스 변수는 감추고 접근하지 못하도록 한다.
  • 인스턴스 변수들은 상속이 불가능하도록 private 속성을 갖도록 하고, 읽기 전용 접근자만 허용한다.
  • 내부 데이터를 바꾸는 게 아니라 새로운 값을 반환하도록 구현한다.
  • 내부에서만 사용하는 가변 객체가 있다면, 외부에서 내부 가변 객체를 반환하거나 수정할 수 있는 인터페이스가 없어야 한다.

하지만 다음의 경우라면 불변 객체보다는 다른 방법을 고민해봐라.
다른 방법 : 가변 객체로 설계를 바꾸고 변동 가능성을 낮추며 구현하라

  1. 내부 데이터가 너무 커서 복사하기 부담스러운 경우
    • 복사하여서 새로운 값을 반환해야 하기 때문
  2. 초기 생성자에서 모든 값을 정할 수 없고, lazy 혹은 점진적으로 데이터를 정해야 하는 경우
  3. 클래스 내부에 구조체를 포함하고, 그 구조체 내부에 변경가능한 하위 요소가 있는 경우
  4. 상태를 공유하는 공용 컨테이너로 동작하는 경우

요약

불변 객체 클래스를 직접 만들거나 코코아 프레임워크에 있는 불변 객체를 사용하면 참조 관계가 단순해져서 복잡도를 낮출 수 있다.
굳이 가변 객체를 사용할 이유가 없다면, 불변 객체를 사용하자.
부작용이 발생할 수 있는 위험을 줄일 수 있다.
최근에는 블록을 활용한 핸들러 코드나 비동기 프로그래밍 방식에서도 불변 객체가 안전하다.
다중 스레드상에서도 동시에 접근해도 훨씬 안전하다.


가변 객체

가변 객체의 특징

  • 초기화 이후에도 객체 내부 값이나 상태를 추가, 삭제, 변경할 수 있다.
  • 여러 객체나 여러 스레드에서 참조하기 위해서는 Concurrency 문제에 대한 예외 처리가 필요하다
  • 성능 특성을 고려해야 한다
  • 어느 시점이든 값이 변경되는 것에 대한 부작용이 생길 수 있다.

가변 객체는 불변 객체의 정 반대의 특성을 갖고 있다.
내부 데이터를 변경할 수 있기 때문에, 변경하는 값이 유효한지 확인해야 한다.

가변 객체 클래스

불변 객체를 사용하는 것이 장점이 많기는 하지만, 가변 객체를 사용해야 하는 경우도 있다.
객체를 초기화하는 과정에서 1) 모든 데이터를 초기화할 수 없을 수도 있고, 2) 점진적으로 값을 변경해서 최종 값과 상태를 사용 하는 경우도 있다.

코코아 프레임워크가 제공하는 가변 객체 클래스 중 자주 사용하는 클래스는 다음과 같다.

  • 다른 객체를 참조하는 클래스 : NSMutableArray, NSMutableDictionary, NSMutableSet
  • 특정 타입을 집합으로 다루는 클래스 : NSMutableIndexSet, NSMutableCharacterSet
  • 문자열을 다루는 클래스 : NSMutableString, NSMutableAttributedString
  • 특정 데이터 구조를 클래스 : NSMutableData, NSMutableURLRequest

불변 객체와 달리 접두어 다음에 Mutable을 넣어서 불변 객체와 구분하고 있다.
내부에는 값을 추가, 수정, 삭제할 수 있는 메서드들이 존재한다.
ex) -insertObject:atIndex: 등

가변 객체 참조 사례1 : 가변 모델 객체와 뷰 객체

가변 객체를 참조하는 경우는 참조하는 객체 내용이 변경되면 그 변화에 따른 일련의 추가 작업이 필요하다.
코드처럼 가변 객체를 참조하는 경우를 살펴보자
ViewController에는 UITableView가 있어서 모델에 해당하는 PenHolder 내부 펜 목록을 테이블 뷰에 표시하는 코드이다.

@interface ViewController () <UITableViewDataSource>

@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (nonatomic) NSArray* tableItems;

@end

@implementation ViewController

- (void)viewDidLoad{
    [super viewDidLoad];

    PenHolder* penHolder = [[PenHolder alloc] init];
    //penHolder에 값을 넣었다고 가정
    self.tableItems = penHolder.pens;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger) section{
    return self.tableItems.count;
}

//이하 생략

@end
  1. PenHolder.pens에는 이미 20개의 Pen 객체가 가변 배열(NSMutableArray)에 포함되어 있다.
  2. 내부 인스턴스 변수인 tableItems 배열에 penHolder.pens 가변 배열을 참조한다.
  3. -tableView:numberOfRowsInSection: 메서드에서 tableItems.count를 리턴해서 테이블 뷰의 Row를 20개 그린다.

데이터 변경

If) 만약 테이블 뷰가 그려진 이후, penHolder.pens 가변 배열의 데이터가 삭제되거나 새로운 Pen 객체를 추가한다면
TableView는 DataSource가 바뀐 것을 모르고, 여전히 기존 데이터만 보여주고 있을 것이다.
이처럼 가변 객체를 참조하는 경우에는 TableView 갱신 문제처럼, 모델에 바뀐 데이터를 화면에 반영하기 위해 KVO/NSNotificationCenter 같은 옵저버 패턴을 활용할 것이다. 결국 가변 데이터의 흐름을 따라서 컨트롤러와 뷰까지 영향을 주는 코드가 이어지게 된다.

PenHolder 클래스의 pens 가변 배열을 외부에서 직접 바꿀 수 없도록 불변 객체로 만들더라도, 기술적으로 완벽한 불변 객체가 아니다.
왜냐하면 키-값 코딩(KVC)를 사용해서 -setValue:forKey: 같은 메서드로 우회적으로 pens 프로퍼티를 변경할 수 있기 때문이다.

그래서 읽기 전용 프로퍼티로 객체 외부에 노출하기보다 감추는 것이 좋다.
객체를 감추는 방법은 구현부에서 클래스 확장 카테고리로 확장하거나, 내부를 변경하는 인터페이스를 제공하고 인터페이스에서 데이터 흐름에 따라 다른 코드로 진행되게 만들기를 권장한다.

가변 객체 참조 사례 : NSMutableSet와 가변 객체

NSMutableSet은 내부에 여러 타입의 객체를 담을 수 있지만, 동일한 객체 인스턴스를 중복해서 추가하지 못한다.
하지만 가변 객체를 참조할 때, 일시적으로 동일한 객체 인스턴스를 포함할 수 있다.

  1. 가변 집합 set을 만들어서 "unique-key" 문자열 추가
  2. 동일한 내요 문자열을 추가할 수 없다.
  3. 다시 "unique"라는 가변 문자열 객체를 추가하면 값이 동일하지 않기 때문에 정상적으로 추가됨
  4. 3에서 추가한 가변 문자열 객체에 "-key"를 덧붙이면, 실제로는 동일한 내용의 문자열이 set에 존재한다.
  5. 이런 조건에서 가변 집합 set을 복사하면, 복사본을 만들면서 집합 내부 객체를 다시 비교하기 때문에, 동일한 값을 가진 객체는 중복해서 만들어지지 않는다.

-> 4번 집합 ["unique-key", "unique-key"]
-> 5번 집합 ["unique-key"]

결국 5번 집합은 4번집합을 복사한 것이지만, 5번 집합 요소를 보면, 4번 집합과는 다른 집합이 된다.

객체 중복성 검사
NSSet이나 NSDictionary처럼 키 값을 사용하는 컬렉션은 내부적으로 객체 중복성을 검사할 때, 객체의 -hash와 -isEqual 메서드가 중요하다
반면에 정렬한 배열처럼 순서가 중요한 컬렉션은, 순서를 정하기 이ㅜ한 비교 메서드가 중요하다.

요약

가변 객체를 사용하는 경우, 가변 객체의 내부 값이 바뀌기 때문에 생기는 부작용에 대비해야 한다.
그러기 위해서 가변 객체를 변경하는 메서드가 배타적으로 동작해야 한다.
데이터 내용이 바뀌는 시점에 따라 데이터 흐름을 처리하는 코드가 있다면, 변화를 감지하기 위한 디자인 패턴(옵저버)을 적용하는 게 좋다
가변 객체를 참조하는 경우는 의도치 않은 변화에 대비해 동작 방식을 정확하게 이해하고 있어야 한다.

객체 복사

2장 메모리 관리에서 객체 인스턴스를 메모리에 만들고, 객체를 포인터 변수로 참조하면서 소유권을 갖거나, 소유권 없이 참조하는 방식에 대해 설명했다.
3장 자동 메모리 관리에서는 ARC 방식을 기준으로 자동 참조 계산과 참조 방식에 따라 다른 구현 방식에 대해 설명했다.

이번에는 이미 만들어진 객체 인스턴스를 참조하는 경우와 달리, 객체 인스턴스 데이터를 새로운 객체 인스턴스로 복사해야하는 경우 필요한 프로토콜을 살펴본다.
그리고 얕은 복사가 아닌 깊은 복사를 위한 아카이브 방식에 대해 알아보자

NSCopying 계열 프로토콜

코코아 프레임워크에서는 객체를 복사하기 위한 방법으로 NSCopying / NSMutableCopying 프로토콜을 지정해서 구현하는 방법을 권장한다.
NSCopying 프로토콜은 객체를 복사하기 위해 클래스에 미리 구현해야 하는 복사용 메서드 목록을 지정해놓은 프로토콜이다.
애플이 만든 코코아 객체들은 이미 NSCopying 프로토콜을 기반으로 만들어져있어서 객체를 복사하기 쉽다.

복사만 가능한 객체

NSCopying 프로토콜은 구현해야 할 메서드가 딱 하나이다.
내가 만든 객체가 복사가능한 객체여야 한다면, NSCopying 프로토콜 내에 있는 해당 메서드를 구현하면 된다.

-(id)copyWithZone: (NSZone*)zone;

앞서 2장에서 설명했듯 현재는 메모리 영역을 zone으로 나누지 않기때문에, 인자는 nil을 넘겨도 된다.
-> 현재 모든 앱은 단일 존(기본 존)을 갖기 때문

final class AObject: NSObject, NSCopying {
    var num1: Int
    let num2: Int

    init(_ num1: Int, _ num2: Int) {
        self.num1 = num1
        self.num2 = num2
        super.init()
    }

    func copy(with zone: NSZone? = nil) -> Any {
        let newObj: CopyTest = .init(num1, num2)
        return newObj
    }
}

final class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let obj1: CopyTest = .init(0, 1)
        let obj2 = obj1.copy()
        //obj1 과 obj2 는 같은 num1과 num2를 갖고, 서로 다른 주소값을 갖는다 -> 등가성은 갖지만, 동일성은 갖지 않는다.
        obj1.num1 = 100
        //obj1 과 obj2 는 같은 num2를 갖고, 서로 다른 num1 값과 주소값을 갖는다
        print(obj1.num1)
        print((obj2 as? CopyTest)?.num1 ?? 0)
    }
}

위의 코드는 obj1과 obj2가 복사하여도 서로 다른 주소값을 갖는 객체를 가르키고 있기때문에, 얕은 복사이다.

복사와 수정이 가능한 객체

  • 코코아 프레임워크에서 -copy 메서드로 복사하는 객체는 불변(immutable) 객체라고 가정한다.
    • NSString 객체 인스턴스를 복사하면 내부 문자열 데이터를 복사해서 새로운 NSString 객체 인스턴스를 만들지만, 이 문자열 객체 내용은 못 바꿈
  • 만약 수정가능한 가변(mutable) 객체로 복사하려면 -mutableCopy 메서드로 복사해야 한다.
    • 해당 메서드는 NSMutableCopying 프로토콜 내에 존재한다.
  • 객체를 복사하는 과정에서 복사할 원래 객체가 가변인지 불변인지는 상관없다.
    • 새로 만들 객체가 가변인지 불변인지만 중요함

요약

애플 프레임워크에 포함된 클래스는 대부분 NSCopying과 NSMutableCopying 프로토콜을 구현하고 있다.
직접 개발한 클래스도 복사 가능한 객체라면, 애플이 만든 클래스와 마찬가지로 직접 구현을 하면 된다.


얕은 복사(shallow copy) vs 깊은 복사(deep copy)

NSArray처럼 내부에 다른 객체를 포함하는 경우에는 객체를 복사할 때, 주의해야 한다.
참조 포인터 변수를 복사해서 포인터에 있는 힙공간 주소값을 복사한다고해서, 참조하던 객체와 동일한 복사본이 하나 더 생기진 않는다.
참조하는 객체를 가르키는 포인터 변수만 하나 더 생길 뿐이다.
이처럼 주소값만 복사하는 방식을 '얕은 복사' 라고한다.

얕은 복사

@interface PenHolder : NSObject <NSCopying> {
  NSMutableArray *_pens;
}
-(void) addPen:(Pen*)pen;
-(void) removePen:(Pen*)pen;

@end

@implementation PenHolder

- (id)copyWithZone:(NSZone *)zone {
  PenHolder *copiedHolder = [[[self class] alloc] init];
  copiedHolder->_pens = [_pens mutableCopy];
  return copiedHolder;
}

@end

PenHolder 클래스는 Pen 객체를 참조하는 가변 배열 컬렉션 NSMutableArray 객체를 포함한다.
PenHolder 객체를 복사하는 -copyWithZone: 메서드는 새로운 PenHolder 객체 인스턴스 copiedHolder를 만들고, 자신의 _pens 배열을 복사해서 copiedHolder 객체 _pens 변수에 설정한다.

해당 복사는 새로운 객체를 만드는 것이 아닌, 기존의 _pens 배열을 참조하는 copiedHolder를 참조하는 포인터가 생기는 것이다.

하지만 Foundation 프레임워크 내에 있는 모든 클래스는 얕은 복사로 구현되어 있다.

이 상태에서 copiedHolder 인스턴스 내 _pens 집합에 있는 Pen 객체를 수정하면, 기존의 penHolder 인스턴스 내 _pens 집합의 Pen 객체로 같이 변경된다.

깊은 복사

앞서 말한 것처럼 NSArray 계열 컬렉션 클래스의 경우 -initWithArray: CopyItems: 초기화 메서드를 활용해서 생성할 때만 깊은 복사가 가능하다.

만약 객체에서 객체를 포함하고, 또 그 객체에서 다른 객체를 포함한다면 -> DFS 방식으로 하위 노드들부터 탐색해서 새로운 객체를 만들어서 복사하고, 이어서 다른 노드를 탐색하다보면 모든 객체를 복사할 수 있다.

하지만 DFS 방식으로 깊은 복사를 하더라도 객체 참조 관계가 기존 객체와 항상 완벽하게 동일하다고는 할 수 없다.
-> 탐색을 하다 보면 어떤 객체는 중간에 여러 객체에서 여러번 참조될 수도 있고, 특정 객체들은 순환 참조 문제가 있을 수도 있다.
-> 약한 참조를 갖고 있으면 해당 객체를 복사하고 약한 참조로 지정해주어야 한다.

따라서 복잡한 참조 관계를 가진 객체를 복사하는 경우에는 객체 참조 그래프를 활용하는 것이 좋다

요약

코코아 프레임워크의 클래스는 객체를 복사할 경우, 얕은 복사 형태로 참조 관계를 유지한다.
특히 NSArray, NSSet 같은 컬렉션 객체나 다른 객체를 참조하는 객체를 복사하는 경우에는 깊은 복사를 고민해봐야 한다.

+ Recent posts