[iOS] Vapor를 통한 서버 사이드 프로젝트

Youngwoo Lee·2021년 7월 11일
2

iOS

목록 보기
20/46
post-thumbnail
post-custom-banner

Vapor

Swift에서 가장 많이 사용하는 Server Side Frameworks이다.

  • Perfect, Vapor, Kitura 가 있는데, Vapor가 가장 github star 개수가 많은 만큼 대중적이다.

Vapor Toolbox의 경우는 Homebrew를 통해 설치할 수 있다

brew install vapor

Vapor Toolbox의 명령어를 확인하기 위해서는 vapor --help 입력

vapor new task-management 를 통해서 vapor 프로젝를 생성

이후 세 가지 옵션을 정해야 된다.

Fluent를 사용할지, 데이터베이스의 종류, Leaf를 사용할지 안할지를 입력한다

Leaf는 HTML 템플릿 언어

폴더 구조

Package.swift

Swift Package Manager가 프로젝트에서 가장 먼저 찾는 파일로, 패키지의 의존성과 타깃 등을 정의합니다. 이 파일은 항상 프로젝트의 루트 디렉터리에 위치하고, Package.swift라는 이름이어야 합니다.

Sources

프로젝트의 모든 Swift 소스 파일을 포함한다

App

앱을 구성하는데 필요한 코드를 포함합니다

Controllers

로직을 그룹화하는 컨트롤러가 위치한다

Migrations

데이터베이스 마이그레이션을 정의하는 타입이 위치한다

Models

데이터베이스의 데이터 구조를 나타내는 모델이 위치한다

Configure.swift

앱을 구성하는 configure(_:) 함수를 포함합니다. main.swift 에 의해 호출되며 이 함수에 라우트, 테이터베이스 등의 서비스를 등록해야 한다.

route.swift

앱에 라우트를 등록하는 route(_:) 함수를 포함하며, configure(_:) 함수에 의해 호출됩니다.

Run

앱을 실행하는데 필요한 코드만 포함합니다.

main.swift

앱의 인스턴스를 생성하고 실행합니다.

Tests

XCTVapor 모듈을 사용하는 단위 테스트를 포함합니다.


프레임 워크

SwiftNIO

Vapor는 Applie에서 만든 SwiftNIO 프레임워크를 기반으로 설계되었습니다. SwiftNIO는 비동기 네트워킹 프레임워크로, Vapor의 모든 HTTP 통신을 처리합니다. Vapor가 요청을 받고 응답을 보낼 수 있게 하며, 연결 및 데이터 전송을 관리합니다.

Vapor의 일부 API는 EventLoopFuture 제네릭 타입을 반환합니다. EventLoopFuture는 나중에 제공될 결과에 대한 플레이스홀더입니다. 비동기적으로 동작하는 함수는 실제 데이터를 즉시 반환하지 않고, 대신 EventLoopFuture를 반환합니다.

Fluent

FluentSwift용 ORM(Object-relational mapping) 프레임워크입니다. Vapor 앱과 데이터베이스 사이의 추상화 계층으로, 데이터베이스를 쉽게 사용할 수 있도록 인터페이스를 제공합니다. 따라서 SQL을 작성하지 않고도 Swift로 데이터베이스 작업을 수행할 수 있습니다.


Project 시작

PostgreSql 설치

Database 설치를 위해서 이번에 사용할 Postgres database를 설치해준다

brew install postgresql 을 통해서 postgreSql을 다운 받아준다

pg_ctl -D /usr/local/var/postgres start 명령어 실행

brew services start postgresql //brew 환경에서 postgresql을 시작시켜 줌


Database 설치

createdb 데이터베이스이름 을 통해서 Database 를 설치


Project내에 의존성 추가

.package(url: "https://github.com/vapor/fluent.git", from: "4.0.0-rc"),
.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0")
// package.swift 파일에 의존성 추가

.product(name: "Fluent", package: "fluent"),
.product(name: "FluentPostgresDriver", package: "fluent-postgres-driver")
// package.swift 파일에 target 추가

Configure

database를 등록하는 과정을 해준다

configure.swift 파일에서

app.databases.use(.postgres(hostname: "localhost", username: "leeyoungwoo", password: "", database: "test1"), as: .psql)
//데이터베이스 등록

Model 생성

테이블 생성을 해주기 위해서는 해당 테이블에 대한 attribute에 대한 정의, Table에 대한 정의가 필요하다

모델은 schema라는 타입 프로퍼티를 갖습니다. 이 프로퍼티의 문자열은 데이터베이스 테이블 또는 컬렉션의 이름을 의미합니다.

final class TestModel: Model, Content {
	static let schema: String = "testSchema"
	// 테이블의 이름을 schema 값에 할당
	
	@ID(key: .id)
	var id: UUID?
  //속성의 고유 아이디가 된다. 이렇게 하면 자동생성이 되고 탐색할 때 이 id로 탐색하게 된다
  //UUID를 보면 이 고유 아이디가 되는 것을 알 수 있고 optional 타입이기 때문에 존재 하지 않을 수도 있다
	
	@Field(key: "name")
	var name: String
  //@Field : 모델은 데이터를 저장하기 위한 @Field 프로퍼티를 가질 수 있다. key 값으로는 데이터베이스 키를 사용하며, 프로퍼티 이름과 같지 않아도 된다.
	
	@Field(key: "job")
	var job: String
	
	init() { }
	//모델은 반드시 빈 이니셜라이저를 가져야만 한다. Fluent가 쿼리에 의해 모델을 생성하는데 내부적으로 사용하기 때문이다. 물론 모든 프로퍼티를 포함하는 이니셜라이저도 정의할 수 있다
  
	init(id: UUID? = nil, name: String, job: String) {
		self.id = id
		self.name = name
		self.job = job
	}
  //초기화 과정이 필수이다
}

객체지향형 데이터베이스 구조이기때문에 Model이 Class로 되어있어야 함을 주의해야된다!!

JSON 타입이기 때문에 Content 프로토콜을 통해서 Codable 하게 해준다

Fluent는 데이터베이스 키에 스네이크 케이스, 프로퍼티 이름에는 카멜케이스를 사용하는 것을 권장한다


Migration

바로 위처럼 테이블을 정의해주었다고 바로 테이블이 생성되는 것은 아니다. Migration(이주) 를 통해서 데이터들을 실제 데이터베이스로 이주시켜줘야 된다. 어떻게 이주시킬 것인지에 대해서 정의를 해주자!!!

import Fluent
import FluentPostgresDriver

struct CreateTestModel: Migration {
	func prepare(on database: Database) -> EventLoopFuture<Void> {
		database.schema("testSchema")
			.id()
			.field("name", .string, required)
			.field("job", .string)
			.create()
	}
	
	func revert(on database: Database) -> EventLoopFuture<Void> {
		database.schema("testSchema").delete()
	}
}

prepare(on:) 메서드는 데이터베이스를 변화시키는 동작을 수행합니다. 테이블, 필드, 제약 등을 추가하고 삭제하는 것과 같이 데이터베이스 스키마를 변경합니다. 또한 모델 인스턴스 생성, 필드 값 업데이트와 같이 데이터를 수정합니다.

revert(on:) 메서드는 이러한 변화를 되돌리는 동작을 수행합니다. 마이그레이션 실행 취소는 테스트를 용이하게 하고, 백업을 제공합니다.

이제 configure.swift 파일로 와서 지금 작성한 Migration 타입을 app에 추가해준다

app.migrations.add(CreateTestModel())
//테이블 추가

그리고 터미널에서

vapor run migrate 를 해주게 되면 이제 실제 Table이 생성된 것을 확인할 수 있다


POST, GET, PATCH 등등 정의해주기

이제 데이터베이스가 구축이 되었으니 데이터 생성, 수정, 삭제 등의 동작에 대한 정의만 해주면 된다

routes.swift 파일로 이동한다

app.post("test") { req -> EventLoopFuture<TestModel> in
	let exist = try req.content.decode(TestModel.self)
	return exist.create(on: req.db).map { (result) -> TestModel in
		return exist
	}
}

app.get("testAll") { req -> EventLoopFuture<[TestModel]> in
	return TestModel.query(on: req.db).all()
}

컨트롤러

컨트롤러는 요청에 대한 응답을 반환하는 메서드를 그룹화하기 좋은 수단입니다. 프로젝트 규모가 커질수록 코드의 책임을 명확히 분리하는 것이 유지보수에 용이하다


RouteCollection

Vapor는 라우트를 그룹화할 수 있도록 RouteCollection 프로토콜을 제공합니다. 컨트롤러가 RouteCollection 프로토콜을 채택하면 라우트를 등록하기 위한 boot(routes: ) 메서드를 구현해야 합니다.

GET /tasks
POST /tasks
DELETE /tasks/:id

우리가 사용하는 엔드포인트는 /tasks라는 같은 경로로 시작합니다. grouped(_:) 메서드를 통해 미리 지정된 경로를 사용하는 라우터를 생성할 수 있습니다.

routes.grouped("tasks")

DELETE 요청의 경우 경로가 /:id로 끝나는데, 이는 동적 파라미터이다. : 접두사로 시작하며, URL에 입력한 값을 가져와 사용할 수 있습니다

import Fluent
import Vapor

struct TaskController: RouteCollection {
	func boot(routes: RoutesBuilder) throws {
		let tasks = routes.grouped("tasks")
		tasks.get(use: showAll)
		tasks.post(use: create)
		tasks.group(":id") { task in
			tasks.delete(use: delete)
		}
	}
	
	func showAll(req: Request) throws -> EventLoopFuture<[Task]> {
		return Task.query(on: req.db).all()
	}
	
	func create(req: Request) throws -> EventLoopFuture {
  	let task = try req.content.decode(Task.self)
  	return task.create(on: req.db).map { task }
  }

  func delete(req: Request) throws -> EventLoopFuture {
    return Task.find(req.parameters.get("id"), on: req.db)
    .unwrap(or: Abort(.notFound))
    .flatMap { $0.delete(on: req.db) }
    .transform(to: .ok)
  }
}

데이터 검증

Vapor는 데이터를 디코딩하기 전에 요청을 검증할 수 있도록 Validation API를 제공합니다. 이를테면, 이메일, 정수 범위, URL 등의 요효성을 검사할 수 있습니다.

코드를 구현하기 위해서는 앞서 잘못된 요청 데이터에 따른 오류 메시지를 살펴보겠습니다. POST/ tasks 엔드포인트의 경우로 가정하며, 아래는 요청 데이터입니다.

{
	"title" : "Vapor 공부하기",
	"status" : "pending"
}

Task 타입으로 디코딩하면 다음과 같은 오류 메시지를 반환합니다.

Value of type Status required for key status

Status 타입의 값으로는 toDo, doing, done 만이 유효하므로 위 메시지는 틀리지 않았습니다. 다만, 메시지 자체로는 사용 가능한 Status 타입의 값을 유추하기가 어렵습니다. Validation API를 사용하면 다음과 같은 오류 메시지를 확인할 수 있습니다.


Heroku 를 통한 API 배포

Vapor를 통해서 앱을 구현해봤자, 이는 로컬에서만 사용이 가능하다. 그렇기 때문에 배포를 통해 URL로 접근이 가능하도록 만들어야 한다. 또한 데이터베이스를 로컬에 직접 설치하지 않고 사용해보자!

Heroku는 클라우드 서비스형 플랫폼(Platform as a Service, PaaS)으로, 여러 언어와 데이터베이스를 지원하며 서버, 하드웨어, 인프라 등을 자동으로 관리한다.


앱 생성

Heroku에서 대시보드를 통해서 앱을 생성, Create new app을 선택


설치

로컬에서 Homebrew를 사용할 수 있도록, Heroku CLI를 설치!!

brew install heroku/brew/heroku


Git

Heroku는 Git을 사용하여 앱을 배포한다. 대시보드에서 생성한 Heroku 앱 이름을 입력하고 연결

heroku git:remote -a my-app-name


PostgresSQL 설정

앱이 원격 데이터베이스에 접근할 수 있도록 설정해주자!!

DATABASE URL의 경우는 Heroku에 의해서 계속해서 변경될 수 있으므로, 따로 설정이 필요하다

if let databaseURL = Environment.get("DATABASE_URL"), var postgresConfig = 		PostgresConfiguration(url: databaseURL) {
	var clientTLSConfiguration = TLSConfiguration.makeClientConfiguration()
	clientTLSConfiguration.certificateVerification = .none
	postgresConfig.tlsConfiguration = clientTLSConfiguration
	app.databases.use(.postgres(configuration: postgresConfig), as: .psql)
}

코드를 configure.swift 폴더에 넣어주자 기존에 databaseURL 은 로컬 db니깐 삭제해준다


배포

Heroku는 기본적으로 master 브랜치를 배포한다. master 이외의 브랜치를 배포할 경우 branch-name:master 와 같이 입력한다.

git push heroku master


마이그레이션

데이터베이스 마이그레이션을 수행합니다

heroku run Run --migrate --env production

profile
iOS Developer Student
post-custom-banner

0개의 댓글