도입부

이 글을 포스팅하고 있는 현재 기준으로 8년 전, WWDC 2014에서 애플은 스위프트(Swift)라는 새로운 언어를 발표했습니다.
Swift가 출시된지 8년이 지난 지금, Swift는 iOS 개발자들의 필수 역량이 되었습니다. 모든 iOS 개발자 채용공고에서 Swift 역량을 묻고 있고, 1984년에 출시한 기존 애플 플랫폼에서 사용하던 언어인 Objective-C의 점유율을 넘어섰습니다. 그만큼 애플 플랫폼 내에서 Swift의 영향력은 대단합니다.
이번 포스팅은 Swift가 뭔지, 어떤 점이 이런 커다란 영향력을 만들 수 있었는지 말하고자 합니다.


Swift란?

Swift는 기존의 iOS, macOS 응용프로그램 개발에 쓰이던 언어인 Objective-C와 공존하면서도 좀 더
안전하고(Safe), 빠르고(Fast), 표현력있는(Expressive) 현대적인 언어라고 소개하고 있습니다.

애플 공식문서에서 Swift의 목표를 아래와 같이 설명했습니다.

The goal of the Swift project is to create the best available language for uses ranging from systems programming, to mobile and desktop apps, scaling up to cloud services.


스위프트 프로젝트의 목표는 시스템 프로그래밍, 모바일 및 데스크탑 앱, 클라우드 서비스 확장에 이르기까지 사용 가능한 최상의 언어를 만드는 것입니다.

공식문서에서 소개한 대로 현재 Swift는 iOS, iPadOS, macOS, tvOS, watchOS 등 모든 애플 운영체제 내 개발 분야에서 사용되고 있습니다.

Swift의 특징

Swift is designed to make writing and maintaining correct programs easier for the developer.


Swift는 개발자가 올바른 프로그램을 더 쉽게 작성하고 유지 관리할 수 있도록 설계되었습니다.

애플 공식문서에서는 Swift에 대해서 위와 같이 말하고 있고, 이를 만족하고 있는 Swift의 언어적 특성을 소개했습니다.

Safe(안전성)

Safe. The most obvious way to write code should also behave in a safe manner. Undefined behavior is the enemy of safety, and developer mistakes should be caught before software is in production. Opting for safety sometimes means Swift will feel strict, but we believe that clarity saves time in the long run.


코드를 작성할 때 안전한 방식으로 작동해야 합니다. 정의되지 않은 행동은 안전의 적이고, 소프트웨어가 생산되기 전에 개발자의 실수를 잡아내야 합니다. 안전을 선택한다는 것은 때때로 Swift가 엄격하게 느껴질 것이라는 것을 의미하지만, 우리는 명확성이 장기적으로 시간을 절약한다고 믿습니다.

요약하자면, Swift는 코드를 작성할 때, 엄격하다고 느낄 정도로 안전을 중요시한다고 합니다.
개발자에게 안전은 물론 중요하지만 도대체 어떤 방식으로 코딩에 안전성을 부여할까요?

변수는 항상 사용 전에 초기화된다.(Definitive Initialization)

Swift에서는 변수를 선언하면 비어있는 메모리를 찾아 확보합니다. 그리고 이 변수에 값이 있다는 것을 개발자가 확신할 수 있는 경우, 이 변수를 "안전하다"고 말합니다. 만약 변수의 값을 초기화해주지 않아 메모리가 비어있다면, 변수에 접근할 때 예상치 못한 오류가 발생할 수 있습니다. Swift는 이 문제를 해결하기 위해 변수가 사용되기 전에 초기화되지 않았다면 컴파일 에러를 발생시킵니다. 그리고 변수를 초기화해주는 2가지 방법을 제공하고 있습니다.

이니셜라이저(initializer)를 사용하여 초기화

class Flower {
	let name: String
}

위 예시에서 Flower 클래스의 name 프로퍼티가 초기화되어있지 않아서 컴파일 에러가 발생했습니다. 이 때, init 키워드를 사용하여 이니셜라이저를 작성함으로써 프로퍼티를 초기화할 수 있습니다.

class Flower {
	let name: String
    
    init(name: String) {
    	self.name = name
    }
}

기본 값을 지정하여 초기화

또 다른 변수 초기화 방법으로는 변수 선언과 동시에 초기값을 지정해주는 것입니다.

class Flower {
	let name: String = "Daisy"
}

배열 인덱스의 범위를 벗어난 오류를 확인한다.

배열의 크기를 벗어나는 인덱스에 값을 넣을 경우, 배열이 할당된 메모리를 초과하여 사용했기 때문에 오류가 발생합니다. 그래서 Swift는 배열의 크기를 벗어나는 인덱스에 값을 넣으면 컴파일 에러가 발생합니다.

var flowerArray = ["rose", "aster"]
flowerArray[10] = "daisy"

flowerArray 배열의 크기는 2인데, 인덱스 [11]에 "daisy"를 넣으려고 하면 Index out of range 라는 컴파일 에러가 발생하는걸 볼 수 있습니다.

Int의 오버플로우를 확인합니다.

Int 타입은 값을 할당할 수 있는 범위가 존재합니다.

Int8의 범위:     -128 ~ 127	
Int16의 범위:    -32768 ~ 32767	
Int32의 범위:	-2147483648 ~ 2147483647
Int64의 범위:	-9223372036854775808 ~ 9223372036854775807
UInt8의 범위:	0 ~ 255
UInt16의 범위:	0 ~ 65535
UInt32의 범위:	0 ~ 4264967295
UInt64의 범위:	0 ~ 18446744073709551615

만약 타입의 범위를 초과하는 연산을 할 경우, 오버플로우가 발생하게 됩니다. 이는 보안상의 취약점이 되어 문제가 될 수도 있습니다.

오버플로우(Overflow)란?

타입이 허용하는 범위를 초과할 때 발생하는 오류입니다. 허용범위의 최댓값을 초과할 경우 최솟값으로 돌아가고, 최솟값의 범위를 넘어설 땐 최댓값으로 돌아가게 됩니다.

// Int8의 허용범위: -128 ~ 127
var someInt8: Int8 = 127
someInt8 &+= 1
print(someInt8) // -128

그래서 Swift에서는 오버플로우가 일어날 경우, 오류 문구를 띄워 이를 알립니다.

옵셔널을 통해 nil 값을 다룬다.

옵셔널(Optional)을 간단하게 설명하자면, 오류대신 "값이 없음"을 의미하는 nil 값을 가질 수 있는 타입입니다.

var someOptional: String? = ""
var anotherOptional: String? = nil

문법적으로는 위와 같이 타입 뒤에 ?를 붙이면 옵셔널 타입으로 정의할 수 있습니다.
someOptional 프로퍼티의 값인 ""anotherOptional 프로퍼티의 값인 nil은 다릅니다.
someOptional 프로퍼티는 값이 존재하지만 빈 값일 뿐인 것이고, anotherOptional 프로퍼티는 값이 존재하지 않는 상태입니다.

이 옵셔널 타입을 이용해서 값이 있을수도, 없을수도 있는 프로퍼티를 정의하고, 이 프로퍼티가 값이 없어 오류가 날 때, 오류 대신 nil을 반환하면서 예상치 못한 충돌을 줄여줍니다.

메모리는 자동으로 관리된다.

class같은 참조타입의 인스턴스는 참조를 통해 여러 곳에서 접근하기 때문에 메모리에서 해제되는 시점이 중요합니다. 만약 인스턴스가 필요없다고 생각해서 메모리에서 해제시켜버렸는데, 다시 그 인스턴스를 사용하게 된다면 프로그램은 오류를 일으킬 것입니다.
그래서 Swift는 ARC(Automatic Reference Counting)라는 메모리 관리기법을 이용해서 메모리를 관리합니다. ARC는 사용중인 인스턴스를 메모리에서 해제하지 않기 위해 인스턴스에 대한 참조를 추적합니다.
간단한 예시를 들어 설명하면,

class Flower {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

let flower1: Flower?
let flower2: Flower?
let flower3: Flower?

Flower 타입의 프로퍼티 3개를 선언했습니다.

flower1 = Flower(name: "Rose") // reference count: 1
// Rose is being initialized

flower2 = flower1 // reference count: 2
flower3 = flower1 // reference count: 3

그 중 flower1Flower 인스턴스를 참조시키고, 이 때부터 ARC는 참조 횟수를 카운팅합니다. 나머지 두 개의 프로퍼티에 flower1을 참조시키면 총 3개의 프로퍼티가 1개의 인스턴스를 참조하게 됩니다. 이 때, Flower 인스턴스에 대한 참조 횟수는 총 3이 됩니다.
이 상황에서 Flower 인스턴스를 메모리에서 해제해버리면, 오류가 생길 수 있기 때문에 ARC는 인스턴스의 참조 횟수가 0이 되기 전에 메모리에서 인스턴스를 해제하지 않습니다.

flower1 = nil reference count: 2
flower2 = nil reference count: 1
flower3 = nil reference count: 0
// Rose is being deinitialized

※위처럼 참조횟수를 계산해주는 행위는 참조 타입인 class의 인스턴스에만 적용됩니다.
즉, struct나 enum은 값 타입이기 때문에 ARC의 관리를 받지 않습니다.

오류 처리(Error handling)를 통해 예상치못한 오류를 제어할 수 있다.

프로그램에는 다양한 이유로 예상치못한 오류가 일어날 수 있습니다. 안전을 강조하는 Swift에서는 오류로 인해 생기는 불안정함을 최소화하기 위해, 오류를 Error 프로토콜을 준수하는 타입의 값으로 표현하여 처리할 수 있습니다.
또, Swift의 열거형은 오류를 모델링하는데 적합하여 Error 프로토콜을 채택하는 타입으로 자주 사용됩니다.

적용 예시

enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
} 

자판기 작동 중 생길 수 있는 오류들을 정의했습니다. 이 오류들은 throw라는 구문을 통해 발생할 수 있습니다.

Throw란?

throw는 Error프로토콜을 준수하는 여러가지 오류들을 발생시킬 수 있는 명령문입니다.

throw VendingMachineError.insufficientFunds(coinsNeeded: 5)

위처럼 오류 앞에 throw키워드를 붙이면 오류를 발생시킬 수 있습니다.

또, 오류를 발생시킬 여지가 있는 함수, 메서드, 이니셜라이저에는 함수 매개변수 선언 뒤에 throws키워드를 붙여줘야 합니다.

func canThrowErrors() throws -> String

func cannotThrowErrors() -> String

물론 throws를 붙이지 않아도 오류를 발생시킬 수 있지만, 반드시 오류가 발생한 함수 내에서 오류가 해결되어야 합니다.

이렇게 오류가 던져지면, 코드 내에서 던져진 오류를 처리해줘야 합니다. Swift에서는 이를 위한 4가지 방법이 내장되어 있습니다.

do-catch 구문을 이용한 오류 처리

do-catch 구문은 do 내에서 발생하는 오류를 해당하는 catch 내에서 처리합니다. 일반적으로 아래와 같은 형식을 가지고 있습니다.

do {
    try expression
    statements
} catch pattern 1 {
    statements
} catch pattern 2 where condition {
    statements
} catch pattern 3, pattern 4 where condition {
    statements
} catch {
    statements
}

적용 예시

위 예시에서 정의한 VendingMachineError 타입으로 예를 들면,

enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
} 

do {
    try someThrowingFunction()
    print("Success!")
} catch invalidSelection {
    print("Invalid Selection.")
} catch insufficientFunds(coinsNeeded: Int) {
     print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
} catch outOfStock {
    print("Out of Stock.")
} catch {
    print("Unexpected error: \(error).")
}

someThrowingFunction에서 VendingMachineError.invalidSelection 오류를 던진다면, invalidSelection 패턴의 catch 코드 블럭이 동작하여 Invalid Selection.을 출력할 것입니다.
만약 someThrowingFunction이 던진 오류가 아무 패턴에도 해당하지 않다면, 패턴이 없는 catch 코드 블럭이 동작합니다. 위 예시에서는 Unexpected error: ...이 출력되겠네요.

오류를 옵셔널 값으로 변환

오류를 던지는 함수 앞에 try? 키워드를 붙여서 오류를 옵셔널 값으로 변환할 수 있습니다. 주로 모든 오류를 동일한 방식으로 처리하거나 간결하게 처리하고 싶을 때 많이 사용합니다.
이 방법은 오류 여부에 따라 두 가지 결과를 이끌어 낼 수 있습니다.

  1. 함수가 오류를 던진다면, nil 값을 반환
  2. 함수가 오류를 던지지않는다면, 이 함수의 리턴값이 옵셔널 값으로 변환되어 반환

코드로 나타내면 아래와 같습니다.

func someThrowingFunction() throws -> Int {
    // ...
}

let x = try? someThrowingFunction()

let y: Int?
do {
    y = try someThrowingFunction()
} catch {
    y = nil
}

오류의 전파 차단

어떤 함수에서 오류가 발생하지 않을 것이라고 확신할 때, try! 키워드를 사용하면 함수가 오류를 전파하지도, 오류를 발생하지도 않게 됩니다.

let x = try! someThrowingFunction()

하지만 실제로 함수에서 오류가 발생한다면 런타임 에러가 발생하기 때문에, 정말 오류가 발생하지 않는다는 확신이 들 경우에만 사용하는걸 권장하고 있습니다.

정리 작업 지정

함수 내에서 오류를 던지거나, return 혹은 break 문에 걸려서 부득이하게 함수 코드 블럭을 종료하게 될 경우가 종종 있습니다. 이 때 정리해야할 변수나 상수들을 손보지 못한 채 코드가 진행되버려 메모리 누수나 데드락 같은 현상이 발생할 수 있는데, defer 구문을 사용하면 이를 해결할 수 있습니다.

defer 구문 안에 있는 코드들은 함수가 종료될 때 실행됩니다.

func secretOpen(_ secretName: String) {
	unlock(secretName)
	defer {
		lock(secretName)
	}
    print("Unlock secret \(secretName)")
    
    // defer 블럭 내에 있는 lock(secretName)이 이 때 실행
}

Fast(신속성)

Fast. Swift is intended as a replacement for C-based languages (C, C++, and Objective-C). Performance must also be predictable and consistent, not just fast in short bursts that require clean-up later.


Swift는 C 기반의 언어들을 대체하기 위해 만들어졌습니다. 스위프트가 보여주는 퍼포먼스는 단순히 빠르고 강렬하기보다는 예측 가능하고 일관성이 있을 것입니다.

공식적으로 검색 알고리즘 완성 속도가 Objective-C보다 최대 2.6배, Python 2.7보다 최대 8.4배 빠르다고 합니다.

Expressive(표현성)

Expressive. Swift benefits from decades of advancement in computer science to offer syntax that is a joy to use, with modern features developers expect.


Swift는 수십년간의 컴퓨터 과학의 발전덕에 개발자들이 기대하는 현대적인 기능들과 즐거움을 느낄 수 있는 문법을 제공하게 되었습니다.

Swift는 이렇게 Safe, Fast, Expressive 세 가지의 언어적 특성을 강조하고 있습니다.
이 외에도

  • 함수 포인터로 통합된 클로저
  • 튜플 및 여러 반환 값
  • 제네릭
  • 범위 또는 컬렉션에 대한 빠르고 간결한 반복
  • 메서드, 확장 및 프로토콜을 지원하는 구조체
  • 함수형 프로그래밍 패턴(예: 맵 및 필터)
  • do, guard, deferrepeat 키워드 를 사용한 고급 제어 흐름

등의 추가 기능들 역시 소개하고 있습니다.

Swift의 프로그래밍 패러다임

Multi Paradigm Language

Swift는 여러 개의 프로그래밍 패러다임을 채택하고 있는 다중 패러다임 프로그래밍 언어입니다.
Swift가 채택하고 있는 패러다임은

으로 세 가지의 패러다임을 채택하는만큼 현대적인 기능과 문법을 제공해서 개발자들에게 효율적인 문제 해결을 가능하게 합니다.

마무리

Swift에 대한 포스팅을 위해 자료조사를 하다보니 Swift가 정말 공들여서 만든 언어라는걸 체감했다. 또 몰랐던 사실도 많이 알게되었고, 의외(?)의 사실들도 알게 되어서 재미있었다.

난 Swift가 Objective-C와 공존하기 위한 언어로 알고 있었다. 하지만 Apple Documentation에서

Swift is intended as a replacement for C-based languages(C, C++, and Objective-C)

라는 구절을 보고 '시간이 지나면 Objective-C가 완전히 대체되겠구나'라는 생각이 들었다.
원래는 SwiftUI, UIKit 같은 프레임워크들 사용이 익숙해지면 Objective-C를 배워볼까 생각하고 있었는데 좀 더 SwiftUI에 집중해야겠다.

참조

오늘의 Swift 상식 (Error Handling)

오늘의 Swift 상식 (Initializer 1편. 초기화, 값 타입의 Initializer)

Swift.org - About Swift
Apple Developer Documentation - Memory Safety: Ensuring Values are Defined Before Use
Swift.org - Initialization

Swift 메모리 관리에 대해 아라보자 - (1) ARC란
Swift.org - Automatic Reference Counting

Apple WWDC 2014 - Swift Introduction

profile
IOS Developer DreamTree

0개의 댓글