• Meet Swift on Server

    • 왜 Swift on the server인가?
      • C수준의 성능
      • 낮은 메모리 footprint
        • 가비지 컬렉션대신 ARC를 사용하기 떄문
        • 이러한 특성은 빠른 시동 시간과 예측 가능한 리소스 소비가 중요한 모던 클라우드 서비스에서 중요하다.
      • 표현력이 풍부하고 안전하여 수많은 버그를 컴파일 타임에 방지해준다.
        • 개발자는 견고하고 신뢰성있는 분산 시스템을 작성할 수 있게 된다.
        • 강 타입, 옵셔널, 메모리 안정성 등이 크래시를 방지해주고 보안 위협을 줄여준다.
      • 동시성을 자체적으로 지원한다.
        • 클라우드 서비스들은 동시에 여러 작업을 해야할 때가 많은데 Swift의 이러한 특성이 확장 가능하고 응답성이 좋으면서도 일반적인 데이터 경쟁 상태를 일으키지 않도록 막아주는 앱을 작성하게 해준다.
    • 이미 애플의 핵심 클라우드 서비스들이 Swift로 돌아간다.
      • iCloud Keychain, Phohos, Notes
      • AppStore 프로세스 파이프라인
      • SharePlay 파일 공유
      • Private Cloud Compute service
      • 여러 서비스에 걸쳐 초당 수백만 건의 요청을 Swift on Server 애플리케이션이 처리하고 있다.
    • 애플 플랫폼 바깥에서도 서버 생태계는 Swift의 초창기 사용자 중 하나였다.
      • Swift Server 워크그룹이 2016년에 만들어졌다. Swift 오픈소스화가 2015년이였는데.
      • 워크그룹은 Swift를 서버에 사용하는 회사들의 대표자들과 개인 기여자들로 이루어진다.
      • 워크그룹은 서버 개발 및 배포에 Swift를 사용하도록 촉진하는 데 촛점이 맞춰져있다.
        • 서버 커뮤니티의 필요를 정의하고 우선순위를 정하기
        • 패키지 인큐베이션 프로젝트를 통해서 중복 노력을 줄이고 호환성을 높이고 모범 사례를 촉진하기
        • 다른 워크그룹들과의 소통 창구
  • Build a service

    • 이벤트와 이벤트 참여자를 목록화하는 서비스만들기

      • 전체 이벤트 리스트 목록화 하기와 이벤트 추가 연산을 필요로 한다.
    • 편집기 선택하기

      • Xcode
      • VS Code
      • Neovim
      • 그 외 Language Server Protocol을 지원하는 모든 편집기
    • 필요한 패키지 정의하기(Package.swift)

      • openapi-generator와 vapor를 사용한다.

        // swift-tools-version:5.9
        import PackageDescription
        
        let package = Package(
          name: "EventService",
          platforms: [.macOS(.v14)],
          dependencies: [
            .package(
              url: "<https://github.com/apple/swift-openapi-generator>",
              from: "1.2.1"
            ),
            .package(
              url: "<https://github.com/apple/swift-openapi-runtime>",
              from: "1.4.0"
            ),
            .package(
              url: "<https://github.com/vapor/vapor>",
              from: "4.99.2"
            ),
            .package(
              url: "<https://github.com/swift-server/swift-openapi-vapor>",
              from: "1.0.1"
            ),
          ],
          targets: [
            .target(
              name: "EventAPI",
              dependencies: [
                .product(
                  name: "OpenAPIRuntime",
                  package: "swift-openapi-runtime"
                ),
              ],
              plugins: [
                .plugin(
                  name: "OpenAPIGenerator",
                  package: "swift-openapi-generator"
                )
              ]
            ),
            .executableTarget(
              name: "EventService",
              dependencies: [
                "EventAPI",
                .product(
                  name: "OpenAPIRuntime",
                  package: "swift-openapi-runtime"
                ),
                .product(
                  name: "OpenAPIVapor",
                  package: "swift-openapi-vapor"
                ),
                .product(
                  name: "Vapor",
                  package: "vapor"
                ),
              ]
            ),
          ]
        )
        
      • swift openapi generator는 api를 YAML로 문서화하고 서버와 클라이언트 코드를 만들어준다.

        • 상세 내용은 작년 Meet Swift OpenAPI Generator 세션 참조
        openapi: "3.1.0"
        info:
          title: "EventService"
          version: "1.0.0"
        servers:
          - url: "<https://localhost:8080/api>"
            description: "Example service deployment."
        paths:
          /events:
            get:
              operationId: "listEvents"
              responses:
                "200":
                  description: "A success response with all events."
                  content:
                    application/json:
                      schema:
                        type: "array"
                        items:
                          $ref: "#/components/schemas/Event"
            post:
              operationId: "createEvent"
              requestBody:
                description: "The event to create."
                required: true
                content:
                  application/json:
                    schema:
                      $ref: '#/components/schemas/Event'
              responses:
                '201':
                  description: "A success indicating the event was created."
                '400':
                  description: "A failure indicating the event wasn't created."
        components:
          schemas:
            Event:
              type: "object"
              description: "An event."
              properties:
                name:
                  type: "string"
                  description: "The event's name."
                date:
                  type: "string"
                  format: "date"
                  description: "The day of the event."
                attendee:
                  type: "string"
                  description: "The name of the person attending the event."
              required:
                - "name"
                - "date"
                - "attendee"
        
      • 초기 서버 구현(정적 데이터만 반환)

        import OpenAPIRuntime
        import OpenAPIVapor
        import Vapor
        import EventAPI
        
        @main
        struct Service {
          static func main() async throws {
            let application = try await Vapor.Application.make()
            let transport = VaporTransport(routesBuilder: application)
        
            let service = Service()
            try service.registerHandlers(
              on: transport,
              serverURL: URL(string: "/api")!
            )
        
            try await application.execute()
          }
        }
        
        extension Service: APIProtocol {
          func listEvents(
            _ input: Operations.listEvents.Input
          ) async throws -> Operations.listEvents.Output {
            let events: [Components.Schemas.Event] = [
              .init(name: "Server-Side Swift Conference", date: "26.09.2024", attendee: "Gus"),
              .init(name: "Oktoberfest", date: "21.09.2024", attendee: "Werner"),
            ]
        
            return .ok(.init(body: .json(events)))
          }
        
          func createEvent(
            _ input: Operations.createEvent.Input
          ) async throws -> Operations.createEvent.Output {
            return .undocumented(statusCode: 501, .init())
          }
        }
        
      • 데이터 추가를 위해 Database driver 추가

        • PostgreSQL
        • MySQL
        • Cassandra
        • MongoDB
        • Oracle
        • SQLite
        • DynamoDB
      • 예제에서는 Postgre를 사용할 것이다.

        • PostgresNIO는 Vapor와 Apple에 의해서 관리되는 오픈소스 database driver다.
        • 1.21부터 PostgresClient를 통해서 async/await 인터페이스를 제공한다.
          • 내부 연결 풀을 사용
            • 네트워크 통신 실패에도 탄력있게 대응가능.
            • 여러 연결을 통해서 쿼리를 넣어서 throughput 향상
            • 빠른 쿼리 실행을 위해서 prewarming
      • 의존성 추가

        // swift-tools-version:5.9
        import PackageDescription
        
        let package = Package(
          name: "EventService",
          platforms: [.macOS(.v14)],
          dependencies: [
            .package(
              url: "<https://github.com/apple/swift-openapi-generator>",
              from: "1.2.1"
            ),
            .package(
              url: "<https://github.com/apple/swift-openapi-runtime>",
              from: "1.4.0"
            ),
            .package(
              url: "<https://github.com/vapor/vapor>",
              from: "4.99.2"
            ),
            .package(
              url: "<https://github.com/swift-server/swift-openapi-vapor>",
              from: "1.0.1"
            ),
            .package(
              url: "<https://github.com/vapor/postgres-nio>",
              from: "1.19.1"
            ),
          ],
          targets: [
            .target(
              name: "EventAPI",
              dependencies: [
                .product(
                  name: "OpenAPIRuntime",
                  package: "swift-openapi-runtime"
                ),
              ],
              plugins: [
                .plugin(
                  name: "OpenAPIGenerator",
                  package: "swift-openapi-generator"
                )
              ]
            ),
            .executableTarget(
              name: "EventService",
              dependencies: [
                "EventAPI",
                .product(
                  name: "OpenAPIRuntime",
                  package: "swift-openapi-runtime"
                ),
                .product(
                  name: "OpenAPIVapor",
                  package: "swift-openapi-vapor"
                ),
                .product(
                  name: "Vapor",
                  package: "vapor"
                ),
                .product(
                    name: "PostgresNIO",
                  package: "postgres-nio"
                ),
              ]
            ),
          ]
        )
        
    • PostgresClient 추가

      import OpenAPIRuntime
      import OpenAPIVapor
      import Vapor
      import EventAPI
      import PostgresNIO
      
      @main
      struct Service {
        let postgresClient: PostgresClient
        
        static func main() async throws {
          let application = try await Vapor.Application.make()
          let transport = VaporTransport(routesBuilder: application)
      
          let postgresClient = PostgresClient(
            configuration: .init(
              host: "localhost",
              username: "postgres",
              password: nil,
              database: nil,
              tls: .disable
            )
          )
          let service = Service(postgresClient: postgresClient)
          try service.registerHandlers(
            on: transport,
            serverURL: URL(string: "/api")!
          )
      
          try await withThrowingDiscardingTaskGroup { group in
            group.addTask {
              await postgresClient.run()
            }
      
            group.addTask {
              try await application.execute()
            }
          }
        }
      }
      
      extension Service: APIProtocol {
        func listEvents(
          _ input: Operations.listEvents.Input
        ) async throws -> Operations.listEvents.Output {
          let rows = try await self.postgresClient.query("SELECT name, date, attendee FROM events")
      
          var events = [Components.Schemas.Event]()
          for try await (name, date, attendee) in rows.decode((String, String, String).self) {
            events.append(.init(name: name, date: date, attendee: attendee))
          }
      
          return .ok(.init(body: .json(events)))
        }
      
        func createEvent(
          _ input: Operations.createEvent.Input
        ) async throws -> Operations.createEvent.Output {
          return .undocumented(statusCode: 501, .init())
        }
      }
      
    • createEvent 구현

      func createEvent(
        _ input: Operations.createEvent.Input
      ) async throws -> Operations.createEvent.Output {
        switch input.body {
        case .json(let event):
          try await self.postgresClient.query(
            """
            INSERT INTO events (name, date, attendee)
            VALUES (\\(event.name), \\(event.date), \\(event.attendee))
            """
          )
          return .created(.init())
        }
      }
      
    • 위 쿼리는 문자열처럼 보이지만 실제로는 Swift의 String interpolation이다.

      • 다음과 동일하다
      let query = PostgresQuery(
            """
            INSERT INTO events (name, date, attendee)
            VALUES ($0, $1, $2)
            """
      )
      
      query.binds.append(event.name)
      query.binds.append(event.date)
      query.binds.append(event.attendee)
      
    • 에러 로깅

      • DB에러(PSQLError)는 의도적으로 상세 정보를 누락하여 DB 정보가 누출되는 것(스키마 등)을 막는다.
      • 이때는 추가적인 도구를 통해서 트러블슈팅을 해야한다.
    • Observability

      • 크게 3개의 축이 있다.
        • Logging: 뭘 했는가?
        • Metrics: 서비스가 어떤 상태인가?
        • Tracing: 어떤 경로를 통해서 요청이 처리되었는가?
          • 모던 클라우드 시스템에서는 분산 시스템 내에서 여러 시스템을 거쳐서 처리되기 때문에 필요
    • Swift는 이 3가지 축에 대한 패키지를 모두 지원한다.

      • distributed tracing의 동작을 더 알고 싶으면 Beyond the basics of structured concurrency 세션 참조
      func listEvents(
        _ input: Operations.listEvents.Input
      ) async throws -> Operations.listEvents.Output {
      	// swift-log로 로그 남기기
        let logger = Logger(label: "ListEvents")
        logger.info("Handling request", metadata: ["operation": "\\(Operations.listEvents.id)"])
      	
      	// swift-metrics로 metric 남기기
        Counter(label: "list.events.counter").increment()
      
      	// swift-distributed-tracing으로 요청 전체에서 쿼리 영역을 정의
        return try await withSpan("database query") { span in
          let rows = try await postgresClient.query("SELECT name, date, attendee FROM events")
          return try await .ok(.init(body: .json(decodeEvents(rows))))
        }
      }
      
    • 이를 위해서는 앱 시작시 각 시스템들을 부트스트래핑 해야한다.

      • 시작하자마자해야 이벤트 유실을 막을 수 있다.

      • log - metric - InstrumentationSystem 순으로 초기화하는 것을 권장한다.

        • metric과 InstrumentationSystem이 로그를 남기고 싶을 수 있으니까
        @main
        struct Service {
        	static func main() async throws {
        		LoggingSystem.bootstrap(StreamLogHandler.standardError)
        		
        		let registry = PrometheusCollectorRegistry()
        		MetricsSystem.bootstrap(PrometheusMetricsFactory(registry: registry))
        		
        		let otelTracer = OTel.Tracer(...)
        		InstrumentationSystem.bootstrap(otelTracer)
        	}
        }
        
    • createEvent에 로그를 남기기

      func createEvent(
        _ input: Operations.createEvent.Input
      ) async throws -> Operations.createEvent.Output {
        switch input.body {
        case .json(let event):
          do {
            try await self.postgresClient.query(
              """
              INSERT INTO events (name, date, attendee)
              VALUES (\\(event.name), \\(event.date), \\(event.attendee))
              """
            )
            return .created(.init())
          } catch let error as PSQLError {
            let logger = Logger(label: "CreateEvent")
      
            if let message = error.serverInfo?[.message] {
              logger.info(
                "Failed to create event",
                metadata: ["error.message": "\\(message)"]
              )
            }
            
            return .badRequest(.init())
          }
        }
      }
      
  • Explore the ecosystem

    • 여러 유즈케이스에 맞는 다양한 라이브러리가 있다.
      • 네트워킹
      • database driver
      • observability
      • message streaming
      • 그외 등등
    • 더 다양한 패키지는 swift.org의 Packages 섹션의 server카테고리 참조
    • Incubation process에서도 좋은 패키지들을 발견할 수 있다.