스위프트와 오브젝티브C의 상호관련성
스위프트는 오브젝티브C와 서로 호환되도록 만들어졌다. 상호 호환성을 유지하고 강화하기 위한 애플의 노력에도 불구하고 현재까지도 오브젝티브C에 있는 상당수의 기능과 함수가 스위프트와 호환되지 않고 있다.
iOS 또는 macOS 개발을 최근에 시작했다면, 오브젝티브C에 대한 경험이 전혀 없을 것이고, 배울 계획이 없을 수도 있지만, 스위프트 개발자로서 오브젝티브C를 아는 것은 매우 중요하다. 지금까지 소개된 오브젝티브C 프레임워크와 클래스 라이브러리의 수가 무척 많으며, 스위프트 프로그래밍을 하다 보면 어느 시점에는 결국 오브젝티브C 코드를 사용해야 하기 때문이다.
끝이 없다. 끝이 없어. (...)
초기화 방식
스위프트에서 오브젝티브C 프레임워크를 사용하려면 아래와 같이 코드 상단에 해당 프레임워크의 이름을 써서 임포트하면 된다.
// 파운데이션 프레임워크에 포함된 오브젝티브C 클래스를 사용할 수 있게 된다.
import Foundation
...
오브젝티브C와 스위프트는 메서드와의 상호작용 방식에 차이가 있다. 스위프트, C# 혹은 자바 등 언어에서는 메서드라는 표현이 일반적이지만, 오브젝티브C에서는 메세지message 라는 개념을 사용한다.
오브젝티브C의 명령 실행 체계인 메세지는 리시버receiver, 셀렉터selector, 파라미터 parameters 등의 요소로 구성된다. 그 중 리시버는 메서드가 실행 결과를 받게 될 대상 객체이고, 셀렉터는 메서드의 이름이며, 파라미터는 메서드에 전달되어 실행될 객체를 의미한다.
NSString *postalCode = [[NSString alloc] initWithFormat: @"%d-%d", 32259, 1234];
// postalCode = "32259-1234"
/*
receiver: postalCode
selector: length(별도의 파라미터 x)
*/
int len = [postalCode length];
// len = 10
(...) 어질어질하다.
오브젝티브C 메세지로 모델model을 전달할 때는 보통의 컴파일 시점의 바인딩
compile-time binding이 아닌, 동적 바인딩dynamic binding 기법을 사용한다. 이렇게 하면
메세지를 미리 구현해 놓지 않고, 런타임에 해당 메세지를 구현 및 실행하는 것이 가능해진다. 런타임에서 특정 객체가
즉각 메세지에 반응할 수 없는 경우에도 상속 연쇄는 해당 객체를 찾을 때까지 기다렸다가 메세지를 전달한다. 메세지를
찾지 못하면 nil 을 반환하며, 이는 컴파일러 설정에서 변경할 수 있다.
오브젝티브C 프레임워크를 스위프트로 임포트하면, 클래스의 init 초기화 객체 initializers 는 init
메서드로 변환된다. 초기화 객체는 initWith: 로 시작하는데, 스위프트 메서드로 변환되면서 셀렉터 이름에서
With 가 삭제된 채 편의 초기화 객체로 임포트되고, 셀렉터의 나머지 부분은 기명 파라미터를 이용해 정의된다.
또한 스위프트는 객체를 생성할 때 일관성을 유지하기 위해 모든 클래스 팩토리 메서드
factory methods를 편의 메서드convenience methods로 바꿔서 임포트한다.
// NSString initWithFormat: -> 변환
public convenience init(format: NSString, _ args: CVarArgType...)
앞선 코드를 Swift로 변환해보며 오브젝티브C와의 차이를 확인해보자.
// NSString *postalCode = [[NSString alloc] initWithFormat: @"%d-%d", 32259, 1234];
var postalCode: NSString = NSString(format: "%d-%d", 32259, 1234)
var len = postalCode.length // 10
스위프트에서 작성한 클래스를 오브젝티브C에서 직접 사용하는 것도 가능하다. 이 경우 해당 클래스가 NSObject
를 상속하거나 또 다른 오브젝티브C 클래스를 상속한 것이어야 한다.
만일 해당 클래스가 NSObject 를 상속하지 않은 경우에도 @objc 속성을 사용해서 해당 메서드에 접근할 수
있다. @objc 속성은 므윞트 메서드, 프로퍼티, 초기화, 객체, 서브스크립트, 프로토콜, 클래스, 열거형 등에
모두 적용 가능하다. 이 속성을 오브젝티브C 코드에서 쓸 다른 이름으로 오버라이딩하고 싶다면 @objc(name)
속성을 사용할 수 있다. 이 때 name 은 사용자가 지정할 수 있다.
@objc(objcMovieLst)
class MovieList: NSObject {
private var tracks = ["A", "B", "C"]
}
스위프트 타입의 호환성
실패 가능 초기화 기능에 대해 설명하고, 이를 남용하지 말도록 권장한다.
구조체와 클래스를 개발하고자 한다면, 마이클 패더스Michael Feathers가 주창한 SOLID 원칙을 준수해야 한다. SOLID는 객체지향 디자인과 프로그래밍을 설명하기 위한 다섯 가지 원칙이다.
- 단일 책임 원칙: 하나의 클래스는 오직 단 하나의 책임만 부담해야 한다.
- 개방 폐쇄의 원칙: 소프트웨어는 확장이라는 측면에서는 개방되어 있어야 하고, 수정이라는 측면에서는 폐쇄되어야 한다.
- 리스코프 치환 원칙: 특정 클래스에서 분화되어 나온 클래스는 원본 클래스로 대체 가능해야 한다.
- 인터페이스 분리 원칙: 개별적인 목적에 대응할 수 있는 여러 개의 인터페이스가 일반적인 목적에 대응할 수 있는 하나의 인터페이스보다 낫다.
- 의존성 역전 원칙: 구체화가 아닌 추상화를 중시한다.
컬렉션 클래스 브릿징
스위프트는 파운데이션 컬렉션 타입인 NSArray , NSSet , NSDictionary 를 스위프트 배열, 세트, 딕셔너리
타입으로 브릿징할 수 있도록 지원한다. 이 기능을 이용하면 컬렉션을 활용하는 다수의 네이티브 스위프트
알고리즘에서 파운데이션 컬렉션 타입과 스위프트 컬렉션 타입을 서로 교환해서 사용할 수 있게 된다.
NSArray를 Array로 브릿징
NSArray 를 파라미터화된 타입으로 브릿징하면 [ObjectType] 형식의 배열이 만들어지고, 별도의 파라머티화된
타입을 지정하지 않으면, [AnyObject] 형식의 배열이 만들어진다.
[AnyObject] 타입으로 브릿징된 NSArray 를 활용해야 하는 경우, 배열이 다른 타입이 포함되어 있을 때는
이들 인스턴스를 별도로 관리해야 한다. 스위프트는 이를 위해 강제 언래핑forced unwrapping과 타입
캐스팅 연산자type casting operator를 제공한다. [AnyObject] 배열에 서로 다른 타입이 포함되어
있는지 알지 못한다면, nil 을 반환할 수 있는 타입 캐스팅 연산자를 사용해 다른 타입의 인스턴스를 안전하게
관리할 수 있다.
NSSet을 set로 브릿징
NSSet 를 파라미터화된 타입으로 브릿징하면 Set<ObjectType> 형식의 타입의 세트가 만들어지고, 별도의 파라머티화된
타입을 지정하지 않으면, Set<AnyObject> 형식의 배열이 만들어진다.
NSDictionary를 dictionary로 브릿징하기
NSDictionary 를 파라미터화된 타입으로 브릿징하면 [ObjectType] 형식의 타입의 딕셔너리가 만들어지고,
별도의 파라머티화된 타입을 지정하지 않으면, [NSObject: AnyObject] 형식의 딕셔너리가 만들어진다.
맺음말
이번 장은.. 책을 통해 읽으면서도 아직 크게 와닿지 않는 부분이 많았다. 한 치 앞도 보지 못하는데, 저 멀리 바다를 보라는 것 같은 느낌. 문법적인 것 보다는 실제로 오브젝티브C를 이해하려 한다면 얼마나 오랜 시간이 걸린 뒤일까.. 이 글을 작성한 현 시점에서의 막막함에 남겨본다.