스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Swift 둘러보기: Swift의 기능 및 디자인 살펴보기
Swift 프로그래밍 언어의 기본적인 특징과 이에 담긴 설계 철학에 대해 알아보세요. 라이브러리, HTTP 서버, 명령어 라인 클라이언트를 포함한 Swift 패키지를 빌드하며 데이터 모델링, 오류 처리, 프로토콜 사용, 동시성 코드 작성 등의 작업을 처리하는 방법을 소개합니다. Swift 여정을 처음 시작하는 개발자부터 Swift 사용 경험이 풍부한 개발자까지, 누구나 Swift를 효과적으로 활용하는 데 도움을 줄 세션입니다.
챕터
- 0:00 - Introduction
- 0:51 - Agenda
- 1:05 - The example
- 1:32 - Value types
- 4:26 - Errors and optionals
- 9:47 - Code organization
- 11:58 - Classes
- 14:06 - Protocols
- 18:33 - Concurrency
- 23:13 - Extensibility
- 26:55 - Wrap up
리소스
- Forum: Programming Languages
- The Swift Programming Language
- Tools used: Ubuntu
- Tools used: Visual Studio Code
- Tools used: Windows
- Value and Reference types
- Wrapping C/C++ Library in Swift
관련 비디오
WWDC23
WWDC22
WWDC21
-
다운로드
안녕하세요, 저는 Swift 컴파일러 담당 Allan Shortlidge입니다 오늘의 주제는 제가 가장 좋아하는 프로그래밍 언어 Swift입니다
최신 프로그래밍 언어인 Swift는 다양한 기능이 있으며 성능이 뛰어나고 안전성도 확실합니다 가벼운 구문으로 쉬운 프로그래밍이 가능하고 강력한 기능으로 생산성을 높일 수 있습니다 Swift는 Apple 기기를 위한 앱을 만드는 데 적합한 언어로 유명하지만 용도는 그 외에도 다양합니다 Swift는 크로스 플랫폼 시스템 언어로 서버 애플리케이션 작성에도 적합합니다 또한 작년부터 시작된 Embedded Swift 프로젝트 덕분에 Swift는 스마트 홈 기기 구동을 위한 칩과 같이 리소스 제약이 심한 환경에서도 사용할 수 있도록 확장되었습니다 오늘 살펴볼 내용은 Swift의 핵심 기능입니다 모든 내용을 다루거나 한 주제만 깊이 다루지는 않겠습니다 오늘 목표는 Swift를 개괄적으로 알아보고 Swift의 고유성을 살리는 디자인 원칙을 배우는 것입니다 Swift를 소개하면서 멋진 차세대 소셜 네트워크를 위한 인프라를 구축하는 것으로 그 기능을 시연해 보겠습니다 코드는 세 가지 구성 요소가 있는 Swift 패키지로 구성됩니다 첫 번째는 데이터 모델을 제공하는 라이브러리로 사용자를 그래프로 표현하는 용도입니다 두 번째 구성 요소인 HTTP 서버는 그래프 쿼리에 응답할 수 있습니다 그리고 마지막 요소인 명령어 라인 유틸리티는 서버에 요청을 보낼 수 있습니다 Swift에 대해 알아보기 전에 기본적인 프로그래밍 개념인 데이터 표현에 대해 알아보겠습니다 Swift의 기본적인 데이터 표현 방법은 값 타입을 사용하는 것입니다 코드로 예시를 보여 드리겠습니다 Swift에서는 var 키워드로 변수를 도입할 수 있습니다
두 번째 변수 y를 선언하고 x라는 값을 할당해 보겠습니다 이제 x의 값을 변경하면 어떻게 될까요? 출력에서 y의 값은 여전히 1입니다 놀라운 일은 아닙니다 대부분의 언어에서 정수 타입이 이렇게 작동하니까요 타입에 값 의미가 있다는 게 어떤 의미인지 잘 보여 주는 예시입니다 값 타입에는 중요한 속성이 있습니다 값 타입의 인스턴스는 상태를 공유하지 않으므로 값 하나를 변경해도 같은 타입의 다른 값에 영향을 줄 수 없습니다 또한 그 둘은 고유 성질이 없습니다 두 값이 같으면 두 값을 서로 교환할 수 있다는 뜻입니다 정수, 불리언, 부동 소수점 숫자 등 기본 타입은 대부분의 언어에서 값 타입입니다 하지만 Swift에서는 값 타입이 어디에나 있습니다 코드를 더 쉽게 추론할 수 있는 또 다른 방법은 데이터 가변성을 제어하는 것입니다 저는 var 키워드에 x와 y를 도입하고 가변적으로 만들었습니다 하지만 var 대신 let을 사용하면 Swift는 이 값이 변경되지 않게 보장합니다
그래프에 사용자 모델을 도입해 좀 더 복잡한 데이터 타입을 만들어 보겠습니다 Swift에는 여러 값을 하나로 집계하는 구조체가 있습니다 하나 만들어 User라고 하겠습니다
이 구조체에는 몇 가지 속성이 필요합니다 사용자 이름에 하나 사용자 표시 여부 제어에 하나 마지막으로 친구 목록은 사용자 이름 문자열 배열로 표현했습니다 Alice라는 사용자를 만들고 친구를 몇 명 추가하겠습니다
Alice는 타입으로 선언하지 않았습니다 Swift는 타입 안전성을 갖춘 언어이고 컴파일 시 모든 변수에 타입이 있지만 여기서는 타입을 User라고 유추할 수 있어 타입을 작성할 필요가 없습니다 다음으로 Bruno라는 사용자를 생성하고 앨리스의 친구들을 주겠습니다 이제 Alice에게 다른 친구를 주면 어떻게 될까요?
출력을 보면 Alice와 Bruno가 서로 다른 친구를 가지고 있습니다 Swift에서는 배열이 값 타입이기 때문입니다 Alice의 친구를 Bruno의 친구에게 할당하자 배열이 복사된 것입니다 User 구조체는 값 타입으로 구성되어 있어서 그 자체가 자동으로 값 타입이 됩니다 일반적인 Swift 코드에서 접하는 대부분의 타입은 값 타입입니다 클래스와 같은 참고 타입도 있으며 이는 나중에 다루겠지만 용도가 좀 더 전문적입니다 Swift가 값 타입과 불변성을 강조하는 이유는 값 변경 조건을 제어하면 코드 추론이 훨씬 쉬워지기 때문입니다 특히 동시 프로그래밍과 같은 까다로운 영역에서 말이죠 다음은 오류입니다 프로그래밍에서 오류는 언제든지 발생합니다 꽉 찬 디스크, 네트워크 연결 실패 사용자가 잘못 제공한 데이터처럼요 오류가 발생해도 프로그램은 계속 작동해야 합니다 사용자에게 발생한 문제를 알리기도 해야 하죠 Swift는 오류를 간편하게 보고하고 적절하게 처리할 수 있는 오류 처리 모델을 제공합니다 오류 처리에 대한 Swift의 철학은 세 가지로 요약할 수 있습니다 첫째, 코드에서 오류의 원인이 될 수 있는 부분을 표시하여 사용자가 놀라지 않도록 해야 합니다 둘째, 오류에 조치 가능한 컨텍스트가 충분히 포함되어야 합니다 마지막으로 Swift는 복구 가능한 오류와 프로그래머의 실수를 구분합니다 네트워크 연결이 실패해도 프로그램은 계속 실행되어야 합니다 반면에 범위를 벗어난 배열 액세스는 코드가 잘못되었다는 의미일 수 있습니다 Swift는 해당 버그가 보안 문제로 확대되는 것을 막기 위해 프로그램을 중단합니다
오류 조건을 확인해 사용자 모델을 더 안전하게 만드는 방법을 보겠습니다 지금은 친구 배열을 직접 수정할 수 있기 때문에 사용자가 자신과 친구가 되거나 친구 목록에 중복된 값이 포함되는 등 잘못된 상태가 발생할 수 있습니다 addFriend라는 메서드를 만들어서 배열에 추가된 항목의 유효성을 검사해 보겠습니다
기본적으로 구조체의 메서드는 구조체 속성을 수정할 수 없습니다 이 메서드는 친구를 변경할 수 있도록 mutating 선언을 해야 합니다 이제 친구 속성을 직접 사용하지 못하도록 친구 속성에 비공개 설정자를 부여하고 대신 addFriend를 호출하도록 전환합니다
다음으로, addFriend의 오류를 보고하고 싶습니다 따라서 오류를 표현할 방법이 필요합니다 Swift enum이 적합한 오류 타입입니다
enum은 여러 다양한 선택지를 나타낼 수 있으며 오류의 발생 가능한 모든 원인을 열거할 수 있습니다 enum이 Error 프로토콜을 준수하게 하려고 합니다 프로토콜은 나중에 더 자세히 다루겠습니다 다음으로 addFriend 입력의 유효성을 확인하고 유효하지 않은 경우 오류를 발생시킵니다 컴파일러에 따르면 이 메서드를 이제 throw로 표시해야 하는데 오류의 원인이 되기 때문입니다
if 구문을 사용하여 입력을 확인할 수도 있었지만 Swift의 guard 구문이 오류 조건 감지에 완벽합니다 가드 조건이 충족되지 않으면 함수에서 반환해야 하기 때문이죠 addFriend가 오류를 표시하고 처리되지 않은 오류의 원인이 있음을 나타내는 진단이 있습니다 오류가 발생할 수 있음을 나타내기 위해 호출에 try 키워드를 표시해야 합니다 오류가 어떤지 확인하고 싶으므로 오류를 트리거하고 호출을 do/catch 블록으로 래핑해서 관찰하겠습니다
duplicateFriend 오류가 발생했군요 하지만 이 오류의 한 가지 문제는 어떤 친구가 중복되는지 알려 주지 않는다는 것입니다 이런 경우 연관 값을 지정해 해당 컨텍스트를 추가할 수 있습니다
이제 오류와 함께 사용자 이름이 표시됩니다 사용자 이름으로 사용자를 찾는 쿼리를 프로토타이핑하겠습니다 사용자를 저장할 장소가 필요하므로 사용자 이름을 사용자 구조체에 매핑하는 딕셔너리를 만들었습니다 이 쿼리 함수에는 사용자 검색 실패를 처리하는 방법이 필요합니다 오류가 발생할 수도 있지만 반환 값을 옵셔널로 만드는 것도 한 가지 방법입니다 Swift는 옵셔널 값에 대해 풍부한 기본 지원을 제공합니다 옵셔널 값은 nil이거나 옵셔널이 래핑하는 타입이 무엇이든 유효한 값입니다 옵셔널에 저장된 값을 가져오려면 래핑을 해제해야 합니다 코드에서 nil 값과 nil이 아닌 값을 모두 처리하도록 요구하면 Swift는 다른 언어에서 흔히 발생할 수 있는 실수인 nil 값이 예기치 않게 프로그램을 충돌시키는 것을 방지합니다 코드에서 옵셔널이 어떻게 사용되는지 보겠습니다 findUser 함수에서 이 딕셔너리 조회 결과를 직접 반환할 수 있습니다 오류가 있네요 Dictionary의 구독 연산자가 옵셔널 User를 반환하기 때문인데 이는 키에 해당하는 값이 없을 수 있기 때문입니다 물음표를 추가해 함수 반환 타입을 옵셔널로 업데이트하겠습니다 findUser를 호출해 보겠습니다
함수가 옵셔널을 반환하므로 User로 작업하려면 결과의 래핑을 해제해야 합니다 Swift에서 옵셔널 래핑을 해제하는 일반적인 방법 중 하나는 if let 구문을 사용하는 것입니다 nil이 아닌 값이 있으면 이 if 구문 본문에 있는 let에 바인드됩니다 런타임에 항상 유효 값이 있을 것이라고 100% 확신하는 상황에서는 느낌표를 사용하여 옵셔널의 래핑을 강제로 해제할 수도 있습니다
Swift는 런타임에 제 가정이 맞는지 확인합니다
이런! 딕셔너리에 dash라는 사용자가 없어서 프로그램이 중단되었네요 다음에는 더 조심해야겠습니다 Swift의 오류 처리와 옵셔널 래핑 해제에는 공통점이 있습니다 두 가지 모두 코드가 모든 가능성을 처리할 수 있게 구조화되도록 설계되었죠 프로그램에서 오류가 발생할 수 있는 각 위치에서 오류를 포착하거나 전파해야 하며 throw 및 try 키워드는 오류가 발생하는 위치를 정확히 보여 줍니다 옵셔널 값으로 작업하는 경우에는 사용하기 전에 래핑을 해제하여 해당 값이 존재하는지 확인해야 합니다 오류와 옵셔널 설계를 통해 Swift에서 정확하고 디버깅 가능한 프로그램을 더 쉽게 작성할 수 있습니다 소셜 그래프의 기본 데이터 모델 개요를 설명했으니 이제 코드에 구조를 추가해야 할 때인 것 같습니다 Swift에서는 모듈과 패키지라는 두 가지 코드 구성 단위를 지원합니다 Swift의 모듈은 항상 함께 빌드되는 소스 파일 모음으로 구성됩니다 모듈은 다른 모듈에 종속될 수도 있습니다 예를 들어 앱을 나타내는 모듈은 앱과 서버 모두에 필요한 핵심 기능을 제공하는 라이브러리 모듈에 종속될 수 있습니다 모듈 컬렉션은 패키지로 배포할 수 있습니다 마지막으로, 한 패키지의 모듈은 다른 패키지의 모듈에 대해 종속성을 가질 수도 있습니다 Swift Package Manager는 패키지를 관리하기 위한 도구입니다 명령어 라인에서 호출하여 코드를 빌드, 테스트, 실행할 수 있습니다 Xcode 또는 VS Code에서 Swift 패키지로 작업할 수도 있습니다 HTTP 서버 생성과 같은 특정 작업을 하기 위한 라이브러리를 찾고 있다면 Swift Package Index에서 해당 작업을 위한 오픈 소스 패키지를 찾을 수 있을 것입니다 나중에 오픈 소스 패키지 몇 가지를 사용하여 서버와 명령어 라인 유틸리티를 구축하겠습니다 코드로 돌아가서 지금까지 작성한 내용을 패키지로 재구성해 보았습니다 패키지의 첫 번째 모듈은 라이브러리 모듈로 소셜 그래프 데이터 모델이 포함되어 있습니다 라이브러리와 함께 제공되는 테스트도 있습니다 앞서 정의한 오류 enum과 User 구조체는 이제 각자의 파일에 있습니다 User 구조체를 살펴봅시다 몇 가지가 변한 것을 보실 수 있을 겁니다 이제 이 구조체와 많은 멤버에 public 한정자가 추가되어 라이브러리 외부의 코드에서 사용할 수 있습니다 Public은 Swift에서 사용 가능한 여러 액세스 제어 수준 중 하나입니다 private, internal, package 수준도 있죠 Private로 표시된 선언은 동일한 파일의 코드를 통해서만 액세스할 수 있습니다 internal 선언은 같은 모듈에 있는 다른 코드를 통해서만 액세스할 수 있습니다 액세스 수준을 지정하지 않으면 Swift는 암시적으로 internal을 사용합니다 Package 선언은 같은 패키지의 다른 모듈에서 액세스할 수 있습니다 public 선언은 다른 모든 모듈에서 액세스할 수 있습니다 지금까지는 값 타입에 대해서만 말씀드렸지만 공유 가변 상태를 표현해야 할 때도 있습니다 이를 위해 Swift에는 클래스와 같은 참조 타입이 있습니다 나중에 소셜 그래프 데이터 모델을 저장하는 HTTP 서버를 구축하겠습니다 이 서버는 친구를 추가하거나 나열하는 등의 작업 요청에 응답해야 합니다 요청이 들어오면 이를 처리하는 코드가 사용자 컬렉션에 추상화를 사용하여 액세스했으면 합니다 이 컬렉션은 여러 요청에 의해 공유되고 변경되어야 하므로 참조 타입을 사용하여 캡슐화해야 합니다 더 간단한 예시를 통해 클래스를 더 자세히 살펴보겠습니다 객체 지향 언어로 프로그래밍을 해본 적이 있다면 Swift의 클래스가 익숙해 보이실 것입니다 클래스는 단일 상속을 지원합니다 이 예시에서 Cat 클래스가 슈퍼클래스 Pet에서 상속하는 것처럼요 서브클래스의 메서드는 슈퍼클래스의 메서드를 오버라이드합니다 그리고 서브클래스에서 슈퍼클래스로 또는 그 반대의 타입 변환도 예상한 대로 작동합니다 Swift는 메모리를 자동으로 관리합니다 참조 타입의 경우 Swift에는 Automatic Reference Counting이라는 기능이 있습니다 컴파일러는 코드 뒤에서 객체에 대한 참조가 있기만 하다면 객체가 살아 있는지 확인합니다 이는 참조 수를 늘리거나 줄이는 방식으로 이루어집니다 객체에 대한 참조가 없어지만 객체는 즉시 할당이 해제됩니다 Automatic Reference Counting은 예측이 가능하므로 성능에 큰 도움이 됩니다 하지만 한 가지 문제는 사이클이 형성되어 객체 할당이 해제되지 않게 된다는 것입니다 여기에는 여러 반려동물이 있는 Owner 클래스가 있습니다 Pet 클래스에는 Owner에 대한 참조가 있어 사이클을 생성합니다 이 사이클을 깨려면 약한 참조를 사용해 Owner에 대한 참조 수가 증가하지 않도록 할 수 있습니다 Owner 속성을 약하게 만들면 속성이 옵셔널이 되기도 합니다 Owner 인스턴스가 Pet보다 먼저 할당이 해제되면 이 속성은 nil이 됩니다 앞서 Swift에서는 값 타입이 중요하다고 말씀드렸지만 클래스도 중요한 역할을 합니다 공유 가변 상태 정체성이 있는 객체 또는 상속이 필요할 때는 클래스가 적합합니다 많은 객체 지향 언어에서 상속은 다형성을 위한 주요 메커니즘입니다 하지만 Swift에서는 프로토콜이 추상화를 구축하는 더 일반적인 방법을 제공하며 값 타입과 참조 타입 모두에서 똑같이 잘 작동합니다 프로토콜은 타입에 대한 요구 사항의 추상적인 집합입니다 타입이 프로토콜을 준수한다고 선언하려면 모든 요구 사항의 구현을 제공하면 됩니다 이 예시에서 StringIdentifiable은 단일한 요구 사항을 가진 프로토콜입니다 User 타입이 StringIdentifiable을 준수하는 방법은 확장에 식별자 속성의 구현을 제공하는 것입니다 확장은 제가 가장 좋아하는 Swift 기능 중 하나입니다 확장을 사용하면 모든 타입에 메서드, 속성 프로토콜 준수를 추가할 수 있습니다 타입이 정의된 위치에 상관없이요 Swift 표준 라이브러리의 컬렉션 타입은 프로토콜을 사용하여 추상화할 수 있는 타입 계열의 좋은 예입니다 Array와 Dictionary와 같은 Swift 컬렉션 타입을 이미 보셨지만 이외에도 더 많습니다 또 다른 컬렉션인 Set은 고유한 요소의 정렬되지 않은 목록을 나타냅니다 문자열은 Swift에서 컬렉션이기도 합니다 유니코드 문자 목록이 포함되어 있기 때문이죠 Collection을 준수하는 모든 타입은 몇 가지 기능을 함께 공유합니다 예를 들어, for 루프를 사용하여 Collection을 준수하는 모든 타입의 콘텐츠를 반복할 수 있습니다 인덱스를 사용하여 컬렉션의 요소에 액세스할 수도 있습니다 Swift 표준 라이브러리에는 모든 컬렉션에서 사용 가능한 많은 표준 알고리즘 구현이 포함되어 있습니다 map, filter, reduce와 같은 알고리즘은 함수형 프로그래밍 사용 경험이 있다면 익숙하게 보이실 것입니다 또한 Swift에는 이러한 알고리즘을 더욱 명쾌하게 사용할 수 있는 클로저를 위한 특별한 단축 구문도 있습니다 매개변수의 이름을 명시적으로 지정하지 않는 클로저에서 달러 기호 접두사가 붙은 변수는 매개변수를 익명으로 나타내므로 간결한 코드를 쉽게 작성할 수 있습니다 Swift의 컬렉션 알고리즘을 적용해 보겠습니다 제 서버에는 제 친구의 친구를 보여주면서 제가 알 수도 있는 사람을 찾아주는 기능이 있습니다 컬렉션 알고리즘을 사용해 이 사용자 집합을 계산할 수 있습니다 이 UserStore 클래스는 사용자 딕셔너리를 캡슐화하고 사용자 이름으로 사용자를 조회하는 기존 메서드가 몇 가지 있습니다 friendsOfFriends를 쿼리하는 메서드를 추가하겠습니다 시작하려면 원래 사용자를 찾아야 합니다 다음으로 원래 사용자와 그 친구의 사용자 아이디를 포함하는 Set을 만들겠습니다 여기에는 결과에서 제외할 사용자 이름이 포함됩니다 이제 함수형 프로그래밍을 사용해 이 메서드의 결과를 빌드하겠습니다 먼저 친구의 사용자명을 User 구조체의 인스턴스에 매핑해 친구에게 액세스할 수 있도록 하겠습니다 저는 compactMap을 사용해 각 친구의 User 구조체를 조회하고 nil인 User는 모두 제외했습니다 다음으로, 사용자의 모든 친구를 모아보겠습니다
flatMap이 이러한 친구 목록을 새로운 배열로 연결합니다 마지막으로 원래 사용자와 친구들을 제외해야 합니다 filter는 제외된 세트에 있는 사용자 이름을 삭제합니다 거의 완료되었지만 결과에 문제가 하나 있습니다 중복된 사용자 이름이 포함되었을 수 있다는 것입니다 표준 라이브러리에는 결과를 가져와서 고유한 요소만 반환하는 알고리즘이 없지만 제네릭을 사용하여 꽤 쉽게 구현할 수 있습니다 Collection을 확장하여 uniqued라는 메서드를 추가하겠습니다 uniqued의 구현은 간단합니다 먼저 컬렉션의 콘텐츠로 Set을 만든 다음 그 Set을 다시 Array로 변환하면 됩니다 이 방법은 잘 작동하진 않는데 Set 타입은 그 안에 저장된 요소가 Hashable 프로토콜을 준수해야 하기 때문입니다 Set는 값을 효율적으로 저장하기 위해 해싱에 의존하기 때문입니다 이 요구 사항을 충족하려면 Collection에 대한 확장을 Hashable을 준수하는 요소가 있는 컬렉션으로만 제한할 수 있습니다 이제 uniqued를 호출하면 끝입니다
몇 줄의 코드만으로 모든 Collection 타입에 적용할 수 있는 유용한 알고리즘을 구축했습니다 Swift 프로토콜의 유연성 덕분에 이제 uniqued 메서드가 있는 타입 세트는 일부 클래스 계층 구조에 제한되지 않습니다 프로토콜과 제네릭으로 훨씬 많은 일을 할 수 있습니다 자세히 알아보고 싶으시다면 ‘Swift 제네릭 사용하기’ 및 ‘Swift의 디자인 프로토콜 인터페이스’ 세션을 시청하세요 좋습니다, HTTP 서버 구축으로 넘어가기 전에 중요한 Swift 개념을 하나 더 알려 드리고 싶습니다 바로 동시 실행입니다 Swift에서 동시 실행의 기본 단위는 태스크로 독립적인 동시 실행 컨텍스트를 나타냅니다 태스크는 가벼우므로 많이 만들 수 있습니다 완료될 때까지 기다렸다가 결과를 얻을 수도 있고 작업 내용이 불필요해지면 취소할 수도 있습니다 태스크는 동시에 실행할 수 있어서 제 서버에서 수신하는 HTTP 요청을 처리하는 데 유용합니다 태스크는 실행되는 동안 비동기 작업을 할 수 있습니다 디스크에서 읽거나 다른 서비스에 메시지를 보내고 응답을 기다릴 수 있죠 태스크가 비동기 작업이 완료되기를 기다리는 동안에는 할 일이 있는 다른 태스크에 CPU를 양보하기 위해 일시 중단됩니다 코드에서 태스크 일시 중단을 모델링하기 위해 Swift는 async/await 구문을 사용합니다 일시 중단할 수 있는 함수는 async 키워드로 표시됩니다 async 함수가 호출되면 await 키워드로 해당 줄에서 일시 중단이 발생할 수 있음을 나타냅니다 서버에 Swift의 동시 실행 기능을 적용해 보겠습니다 저는 앞서 작업했던 패키지를 계속 빌드해 보겠습니다 서버 개발 환경에서 VSCode를 사용하겠습니다 서버에 대한 새 대상을 생성해 패키지를 업데이트했습니다 이 패키지는 오픈 소스 HTTP 서버 프레임워크인 Hummingbird에 종속되어 있습니다 Hummingbird는 요청 수신과 응답 전송을 처리하므로 애플리케이션 로직에 집중할 수 있습니다 연결을 수신하기 위한 최소한의 코드를 작성했지만 아직 요청을 할 수는 없습니다 요청 핸들러에는 UserStore에 대한 참조가 필요합니다 편의상 UserStore를 확장하여 요청 사이에 공유할 정적 인스턴스를 추가하겠습니다 코드에 문제가 있는 것 같군요 전역 UserStore 변수에 액세스하면 데이터 경합이 발생할 수 있는데 UserStore가 Sendable이 아니기 때문입니다 무슨 의미인지 살펴보겠습니다 서버가 두 개의 요청을 동시에 수신한다고 가정해 보겠습니다 첫 번째 요청과 관련된 태스크는 사용자를 조회해야 하고 다른 태스크는 새 사용자를 생성하는 중입니다 UserStore는 공유 변경 가능 상태이므로 태스크들이 서로 다른 스레드에서 동일한 메모리에 액세스할 수 있습니다 이것은 데이터 경합이며 충돌이나 예측할 수 없는 동작 가능성이 생깁니다 코드에서 데이터 경합을 피하는 것은 중요합니다 그렇기 때문에 Swift 6 언어 모드에서는 컴파일을 할 때 프로그램의 데이터 경합 안전성이 완전히 검증됩니다 데이터 경합 안전성을 확보하는 방법 중 하나는 동시 실행 도메인 사이에서 Sendable 값이 공유되어야 한다는 것입니다 Sendable 값은 해당 상태를 동시 액세스에서 보호하는 값입니다 예를 들어, 어떤 타입이 Sendable에 해당하려면 가변 상태를 읽고 쓰는 동안 락을 획득해야 합니다 컴파일러에서 UserStore 전역 변수가 안전하지 않다고 한다면 UserStore가 Sendable로 알려지지 않았기 때문입니다 UserStore를 Sendable로 만들려면 수동으로 동기화를 추가하면 됩니다 하지만 Swift에는 액터라는 더 편리한 기능이 있습니다 액터는 공유 가변 상태를 캡슐화할 수 있는 참조 타입이라는 점에서 클래스와 유사합니다 하지만 액터는 액세스를 직렬화해 자동으로 상태를 보호합니다 액터에서는 한 번에 하나의 태스크만 실행할 수 있습니다 액터 컨텍스트 외부에서 액터 메서드에 대한 호출은 비동기식입니다 이제 이 오류가 무엇을 알려 주는지 조금 더 알게 되었으니 UserStore를 액터로 만들어 동시 액세스를 안전하게 만들 수 있습니다
이제 액세스가 동기화되어서 오류가 사라졌습니다 이제 HTTP 요청 핸들러를 작성할 수 있습니다 friendsOfFriends 경로를 추가하면 사용자 이름을 인수로 받고 문자열 배열을 반환합니다 이 핸들러는 Hummingbird가 독립적인 태스크에서 실행하는 클로저입니다 UserStore는 액터이고 이 클로저에의 다른 동시성 도메인에서 액세스하고 있으므로 해당 액세스는 비동기식이며 await 키워드를 사용해야 합니다
이제 서버에 curl로 요청을 전송해 핸들러를 빠르게 테스트해 보겠습니다
기대했던 응답이 나왔습니다 Swift 6의 완벽한 데이터 경합 방지 기능 덕분에 서버가 동시 실행을 올바르게 처리할 것이라고 믿을 수 있습니다 태스크, async/await, 액터 등 Swift의 동시 실행 코드 작성의 기본을 다루어 보았지만 이보다 훨씬 많은 내용이 있습니다 ‘Swift의 구조화된 동시성 살펴보기’를 시작으로 삼아 보세요
마지막으로 살펴볼 Swift 기능 카테고리는 언어 확장과 관련된 강력한 기능입니다 라이브러리 작성자가 표현력이 풍부하고 타입이 안전한 API를 구축하고 애플리케이션에서 상용구 코드를 제거하기 위해 자주 사용하죠 첫 번째 기능은 속성 래퍼입니다 이 래퍼 타입은 저장된 값을 관리하기 위한 로직을 캡슐화합니다 속성 읽기 및 쓰기 호출을 인터셉트하여 간단한 어노테이션으로 속성에 적용할 수 있는 재사용 가능한 로직을 구현합니다 이 예시에서는 Argument 속성 래퍼를 swift-argument-parser 패키지에서 사용자 이름 속성에 적용했습니다 Argument 래퍼는 속성이 명령줄 인수의 값을 저장하도록 지정합니다 어떻게 작동하는지 살펴보겠습니다 패키지에서 명령줄 유틸리티를 위한 새 대상을 만들었습니다 새 대상은 명령줄 인수를 구문 분석하는 데 사용하는 ArgumentParser 패키지에 종속됩니다 AsyncParsableCommand를 준수하는 타입이 메인 파일에서 제 도구에서 허용하는 인수의 최상위 구성을 제공하고 있습니다
어떻게 작동하는지 살펴보겠습니다
인수 파서가 제 도구에 대한 멋진 도움말 메시지를 생성했네요 하지만 지금까지 도구가 한 것은 이 설명을 출력하는 것뿐입니다 첫 번째 하위 명령을 추가하여 이를 바꿔 보겠습니다 AsyncParsableCommand를 준수하는 새 구조체를 만들어 시작합니다 이 명령은 사용자의 친구의 친구를 요청하며 앞서 구축한 서버 경로를 사용합니다 이를 하위 명령으로 등록해야 합니다 이 명령은 사용자 이름이라는 인수 하나를 가지며 Argument 속성 래퍼를 사용하여 해당 속성에 어노테이션했습니다 다음으로 실행 메서드의 구현을 채워야 합니다 저는 이 도구가 전송할 HTTP 요청을 캡슐화하기 위해 제가 작성한 Request 유틸리티를 활용하고 있습니다 이 유틸리티는 명령의 상대 경로 응답에 필요한 데이터 타입과 딕셔너리 형식의 일부 인수로 초기화됩니다 이 메서드는 HTTP get 작업이므로 get 메서드를 호출하면 요청이 실행됩니다 이 메서드는 네트워크 요청을 전송하므로 async이며 응답을 기다리는 동안 현재 태스크는 일시 중단되어야 합니다 실행해 보겠습니다
사용자 이름을 지정하는 것을 잊었네요 인수 파서가 누락된 인수를 설명하기 위해 자동으로 유용한 출력을 몇 가지 만들었습니다 다시 실행하여 Alice를 지정하면 어떤 응답이 나오는지 보겠습니다
좋아요, 제대로 작동합니다 Argument 속성 래퍼를 사용하면 도구의 명령을 쉽게 빌드할 수 있으며 이는 Swift로 만들어낼 수 있는 명확한 API의 좋은 예입니다 라이브러리 개발자가 활용할 수 있는 또 다른 언어 도구는 결과 빌더입니다 결과 빌더를 사용하면 복잡한 값을 선언적으로 표현할 수 있습니다 결과 빌더 API는 특수하고 가벼운 구문으로 결과 값을 점진적으로 구축하는 클로저를 사용합니다 이 기능은 네이티브 UI 프레임워크 웹 페이지 생성기 등을 구축하는 데 사용되었습니다 Swift 표준 라이브러리에서 정규 표현식 빌더는 이 기능을 활용해 지나치게 간결한 정규 표현식 문자열의 대신 읽기 쉬운 대체를 제공합니다 속성 래퍼와 결과 빌더 외에도 매우 유연한 또 다른 도구 매크로가 있습니다 매크로는 컴파일러 플러그인 역할의 Swift 코드로 구문 트리를 입력으로 받아 변환된 코드를 출력으로 반환합니다 더 자세히 알아보려면 결과 빌더에 대한 세션 ‘Write a DSL in Swift using result builders’를 시청하세요 매크로에 대한 내용은 ‘Swift 매크로 상세히 알아보기’에서 다룹니다 지금까지 Swift를 간략하게 살펴보았습니다 Swift를 처음 사용하시는 분도 한동안 사용하고 계신 분도 오늘 배운 내용을 바탕으로 Swift의 고유한 기능을 사용해 멋진 것을 만들어 보시기 바랍니다 코드를 작성하면서 작업에 적합한 도구를 선택할 수 있는 기회를 찾아보세요 클래스 대신 값 타입으로 무언가를 모델링하거나 제네릭을 사용해 알고리즘을 완전히 일반화하거나 액터로 데이터 경합을 해결할 수도 있습니다 Swift에는 간결하면서도 강력한 코드 작성에 필요한 모든 도구가 있습니다 오늘 참여해 주셔서 감사합니다 멋진 WWDC 되세요
-
-
1:49 - Integer variables
var x: Int = 1 var y: Int = x x = 42 y
-
3:04 - User struct
struct User { let username: String var isVisible: Bool = true var friends: [String] = [] } var alice = User(username: "alice") alice.friends = ["charlie"] var bruno = User(username: "bruno") bruno.friends = alice.friends alice.friends.append("dash") bruno.friends
-
3:05 - User struct error handling
struct User { let username: String var isVisible: Bool = true var friends: [String] = [] mutating func addFriend(username: String) throws { guard username != self.username else { throw SocialError.befriendingSelf } guard !friends.contains(username) else { throw SocialError.duplicateFriend(username: username) } friends.append(username) } } enum SocialError: Error { case befriendingSelf case duplicateFriend(username: String) } var alice = User(username: "alice") do { try alice.addFriend(username: "charlie") try alice.addFriend(username: "charlie") } catch { error } var allUsers = [ "alice": alice ] func findUser(_ username: String) -> User? { allUsers[username] } if let charlie = findUser("charlie") { print("Found \(charlie)") } else { print("charlie not found") } let dash = findUser("dash")!
-
11:01 - SocialGraph package manifest
// swift-tools-version: 6.0 import PackageDescription let package = Package( name: "SocialGraph", products: [ .library( name: "SocialGraph", targets: ["SocialGraph"]), ], dependencies: [ .package(url: "https://github.com/apple/swift-testing.git", branch: "main"), ], targets: [ .target( name: "SocialGraph"), .testTarget( name: "SocialGraphTests", dependencies: [ "SocialGraph", .product(name: "Testing", package: "swift-testing"), ]), ] )
-
11:12 - User struct
/// Represents a user in the social graph. public struct User: Equatable, Hashable { /// The user's username, which must be unique in the service. public let username: String /// Whether or not the user should be considered visible /// when performing queries. public var isVisible: Bool /// The usernames of the user's friends. public private(set) var friends: [String] public init( username: String, isVisible: Bool = true, friends: [String] = [] ) { self.username = username self.isVisible = isVisible self.friends = friends } /// Adds a username to the user's list of friends. Throws an /// error if the username cannot be added. public mutating func addFriend(username: String) throws { guard username != self.username else { throw SocialError.befriendingSelf } guard !friends.contains(username) else { throw SocialError.alreadyFriend(username: username) } friends.append(username) } }
-
12:36 - Classes
class Pet { func speak() {} } class Cat: Pet { override func speak() { print("meow") } func purr() { print("purr") } } let pet: Pet = Cat() pet.speak() if let cat = pet as? Cat { cat.purr() }
-
12:59 - Automatic reference counting
class Pet { var toy: Toy? } class Toy {} let toy = Toy() let pets = [Pet()] // Give toy to pets for pet in pets { pet.toy = toy } // Take toy from pets for pet in pets { pet.toy = nil }
-
13:26 - Reference cycles
class Pet { weak var owner: Owner? } class Owner { var pets: [Pet] }
-
14:20 - Protocols
protocol StringIdentifiable { var identifier: String { get } } extension User: StringIdentifiable { var identifier: String { username } }
-
15:21 - Common capabilities of Collections
let string = "🥚🐣🐥🐓" for char in string { print(char) } // => "🥚" "🐣" "🐥" "🐓" print(string[string.startIndex]) // => "🥚"
-
15:31 - Collection algorithms
let numbers = [1, 4, 7, 10, 13] let numberStrings = numbers.map { number in String(number) } // => ["1", "4", "7", "10", "13"] let primeNumbers = numbers.filter { number in number.isPrime } // => [1, 7, 13] let sum = numbers.reduce(0) { partial, number in partial + number } // => 35
-
15:45 - Collection algorithms with anonymous parameters
let numbers = [1, 4, 7, 10, 13] let numberStrings = numbers.map { String($0) } // => ["1", "4", "7", "10", "13"] let primeNumbers = numbers.filter { $0.isPrime } // => [1, 7, 13] let sum = numbers.reduce(0) { $0 + $1 } // => 35
-
16:13 - Friends of friends algorithm
/// An in-memory store for users of the service. public class UserStore { var allUsers: [String: User] = [:] } extension UserStore { /// If the username maps to a User and that user is visible, /// returns the User. Returns nil otherwise. public func lookUpUser(_ username: String) -> User? { guard let user = allUsers[username], user.isVisible else { return nil } return user } /// If the username maps to a User and that user is visible, /// returns the User. Otherwise, throws an error. public func user(for username: String) throws -> User { guard let user = lookUpUser(username) else { throw SocialError.userNotFound(username: username) } return user } public func friendsOfFriends(_ username: String) throws -> [String] { let user = try user(for: username) let excluded = Set(user.friends + [username]) return user.friends .compactMap { lookUpUser($0) } // [String] -> [User] .flatMap { $0.friends } // [User] -> [String] .filter { !excluded.contains($0) } // drop excluded .uniqued() } } extension Collection where Element: Hashable { func uniqued() -> [Element] { let unique = Set(self) return Array(unique) } }
-
19:23 - async/await
/// Makes a network request to download an image. func fetchUserAvatar(for username: String) async -> Image { // ... } let avatar = await fetchUserAvatar(for: "alice")
-
19:43 - Server
import Hummingbird import SocialGraph let router = Router() extension UserStore { static let shared = UserStore.makeSampleStore() } let app = Application( router: router, configuration: .init(address: .hostname("127.0.0.1", port: 8080)) ) print("Starting server...") try await app.runService()
-
20:20 - Data race example
// Look up user let user = allUsers[username] // Store new user allUsers[username] = user // UserStore var allUsers: [String: User]
-
22:24 - Server with friendsOfFriends route
import Hummingbird import SocialGraph let router = Router() extension UserStore { static let shared = UserStore.makeSampleStore() } router.get("friendsOfFriends") { request, context -> [String] in let username = try request.queryArgument(for: "username") return try await UserStore.shared.friendsOfFriends(username) } let app = Application( router: router, configuration: .init(address: .hostname("127.0.0.1", port: 8080)) ) print("Starting server...") try await app.runService()
-
23:27 - Property wrappers
struct FriendsOfFriends: AsyncParsableCommand { @Argument var username: String mutating func run() async throws { // ... } }
-
23:57 - SocialGraph command line client
import ArgumentParser import SocialGraph @main struct SocialGraphClient: AsyncParsableCommand { static let configuration = CommandConfiguration( abstract: "A utility for querying the social graph", subcommands: [ FriendsOfFriends.self, ]) } struct FriendsOfFriends: AsyncParsableCommand { @Argument(help: "The username to look up friends of friends for") var username: String func run() async throws { var request = Request(command: "friendsOfFriends", returning: [String].self) request.arguments = ["username" : username] let result = try await request.get() print(result) } }
-
26:07 - Result builders
import RegexBuilder let dollarValueRegex = Regex { // Equivalent to "\$[0-9]+\.[0-9][0-9]" "$" OneOrMore(.digit) "." Repeat(.digit, count: 2) }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.