iOS & Swift

[Swift 철학 #1] Swift는 왜 이렇게 깐깐할까? Safe·Fast·Expressive 3대 철학 총정리

Swift로 코드를 쓰다 보면 한 번쯤 궁금해지는 순간이 옵니다. 옵셔널은 왜 이렇게 깐깐할까, 배열은 왜 복사될까, guard는 왜 있을까. 이런 질문들을 하나씩 파고들다 보면 결국 한 지점에서 만나게 됩니다. Swift가 처음부터 내걸었던 세 가지 목표, Safe(안전하게),…

이석우iOS Developer10분 읽기
[Swift 철학 #1] Swift는 왜 이렇게 깐깐할까? Safe·Fast·Expressive 3대 철학 총정리 대표 이미지

Swift로 코드를 쓰다 보면 한 번쯤 궁금해지는 순간이 옵니다. 옵셔널은 왜 이렇게 깐깐할까, 배열은 왜 복사될까, guard는 왜 있을까. 이런 질문들을 하나씩 파고들다 보면 결국 한 지점에서 만나게 됩니다. Swift가 처음부터 내걸었던 세 가지 목표, Safe(안전하게), Fast(빠르게), Expressive(표현력 있게)입니다.

Swift 공식 사이트(swift.org)는 언어 소개 첫머리에서 이 셋을 명시합니다. 그냥 마케팅 문구가 아니에요. 지난 10여 년간 Swift에 추가된 거의 모든 기능이 이 세 가지 잣대로 심사받고 들어왔습니다. 이 글에서는 세 철학이 각각 어떤 언어 기능으로 구현됐는지, 그리고 셋이 충돌할 때 Swift가 어느 쪽 손을 들어줬는지 정리합니다.

이 글은 Swift 철학 시리즈의 첫 편입니다. Swift가 태어난 배경, 그러니까 크리스 래트너가 왜 Objective-C를 버렸는지는 별도 글에서 다뤘으니, 여기서는 “태어난 뒤 어떤 원칙으로 자랐는가”에 집중합니다.

Swift의 모든 설계는 Safe·Fast·Expressive 세 기둥 위에 있습니다
Swift의 모든 설계는 Safe·Fast·Expressive 세 기둥 위에 있습니다

Safe — 컴파일러가 버그를 미리 막는다

Swift 설계에서 가장 우선순위가 높은 가치는 안전입니다. 여기서 안전이란 “실수하기 어렵게 만든다”는 뜻이에요. 프로그래머의 선의나 집중력에 기대지 않고, 언어 차원에서 실수의 통로를 막아버리는 접근입니다.

대표 주자가 옵셔널입니다. Objective-C 시절에는 어떤 포인터든 nil일 수 있었고, nil인 채로 메시지를 보내면 조용히 무시됐습니다. 크래시는 안 나지만 버그가 어디서 시작됐는지 추적하기 어려웠죠. C 계열 언어에서는 아예 크래시가 났고요. 토니 호어가 null 참조를 두고 “10억 달러짜리 실수”라고 부른 건 유명한 이야기입니다.

Swift는 이 문제를 타입 시스템으로 끌어올렸습니다. 값이 없을 수 있는 변수는 String?처럼 타입에 물음표를 박아서 선언해야 하고, 물음표가 없는 타입은 절대 nil이 될 수 없습니다. nil일 수 있는 값을 쓰려면 if let이나 guard let으로 벗겨내는 절차를 컴파일러가 강제해요.

var name: String? = fetchUserName()

// 컴파일 에러 — 옵셔널을 그냥 쓸 수 없다
// print(name.count)

if let name {
    print(name.count) // 여기서는 안전하게 보장됨
}

“nil 체크를 깜빡했다”는 런타임 버그가, “옵셔널을 안 벗겼다”는 컴파일 에러로 바뀐 겁니다. 버그가 사용자 손에 도달하기 전에, 빌드 단계에서 죽는 거죠.

안전 철학은 옵셔널 말고도 곳곳에 있습니다.

  • 변수는 사용 전 초기화 강제: 초기화 안 된 메모리를 읽는 버그가 원천 차단됩니다.
  • 배열 범위 체크: 인덱스가 범위를 벗어나면 이상한 메모리를 읽는 대신 즉시 멈춥니다.
  • 정수 오버플로 감지: C에서는 조용히 값이 뒤집히지만 Swift는 기본 연산에서 트랩을 겁니다.
  • 타입 추론은 있지만 암시적 변환은 없음: IntDouble을 그냥 더할 수 없습니다. 귀찮지만 C의 암시적 형변환이 만든 미묘한 버그들을 생각하면 남는 장사예요.

여기서 눈여겨볼 지점이 하나 있습니다. Swift의 안전은 “크래시가 안 난다”가 아니라 “정의되지 않은 동작이 없다”에 가깝습니다. 배열 범위를 벗어나면 Swift는 오히려 일부러 크래시를 냅니다. 이상한 값으로 계속 실행되는 것보다, 문제 지점에서 확실하게 멈추는 쪽이 더 안전하다는 판단이에요.

런타임 버그를 컴파일 게이트에서 미리 세우는 게 Swift의 안전 철학입니다
런타임 버그를 컴파일 게이트에서 미리 세우는 게 Swift의 안전 철학입니다

Fast — 안전을 위해 성능을 포기하지 않는다

안전한 언어는 많습니다. 문제는 안전 장치가 보통 비싸다는 거예요. 가비지 컬렉션, 런타임 타입 체크, 인터프리터. 안전과 편의를 얻는 대신 속도를 내주는 게 스크립트 언어들의 거래였습니다.

Swift의 야심은 이 거래 자체를 거부하는 데 있습니다. 목표는 명확해요. C 계열 언어에 준하는 성능을 내면서 위의 안전 장치를 전부 유지하는 것. 이를 위한 장치가 몇 겹으로 깔려 있습니다.

첫째, 컴파일 시점에 최대한 결정합니다. Swift는 정적 타입 언어고, 컴파일러가 타입을 다 알기 때문에 메서드 호출을 컴파일 시점에 직접 주소로 박아버릴 수 있습니다(정적 디스패치). Objective-C가 모든 메서드 호출을 런타임에 objc_msgSend로 조회하던 것과 대비되는 지점이에요. 클래스에 final을 붙이면 더 적극적으로 최적화되는 것도 이 원리 때문입니다.

둘째, 가비지 컬렉션 대신 ARC(Automatic Reference Counting, 자동 참조 계수)를 씁니다. 참조 카운팅은 컴파일 시점에 retain/release 코드가 삽입되는 방식이라, GC(가비지 컬렉션)처럼 런타임에 프로그램을 멈추고 메모리를 청소하는 일시 정지가 없습니다. 메모리가 언제 해제될지 예측 가능하다는 것도 장점이고요.

셋째, 값 타입과 프로토콜 중심 설계입니다. struct는 힙 할당과 참조 카운팅 비용 없이 스택에 놓일 수 있고, 제네릭은 특수화(specialization)를 거치면 타입별 전용 코드로 컴파일됩니다. 추상화를 썼다고 런타임 비용을 내는 게 아니라, 컴파일러가 추상화를 벗겨내고 구체 코드를 만들어내는 거죠. 이런 걸 비용 없는 추상화(zero-cost abstraction)라고 부릅니다.

물론 현실은 목표보다 복잡합니다. 프로토콜 타입을 담는 existential container, 클래스의 참조 카운팅 오버헤드처럼 Swift에도 성능 비용이 숨어 있는 구석이 있어요. 그래서 “Swift는 무조건 빠르다”보다는 “빠를 수 있는 길을 언어가 열어놨고, 그 길을 벗어나면 비용을 낸다”가 정확한 이해입니다. 어디가 그 길인지는 심화편(디스패치, 메모리 레이아웃)에서 자세히 다룰 예정입니다.

Expressive — 의도가 코드에 그대로 보이게

셋 중 가장 손에 안 잡히는 가치가 표현력입니다. 풀어 말하면 이렇습니다. 코드를 읽었을 때 작성자의 의도가 그대로 전달되어야 하고, 하고 싶은 말을 장황한 의식 없이 쓸 수 있어야 한다는 것.

Objective-C와 비교하면 체감이 확실합니다.

// Objective-C
NSArray *names = @[@"Kim", @"Lee", @"Park"];
NSMutableArray *upper = [NSMutableArray array];
for (NSString *name in names) {
    [upper addObject:[name uppercaseString]];
}
// Swift
let names = ["Kim", "Lee", "Park"]
let upper = names.map { $0.uppercased() }

줄 수가 줄어든 것도 있지만, 더 중요한 건 의도의 밀도입니다. map 한 단어가 “각 원소를 변환해서 새 배열을 만든다”는 의도를 통째로 전달해요. for 루프 버전에서는 그 의도를 읽는 사람이 재구성해야 하고요.

표현력을 위한 장치들을 몇 개만 꼽아보면 이렇습니다.

  • 타입 추론: let names = ["Kim", "Lee"]라고 쓰면 [String]인 걸 컴파일러가 압니다. 타입 안전은 유지하면서 타입 표기의 소음만 줄인 거예요.
  • enum과 연관값: “성공이면 데이터, 실패면 에러”라는 상태를 case success(Data), case failure(Error)로 그대로 모델링합니다. 상태와 데이터가 따로 놀 수 없게요.
  • 후행 클로저, 서브스크립트, 연산자 정의: 라이브러리가 언어 문법처럼 읽히는 API를 만들 수 있게 합니다.
  • resultBuilder: SwiftUI의 선언형 문법이 이걸로 만들어졌습니다. UI 구조가 코드 구조와 일치하게 되죠.

주의할 점도 있습니다. 표현력은 “짧게”와 동의어가 아니에요. Swift API 디자인 가이드라인의 제1원칙은 “사용 지점에서의 명료함(clarity at the point of use)“이고, 간결함보다 명료함이 우선입니다. remove(at: 3)처럼 인자 레이블을 굳이 붙이는 문법이 존재하는 이유가 이겁니다. remove(3)이 더 짧지만, “3번째 위치를 지우는 건지, 값 3을 지우는 건지” 읽는 사람이 헷갈리니까요.

충돌하면 안전이 이기고, 성능·표현력은 컴파일러가 회수합니다
충돌하면 안전이 이기고, 성능·표현력은 컴파일러가 회수합니다

셋이 충돌하면 누가 이길까

철학이 셋이면 반드시 충돌하는 순간이 옵니다. Swift의 진짜 성격은 이 충돌의 판정 기록에서 드러나요.

안전 vs 표현력: 옵셔널 언래핑은 분명 코드를 시끄럽게 만듭니다. 파이썬처럼 그냥 쓰게 두면 더 짧을 텐데요. Swift는 안전의 손을 들었습니다. 대신 if let 축약 문법, 옵셔널 체이닝(user?.name), nil 병합(??)처럼 안전을 유지하면서 소음을 줄이는 문법을 계속 추가하는 식으로 표현력을 보상했어요.

안전 vs 성능: 배열 범위 체크는 매 접근마다 비교 연산이 붙는다는 뜻입니다. Swift는 기본값으로 안전을 택하되, 컴파일러가 범위 초과가 불가능하다고 증명할 수 있는 경우 체크를 제거하는 최적화로 성능을 회수합니다. 그리고 정말 필요한 사람을 위해 withUnsafeBufferPointer 같은 탈출구를 열어뒀습니다. 이름에 unsafe를 박아서, 위험을 감수했다는 사실이 코드에 남게요.

성능 vs 표현력: 고차 함수와 제네릭 같은 추상화는 표현력의 핵심인데, 순진하게 구현하면 느립니다. Swift는 인라이닝과 제네릭 특수화로 “추상화를 써도 손으로 푼 코드와 같은 기계어가 나오게” 만드는 데 컴파일러 역량을 쏟았습니다.

패턴이 보이시나요? 기본값은 언제나 안전이고, 성능과 표현력은 컴파일러 최적화와 명시적 탈출구로 회수한다. Swift의 거의 모든 설계 결정이 이 공식으로 설명됩니다.

이 철학이 실무에서 갖는 의미

철학 이야기가 뜬구름처럼 느껴질 수 있는데, 실무와 바로 이어지는 지점이 있습니다.

첫째, 컴파일러와 싸우지 말고 편에 서는 게 이득입니다. Swift에서 컴파일 에러는 대부분 “미래의 런타임 버그를 지금 잡았다”는 신호입니다. 옵셔널이 귀찮다고 !를 남발하는 건 언어가 세워둔 방어선을 스스로 허무는 일이에요.

둘째, API를 설계할 때 잣대가 생깁니다. 내가 만든 함수가 잘못 쓰기 쉬운 형태라면 Swift답지 않은 겁니다. 잘못된 사용이 컴파일 에러가 되도록 타입을 설계하는 것, 이게 Swift 스타일의 핵심이고 이후 시리즈에서 반복해서 만날 주제입니다.

셋째, 새 기능이 이해됩니다. async/await는 콜백 지옥이라는 표현력 문제와 데이터 레이스라는 안전 문제를 동시에 겨냥한 기능이고 Swift 6의 strict concurrency는 “동시성 버그도 컴파일 타임에 잡겠다”는 안전 철학의 연장입니다. 매크로는 보일러플레이트라는 표현력 문제를 컴파일 타임에 푸는 장치고요. 세 철학을 알고 있으면 새 기능이 나올 때마다 “이번엔 어떤 가치를 어떻게 밀었구나”가 보입니다.

정리

  • Swift의 모든 설계는 Safe, Fast, Expressive 세 가치의 균형에서 나왔습니다.
  • Safe: 옵셔널, 초기화 강제, 범위 체크 — 실수를 런타임 버그가 아니라 컴파일 에러로 만듭니다.
  • Fast: 정적 디스패치, ARC, 값 타입, 제네릭 특수화 — 안전 장치를 유지하면서 C급 성능을 노립니다.
  • Expressive: 타입 추론, enum, 클로저 — 짧음이 아니라 의도의 명료함이 목표입니다.
  • 충돌 시 기본값은 안전, 성능과 표현력은 컴파일러 최적화와 명시적 탈출구로 회수합니다.

다음 편에서는 이 철학이 학습 곡선에 어떻게 반영됐는지를 다룹니다. print 한 줄짜리 스크립트와 제네릭 라이브러리가 같은 언어에 공존하는 비밀, Progressive Disclosure입니다.

함께 보면 좋은 글