Swift
고친 사람 github-actions[bot]
Swift 는 iPhone 과 Mac 에서 도는 앱을 짜는 프로그래밍 언어입니다. Apple 이 자기 기기의 앱을 짜라고 만들었습니다. 코드는 미리 기계어로 컴파일되어 가상 머신 없이 돕니다. 비어 있을 수 있는 값을 타입에 표시하게 해서 빈 값을 건드리는 실수를 컴파일할 때 걸러 줍니다.
쉽고 빠른 이해
Swift 는 Apple 기기의 앱을 짜는 언어입니다. 서버의 Java 코드와 달리 가상 머신 없이 기계어로 바로 돕니다.
앱은 화면이 잠깐만 끊겨도 티가 납니다. 그래서 메모리 청소 때문에 멈추는 일이 없어야 합니다. 빈 값을 건드려 앱이 꺼지는 일도 줄여야 합니다.
어떻게 도는가:
- 컴파일러가 소스를 휴대폰 프로세서의 기계어로 미리 바꿉니다
- 값이 없을 수 있는 변수는 타입 뒤에 물음표를 붙입니다. 그 변수는 확인한 뒤에만 씁니다
- 객체를 가리키는 수를 세다가 0 이 되는 순간 바로 지웁니다
대가도 있습니다. 서로를 가리키는 두 객체는 저절로 안 지워져서 짜는 사람이 고리를 끊어야 합니다. 실행 파일은 운영체제와 프로세서마다 따로 빌드합니다.
여러 운영체제에서 실행 파일 하나를 그대로 돌려야 한다면 Swift 로 얻는 것이 적습니다. Java 로 쌓아 둔 서버 코드와 라이브러리가 많을 때도 그렇습니다.
상세
이 절은 Swift 를 백엔드 개발자에게 익숙한 Java 와 견주며 봅니다. 먼저 소스가 실행 파일이 되는 길을 봅니다. 이어서 빈 값과 복사와 메모리, 타입을 묶는 방식을 Swift 가 어떻게 다루는지 짧은 코드로 하나씩 짚습니다. Java 쪽 모습을 옆에 두면 Swift 의 문법이 왜 그렇게 생겼는지가 드러납니다.
소스에서 실행 파일까지
Swift 코드는 .swift 로 끝나는 소스 파일에 적습니다. swiftc 라는 컴파일러가 이 파일을
기계어로 바꿉니다. 기계어는 프로세서가 받아서 곧장 실행하는 명령입니다.
Java 는 한 단계를 더 거칩니다. javac 가 먼저 바이트코드를 담은 클래스 파일을 만듭니다. 그 파일은 JVM(Java Virtual Machine, 자바 가상 머신)이 읽습니다. JVM 은 실행하는 동안 그 파일을 기계어로 옮깁니다. 켠 직후에는 이 옮기는 일 때문에 조금 느립니다.
flowchart TD
subgraph Swift
S["app.swift · Swift 소스"] --> SC["swiftc"]
SC --> SB["기계어 실행 파일"]
end
subgraph Java
J["App.java · Java 소스"] --> JC["javac"]
JC --> JB["클래스 파일"]
JB --> V["JVM"]
end
SB --> P["프로세서"]
V --> P
그림에서 Swift 쪽에는 가상 머신 칸이 없습니다. 이렇게 실행하기 전에 기계어로 다 옮겨 두는 방식을 AOT 컴파일(Ahead-Of-Time compilation, 미리 하는 컴파일)이라고 부릅니다. 켜자마자 제 속도로 돕니다. 가상 머신을 따로 깔 필요도 없습니다.
대신 만든 실행 파일은 그 운영체제와 프로세서에서만 돕니다. Mac 에서 돌릴 파일과 Linux 서버에서 돌릴 파일을 따로 빌드합니다. Java 의 클래스 파일 하나가 JVM 만 있으면 어디서나 도는 것과 갈리는 대목입니다.
빈 값을 담는 옵셔널
Java 에서는 객체를 가리키는 변수라면 무엇이든 null 일 수 있습니다. 그런 변수로 메서드를 부르면 실행 도중에 NullPointerException 이 납니다. 컴파일러는 이 실수를 미리 알려 주지 않습니다.
Swift 는 비어 있을 수 있는 값을 따로 된 타입으로 둡니다. 이것을 옵셔널이라고 부릅니다. 타입 뒤에
물음표를 붙여 String? 처럼 적습니다. 빈 상태는 nil 이라고 씁니다.
var a: String = "hi"
a = nil // 컴파일 오류
var b: String? = "hi"
b = nil // 통과
물음표 없는 String 에는 nil 을 넣을 수 없습니다. 그래서 그 변수를 쓰는 코드는 빈 값을 걱정하지
않아도 됩니다.
옵셔널은 그냥은 못 씁니다. 값이 들었는지 먼저 확인하고 꺼내야 합니다. 흔한 방법은 if let 입니다.
값이 있을 때만 새 변수에 꺼내 담습니다.
let name: String? = "swift"
if let n = name {
print(n.count) // 5
}
딕셔너리에서 값을 찾을 때도 옵셔널이 나옵니다. 딕셔너리는 키로 값을 찾는 자료형입니다.
Swift 에서는 [String: Int] 처럼 키와 값의 타입을 적습니다. 찾는 키가 없을 수 있어서 찾은 결과는 옵셔널로 나옵니다.
let ages = ["kim": 30, "lee": 25]
print(ages["kim"] ?? 0) // 30
print(ages["park"] ?? 0) // 0
?? 는 비었을 때 쓸 값을 정해 두는 연산자입니다. 왼쪽이 nil 이면 오른쪽 값을 씁니다.
확인 없이 꺼내는 길도 열려 있습니다. 변수 뒤에 느낌표를 붙인 name! 은 값을 바로 꺼냅니다. 값이
없으면 프로그램이 거기서 멈춥니다.
복사되는 값 타입
Java 에서 객체를 변수에 넣으면 객체를 가리키는 참조가 들어갑니다. 두 변수가 같은 객체를 가리키게 되어, 한쪽에서 고치면 다른 쪽에서도 바뀐 값이 보입니다. 이런 타입을 참조 타입이라고 부릅니다.
Swift 는 타입을 만들 때 둘 중 하나를 고릅니다. class 로 만들면 참조 타입입니다. struct 로 만들면
값 타입입니다. 값 타입은 다른 변수에 넣을 때 내용이 복사됩니다.
struct Point { var x: Int }
var p1 = Point(x: 1)
var p2 = p1 // 복사된다
p2.x = 9
print(p1.x) // 1
p2 를 고쳐도 p1 은 1 그대로입니다. 넣는 순간 둘이 따로 된 값이 되었기 때문입니다.
Swift 의 문자열과 배열과 딕셔너리도 값 타입입니다. 함수에 배열을 넘기면 받은 쪽이 고친 내용이 넘긴 쪽으로 새어 나오지 않습니다. Java 에서 목록을 넘길 때 방어용으로 복사해 두던 일이 줄어듭니다.
넣을 때마다 내용을 전부 복사하면 느립니다. 그래서 이 타입들은 복사해도 처음에는 내용을 같이 쓰다가, 한쪽이 고치는 순간에 복사합니다. 이 방식을 쓰기 시 복사라고 부릅니다.
메모리를 치우는 ARC
참조 타입의 객체는 다 쓰고 나면 치워야 합니다. Java 에서는 가비지 컬렉션이 이 일을 합니다. 런타임이 틈틈이 살아 있는 객체를 찾습니다. 찾지 못한 나머지는 지웁니다. 치우는 동안 프로그램이 잠깐 서기도 합니다.
Swift 는 ARC(Automatic Reference Counting, 자동 참조 카운팅)를 씁니다. 객체마다 자기를 가리키는 참조가 몇 개인지 세다가, 그 수가 0 이 되면 바로 지웁니다. 수를 늘리고 줄이는 코드는 컴파일러가 빌드할 때 끼워 넣습니다.
class Order {}
var a: Order? = Order() // 참조 1
var b = a // 참조 2
a = nil // 참조 1
b = nil // 참조 0, 해제
객체는 마지막 줄에서 곧바로 사라집니다. 지워지는 때가 코드로 정해지므로, 메모리를 치우려고 앱이 잠깐 서는 일이 없습니다.
대가는 순환 참조입니다. 두 객체가 서로를 가리키면 둘 다 참조 수가 0 으로 내려가지 않습니다. 아무도 안 쓰는데 지워지지도 않는 메모리 누수가 됩니다.
이 고리는 한쪽 참조를 약한 참조로 두어 끊습니다. 약한 참조는 객체를 가리키기만 하고 참조 수를
늘리지 않습니다. Swift 에서는 변수 앞에 weak 를 붙여 표시합니다.
메모리 안전을 지키는 장치
메모리 안전은 프로그램이 제 몫이 아닌 메모리를 건드리지 못하게 막는 성질입니다. 배열 끝을 넘어 옆 데이터를 읽거나, 이미 돌려준 메모리에 다시 쓰는 일이 여기에 걸립니다. Swift 는 이 성질을 언어가 지켜 줍니다.
앞에서 본 두 장치가 그 일부입니다. 옵셔널은 빈 값을 건드리는 코드를 막습니다. ARC 는 아직 누가 쓰는 객체를 먼저 지우지 않습니다.
배열 번호도 확인합니다. 접근할 때마다 번호가 범위 안인지 보는 것을 경계 검사라고 합니다. 범위를 넘는 번호로 읽으면 옆 메모리를 가져오는 대신 프로그램이 멈춥니다.
let nums = [10, 20, 30]
print(nums[1]) // 20
print(nums[5]) // 실행 중 멈춤
변수는 값을 넣기 전에 읽을 수 없습니다. 값을 안 넣은 변수를 읽는 코드는 컴파일러가 거부합니다. 그래서 전에 메모리에 남아 있던 엉뚱한 값이 새어 나오지 않습니다.
이 보호를 끄는 길도 있습니다. 이름이 Unsafe 로 시작하는 포인터 타입을 쓰면 C 처럼 메모리 주소를
직접 다룹니다. 이름에 표시가 남아서 위험한 코드가 어디 있는지 검색으로 찾을 수 있습니다.
Java 인터페이스에 해당하는 프로토콜
Swift 에서 인터페이스에 해당하는 것을 프로토콜이라고 부릅니다. 네트워크에서 말하는 프로토콜과는 다른 뜻입니다. 타입이 갖춰야 할 메서드를 적어 둔 약속입니다.
protocol Shape {
func area() -> Double
}
struct Square: Shape {
var side: Double
func area() -> Double { side * side }
}
print(Square(side: 3).area()) // 9.0
Square 는 struct 인데도 프로토콜을 따릅니다. Java 에서는 클래스만 인터페이스를 구현하지만,
Swift 에서는 값 타입도 같은 약속을 지킵니다.
struct 는 클래스와 달리 다른 타입을 상속받지 못합니다. 그래서 여러 타입을 한 이름으로 묶을 때는
상속 대신 프로토콜을 씁니다. 이렇게 짜는 방식을 프로토콜 지향 프로그래밍이라고 부릅니다.
Swift 로 짜는 애플리케이션
Swift 는 iOS 와 macOS 앱을 짤 때 주로 쓰는 언어입니다. 앱 화면인 UI(User Interface, 사용자 인터페이스)는 Apple 의 SwiftUI 나 UIKit 로 그립니다. 둘 다 Swift 코드에서 부릅니다. 이 둘은 Apple 기기에서만 돕니다.
Swift 전에는 Objective-C 로 짰습니다. 한 앱 안에서 두 언어를 섞어 쓸 수 있어서, 오래된 코드를 버리지 않고 조금씩 옮겨 갑니다.
빌드는 대개 Xcode 에서 합니다. Xcode 는 Apple 이 내놓는 개발 도구 모음입니다. 이 도구는 macOS 에서만 돕니다. 그래서 iOS 앱을 빌드하려면 Mac 이 있어야 합니다.
서버도 짤 수 있습니다. Swift 는 오픈 소스로 공개되어 Linux 에서도 돕니다. Vapor 같은 웹 프레임워크도 있습니다. 서버에서 쓸 라이브러리와 운영 도구는 Java 쪽이 더 오래 쌓아 왔습니다.
고른 것과 내준 것
위의 장치들은 저마다 무언가를 내주고 얻은 것입니다. 한데 모으면 이렇습니다.
| 고른 것 | 얻는 것 | 내준 것 |
|---|---|---|
| 기계어로 미리 컴파일한다 | 가상 머신 없이 켜자마자 제 속도로 돈다 | 운영체제와 프로세서마다 따로 빌드한다 |
| 옵셔널을 타입에 둔다 | 빈 값을 건드리는 실수를 컴파일할 때 잡는다 | 값을 꺼내는 확인 코드가 늘어난다 |
| 컬렉션을 값 타입으로 둔다 | 넘긴 데이터가 받은 쪽에서 몰래 바뀌지 않는다 | 고칠 때 복사가 일어날 수 있다 |
| ARC 로 메모리를 치운다 | 지워지는 때가 정해져 앱이 서지 않는다 | 순환 참조는 짜는 사람이 weak 로 끊는다 |
| 배열 번호를 확인한다 | 범위 밖을 읽으면 옆 메모리 대신 멈춤이 돌아온다 | 접근마다 확인하는 [[시간과 시계 |
그래서 Swift 는 화면이 끊기면 안 되는 Apple 기기의 앱에 맞춰진 언어입니다. 가상 머신과 멈춤을 걷어 내고, 그 대신 메모리 고리를 끊는 일을 짜는 사람에게 남겼습니다.
반대로 여러 운영체제에 같은 실행 파일 하나를 돌리고 싶거나, 이미 JVM 위에 쌓아 둔 서버 코드와 라이브러리가 많으면 Swift 가 주는 것이 적습니다.
관련 항목
Swift 코드를 실행 파일로 만드는 도구
swiftc · LLVM · Xcode · Swift Package Manager · 컴파일러 · AOT 컴파일 · 기계어
Swift 언어를 이루는 문법 요소
옵셔널 · 옵셔널 체이닝 · 값 타입 · 참조 타입 · 구조체 · 열거형 · 클로저 · 제네릭 · 타입 추론 · 프로토콜 지향 프로그래밍 · 익스텐션
Swift 표준 라이브러리가 주는 컬렉션
Swift 표준 라이브러리 · 배열 · 딕셔너리 · 집합 · 문자열
Swift 가 메모리를 다루는 장치
ARC · 참조 카운팅 · 약한 참조 · 순환 참조 · 메모리 누수 · 쓰기 시 복사 · 경계 검사 · 메모리 안전
Swift 가 동시 처리에 쓰는 장치
동시성 · 비동기 프로그래밍 · 액터 · 데이터 경쟁 · 스레드
Swift 로 짓는 애플리케이션과 그 프레임워크
iOS · macOS · SwiftUI · UIKit · Vapor · Linux
Swift 와 섞어 쓰거나 겨루는 프로그래밍 언어
Objective-C · Kotlin · Java · Rust · C++ · Dart
Swift 가 속하는 상위 분류
프로그래밍 언어 · 정적 타입 · 컴파일 언어 · 객체 지향 프로그래밍 · 함수형 프로그래밍 · 오픈 소스
다른 이름: 스위프트 · Swift 언어 · 스위프트 언어