Swift의 특징들 : Modern Programming Language Design
옛날 Objective-C를 배우고 사용할 때 개인적으로 참 불편하고 어색했으며 자주 사용해도 별 매력을 느끼지 못한 프로그래밍 언어였다. 나름 스티브 잡스가 Next라는 회사에서 고민고민하면서 역작을 만들어 냇음에도 불구하고 말이다.
덕지덕지 붙는 +, – 기호와 ()괄호, [] 대괄호들, 불필요한 클래스의 접두어들들이 코드를 읽기 어렵게 만들었고, 나름 C/C++에 익숙하다고 했음에도 적응이 쉽지 않았다.
최근 Apple에서 만든 Swift Language를 사용할 일이 있었는데, Swift를 쓰면서 애플과 Chris Lattner가 Objective-C의 단점을 경험하며 이번에는 정말 꽤 제대로 만들었구나 라는 생각이 들었다.
Swift의 튜토리얼을 쓸 생각은 없다. 다만, 간단하게 Swift를 만들 때 애플이 나름 치열하게 생각하고 고민했을 것 같았던 점들을 나열해 보며 Modern Programming Language Design의 방향이 어떤식으로 가고 있는지 살펴 볼까 한다.
-> 라인 끝에 불필요하게 세미콜론(;)을 붙일 필요가 있을까?
Swift는 라인의 끝을 알리는 세미콜론이 없다. 대신, 여러줄 코드를 한줄에 쓸 때에만 붙인다. 이로 인해 습관적으로 라인을 끝마치고 새로 시작할 때 새끼손가락으로 세미콜론과 엔터를 연속으로 누를 필요가 없다.
-> 변수 및 상수를 구분하여 에러의 가능성을 줄이자.
Swift의 상수 선언은 간단하다. C/C++에서 처럼 const를 붙이지 않아도 된다.
1 2 3 4 5 |
let sangSu = 18 //or let sangSu: Int = 18 |
변수 선언은 일반적 다른 언어와 비슷하다.
1 2 3 |
var legalAge = 18 |
-> Collection의 일종인 Array와 Dictionary 는 선언이 꽤 복잡한 경우가 많다. 좀더 간단히 가능하지 않나?
먼저 String Type의 Array를 선언하기 위해 C# 에서 보면
1 2 3 |
var arr = new string[] { "One", "Two" }; |
이걸 Swift에서는 간단하게 아래처럼 표현 가능하다.
1 2 3 4 5 |
var arr = ["One", "Two"] //Integer type var arr = [1, 2]; |
C#에서 Dictionary는 아래와 같이 선언이 아래와 같이 길다.
1 2 3 4 5 6 7 8 9 10 11 |
Dictionary<string, int> dictionary = new Dictionary<string, int>(); Dictionary<string, int> d = new Dictionary<string, int>() { {"cat", 2}, {"dog", 1}, {"llama", 0}, {"iguana", -1} }; |
이걸 Swift에서는 아래 처럼 간단하게 할 수 있다.
1 2 3 4 5 6 7 8 |
var emptyDict: [String: String] = [:] var responseMessages = [200: "OK", 403: "Access forbidden", 404: "File not found", 500: "Internal server error"] var dict = Dictionary<String, String>() |
-> enum을 단순 상수가 아닌, 어떤 연관된 값을 저장할 수 없을까? (associated value)
enum을 김씨, 이씨, 박씨, 강씨, 홍씨 등으로 정해 놓고 그에 따른 type만을 구분하는 것이 전통적인(?) 언어의 방식이었다면, Swift는 추가로 enum 형 변수가 Associated value를 가질 수 있다. 즉, Custom 값으로 그 사람의 이름을 같이 저장할 수 있다면 enum값 만으로도 이름을 간단히 분류 가능하고, 분류된 이름으로 별도의 변수 없이 enum에서 연관값(Associated value)를 읽어서 처리하게 할 수 있다.
1 2 3 4 5 6 |
enum Barcode { case upc(Int, Int, Int, Int) case qrCode(String) } |
바코드를 위와 같이 정의하고,
1 2 3 |
var productBarcode = Barcode.upc(8, 85909, 51226, 3) productBarcode = .qrCode("ABCDEFGHIJKLMNOP") |
위와 같이 enum 변수를 설정 가능하다.
이게 어떻게 추가로 이용될 수 있냐 하면, switch-case 구문에서
1 2 3 4 5 6 7 8 |
switch productBarcode { case .upc(let numberSystem, let manufacturer, let product, let check): print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).") case .qrCode(let productCode): print("QR code: \(productCode).") } |
위처럼 type 별로 구분하고, 저정된 연관값을 바로 접근할 수 있다.
-> switch 에서 조건을 줄 수는 없을까?
C/C++, C#등 전통적인 언어들에서는 switch를 단순 상수를 구분하는데만 쓰였다. 그런데, 간혹 너무 switch가 길어지고 나열하는 갯수가 너무 많아질 때가 많았다. 이걸, 간단하게 조건을 줄 수 있도록 하면 훨씬 편리하고 간결하게 될 것이라고 생각한 것이다.
Swift에서 가능하다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
let approximateCount = 62 let countedThings = "moons orbiting Saturn" let naturalCount: String switch approximateCount { case 0: naturalCount = "no" case 1..<5: naturalCount = "a few" case 5..<12: naturalCount = "several" case 12..<100: naturalCount = "dozens of" case 100..<1000: naturalCount = "hundreds of" default: naturalCount = "many" } print("There are \(naturalCount) \(countedThings).") // Prints "There are dozens of moons orbiting Saturn." |
조건을 아래처럼 where 절을 사용해서 줄수도 있고, tuple이라고 타입이 다른 변수들을 간단하게 묶어서 하나의 변수처럼 접근 가능하게 하는 타입인데, 이걸 swtch-case의 구분 변수로 사용할 수도 있다.
1 2 3 4 5 6 7 8 9 10 11 |
let yetAnotherPoint = (1, -1) switch yetAnotherPoint { case let (x, y) where x == y: print("(\(x), \(y)) is on the line x == y") case let (x, y) where x == -y: print("(\(x), \(y)) is on the line x == -y") case let (x, y): print("(\(x), \(y)) is just some arbitrary point") } |
-> ?? 연산자 (Nil Coalesing Operator)
“닐 코얼레싱 연산자”, 이걸 한국어로 뭐라 그러는지 모르겠는데, 동작은 아래 구문을 보면 간단히 이해된다.
C에서의 Ternary Operator인 ?를 쓰는 방식으로 Swift에도 이렇게 할 수 있는데,
1 2 3 4 |
var anOptionalInt: Int? = 10 var anotherOptional = (anOptionalInt != nil ? anOptionalInt! : 0) |
이것도 사실 Swift 언어 팀에게는 Verbose해 보였다. 좀더 간단히 아래와 같이 표현 가능하다.
1 2 3 4 |
var anOptionalInt: Int? = 10 var anotherOptional = anOptionalInt ?? 0 |
1 2 3 4 |
let name: String? = nil print("Hello, \(name ?? "Anonymous")!") |
위와 같은 표현도 가능하다.
-> Switch-case나 for, while 반복 문에서 가장 안쪽 블럭에서 가장 바깥쪽에 있는 블럭을 break 할 수 없을까?
원래 이런걸 가능하게 하려면 변수를 하나 또 따로 둬서 체크해야 한다. 그런데 Swift는 라벨을 제공해서 바로 break 하게 할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
gameLoop: while square != finalSquare { diceRoll += 1 if diceRoll == 7 { diceRoll = 1 } switch square + diceRoll { case finalSquare: // diceRoll will move us to the final square, so the game is over break gameLoop case let newSquare where newSquare > finalSquare: // diceRoll will move us beyond the final square, so roll again continue gameLoop default: // this is a valid move, so find out its effect square += diceRoll square += board[square] } } print("Game over!") |
– 변수의 변경 시점을 알 수 없을까? 변경 되기 전에, 혹은 변경 된 후에 바로 무엇을 해야할 때가 많은데 polling으로 항상 체크하기에는 너무 부하가 많다.
Swift에선 가능하다. Property를 이용하면 아래와 같이 해당 변수가 변경 되기 전, 변경 된 후에 불리워지는 코드를 작성할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 |
var totalSteps: Int = 0 { willSet(newTotalSteps) { print("About to set totalSteps to \(newTotalSteps)") } didSet { if totalSteps > oldValue { print("Added \(totalSteps - oldValue) steps") } } } |
– 때로는 클래스 시작 시점에 값을 모르거나, 혹은 변수에 저장하는 값이 매우 큰 메모리를 먹고 있어서, 해당 변수가 사용되는 시점에 로딩을 하고 싶은 경우가 있다. 프로그램이 실행 될 때 그 변수값이 사용될 때도 있고 아닐 때도 있는데 시작 시점에 무조건 로딩하는 것은 자원 낭비일 때가 많기 때문이다. 이걸 프로그래밍 할 수 있으나 좀 편하게 해줄 수 없나?
이건 다른 언어에도 비슷하게 흉내낼 수 있다. 가령 Objective-C에서는 이렇게 접근 시점에 null 체크를 해서 없으면 생성후 리턴하게 한다.
1 2 3 4 5 6 7 8 9 10 |
@property (nonatomic, strong) NSMutableArray *characters; - (NSMutableArray *)characters { if (!_characters) { _characters = [[NSMutableArray alloc] init]; } return _characters; } |
Swift에선 아예 언어단에서 지원한다. lazy property 기능을 이용하면 된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class DataImporter { /* DataImporter is a class to import data from an external file. The class is assumed to take a non-trivial amount of time to initialize. */ var filename = "data.txt" // the DataImporter class would provide data importing functionality here } class DataManager { lazy var importer = DataImporter() var data = [String]() // the DataManager class would provide data management functionality here } let manager = DataManager() manager.data.append("Some data") manager.data.append("Some more data") // the DataImporter instance for the importer property has not yet been created print(manager.importer.filename) |
위의 코드에서 DataManager의 Importer 변수는 manager.importer.file이 호출되기 전까지는 DataImporter 클래스 생성자가 호출되지 않는다. 이런 lazy 변수를 통해서 초기 실행 속도와 메모리 가용량을 획기적으로 줄일 수 있다.
-> 함수의 리턴을 여러개 받을 수 없을까?
전통적으로 함수의 리턴은 1개 밖에 받을 수 없었다. 그래서 C/C++에서는 Reference로 넘겨서 셋팅해서 받고, C#에서도 in/out 을 써야만 했다. Swift는 그냥 tuple을 사용하면 여러개의 다른 Type 변수를 쉽게 return 받을 수 있다. 아래처럼 함수 return을 tuple로 하고,
1 2 3 |
func getTime() -> (Int, Int, Int) { ... return ( hour, minute, second) } |
호출하는 쪽에서는 이렇게 사용 가능하다.
1 2 3 |
let (hour, minute, second) = getTime() |
Good~! 🙂
– 함수는 당연히 First class object로 만들어야 하겠지.
당연히 Swift에서는 함수가 First Class Object(1등 객체)로 대세를 따른다. 변수에 저장 가능하고, 파라미터로 전달 가능하다.
1 2 3 4 |
var mathFunction: (Int, Int) -> Int = addTwoInts //변수 저장 println("10+20 = \(mathFunction(10,20))") |
매개 변수 전달 역시 가능하다.
1 2 3 4 5 6 7 8 9 10 |
//f 변수는 (int, int)를 받고 int를 리턴하는 함수를 매개 변수로 전달 받음. func reduceIntArrayByIteration(a0:Int, arr: Int[], f: (Int,Int)->Int) -> Int { var result = a0 for x in arr { result = f(result, x) } return result } |
– 프로그래밍 코드를 읽다보면, 변수를 파라미터로 넘기는 경우가 많다. 이 경우 그 해당 변수가 그 함수 안에서 변경이 되었는지 안되었는지를 몰라서, 함수 안으로 들어가서 읽어야 하는 불편함이 있다. 이거 없앨 수 없나?
Swift에서는 변수를 파라미터로 일단 넘겨주면 기본적으로 해당 변수의 변경은 불가능하다. Reference Type으로 넘겨 주지 않는 이상 내부 값의 변경은 불가능하게 만들었다. 그래서, 변경 될 가능성이 있는 변수에 대해서는 & 기호를 붙이고, Inout을 명시해서 코드를 읽어 내려갈 때 함수 호출부만 보면 변경 여부를 쉽게 알 수 있게 했다.
1 2 3 4 5 6 7 8 9 10 11 |
func swap(inout x:Double, inout y:Doube) { let t = x x = y y = t } var w1 = 10, w2 = 20 println("w1=\(w1) w2=\(w2)") swap(&w1,&w2) println("w1=\(w1) w2=\(w2)") |
– anonymous function도 지원하며, 추가로 Closure도 지원하자.
Swift는 Closure를 지원한다. 환경을 기억하는 Code Block, Javascript를 써본 사람들은 Closure가 그리 낯설진 않을 것이다. 또한 Swift의 Closure Syntax는 사기에 가깝게 간단하다. (너무 간단해서 오히려 Readability를 떨어뜨리지 않는가 싶다.)
아래와 같이 sort 함수 파라미터인 backward(_:_:) 함수를 inline closure로 때려 넣을 수 있다.
1 2 3 4 5 |
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 }) |
위 Closure 표현에서 컴파일러가 Type을 추론할 수 있기 때문에 심지어 더 간단하게 쓸 수 있다.
1 2 3 |
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } ) |
또한, Closure가 한줄로 표현되어 있을 때 내부적으로 return도 생략 가능하다.
1 2 3 |
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } ) |
심지어 Swift는 inline closure에 대해서 shorthand argument name을 제공한다. 그래서 더 줄이면
1 2 3 |
reversedNames = names.sorted(by: { $0 > $1 } ) |
여기서 또한번 더, 문자열에 특화된 > Operator에 의해서 더 줄일 수도 있다. (아, 근데 이건 너무하다. -_-;)
1 2 3 |
reversedNames = names.sorted(by: >) |
Closure가 환경을 기억하는 Context Block이므로 변수들을 Capturing 한다. 이를 이용해서 조금 차원이 다른 구현들을 할 수 있다. 정의 참고는 여기
-> Delegate의 구현은 Protocol을 이용해서 구현하자.
C#처럼 Delegate 변수를 따로 선언해서 Assign하는 방식이 아닌, 프로토콜을 구현하여 객체를 지정하면 자동 호출이 되는 방식이다.
– C/C++등 옛날 언어들은 메모리 관리가 너무 머리 아프다. Objective-C에서도 retain, release, retainCount, autorelease, dealloc등 메모리 관리에 신경써야 할 코드가 꽤 많다. 메모리 관리에 대해서 신경 덜쓰고 앱에 더 집중하여 코딩할 수 있으면 좋겠다.
Swift는 ARC(Automatic referernce counting) 방식의 자동 GC를 지원한다. Retain Cycle만 잘 피하면, 그 외 메모리에 대한 고민을 많이 없애 주었다.
Swift가 선택한 ARC 방식은 기존 Java, C# 등에서 사용하는 GC 방식인 Cycle Collector와는 다르다. Reference Counter가 0이 되는 순간 즉시 해제 된다. 이를 Deterministic destruction이라 부르고, 항상 예측 가능한 메모리 해제 방식이다.
장단이 있지만, 주기적으로 메모리 Reference를 확인하는 Cycle Collector GC에 비해서 Lag 문제가 없고, 메모리 사용이 효율적이다. 이를 통해 메모리 관리에 관해서는 크게 신경쓰지 않고 편하게 프로그래밍 할 수 있다. 관련 이슈 Link
이상, 옛날 언어들과 다른 Swift의 특징들을 짚어 보았다. 물론 장단점이 있다. Coroutine을 지원하지 않고, Red나, Juniper처럼 요즘 대세인 Reactive Programming이 가능한 문법도 Native하게 지원하지는 않는다. 그럼에도 불구하고 Swift는 나름 잘 고민하며 만들었고, 다른 언어들의 장점을 잘 조합한 듯하다. 기존에 Objective-C를 읽을 때와는 다르게 Swift는 왔다갔다하지 않고 조금은 덜 어렵게 읽힌다. Obj-C를 버리고 과감하게 도전하여 좋은 언어를 만들어 냈다는데 애플에게 박수를 주고 싶다. Swift를 통해서 현재 프로그래밍 언어 설계의 방향과 대세가 어떠한지 조금은 감을 잡을 수 있으리라 생각된다.