Scala Tapir로 HTTP API 구축 및 테스트 방법

INEZA Felin-Michel

INEZA Felin-Michel

22 May 2026

Scala Tapir로 HTTP API 구축 및 테스트 방법

Apidog 엔터프라이즈

온프레미스 배포

SSO & RBAC

SOC 2 준수

Apidog Enterprise 살펴보기

Tapir는 Typed API descRiptions의 약자입니다. 이는 HTTP 엔드포인트를 메서드, 경로, 입력, 출력 및 오류 사례와 같은 모든 요소를 타입 시스템에 캡처된 일반 값으로 설명할 수 있게 해주는 Scala 라이브러리입니다. 이 단일 설명을 통해 Tapir는 서버 경로, 클라이언트 및 OpenAPI 문서를 파생합니다. 한 번 계약을 작성하면 나머지는 따라옵니다.

이것이 Tapir가 웹 프레임워크에서 직접 경로를 작성하는 것과 다른 점입니다. 경로 핸들러는 HTTP 연결을 로직에 결합합니다. Tapir 엔드포인트는 이들을 분리합니다. 설명은 하나의 값이고, 비즈니스 로직은 별도의 함수이며, Tapir가 이들을 연결합니다. 이 튜토리얼은 작은 작업 API를 구축하고, 이를 서버에 연결하고, 문서를 생성하고, 실제 Scala 코드를 사용하여 테스트합니다.

Tapir가 제공하는 것

Tapir는 세 가지 이점을 가진 단순한 아이디어를 기반으로 합니다.

첫 번째는 타입 안전성입니다. 엔드포인트의 입력과 출력은 타입이 지정되어 있으므로, 컴파일러는 핸들러가 올바른 형태를 반환하는지 확인합니다. 엔드포인트가 약속하는 것과 로직이 생성하는 것 사이의 불일치는 프로덕션 버그가 아니라 컴파일 오류입니다.

두 번째는 단일 진실 공급원(single source of truth)입니다. 엔드포인트가 값이기 때문에, 동일한 설명으로부터 OpenAPI 사양, 서버 인터프리터 및 클라이언트를 생성합니다. 설명이 하나뿐이므로 서로 멀어질 수 없습니다. 이는 컴파일러에 의해 강제되는 API 계약 테스트 뒤에 있는 것과 동일한 원칙입니다.

세 번째는 프레임워크 독립성입니다. 엔드포인트 설명은 Akka HTTP, Pekko, http4s 또는 Netty에서 실행되는지 알지 못합니다. 서버 인터프리터를 별도로 선택하므로, 설명은 어떤 프레임워크 선택보다도 오래 지속됩니다.

Tapir가 무엇이 아닌지 명확히 할 가치가 있습니다. Tapir는 웹 프레임워크가 아닙니다. 소켓, 스레드 풀 또는 라우팅 엔진을 처리하지 않습니다. Tapir는 선택한 프레임워크 위에 있는 설명 계층입니다. 또한 일반적인 의미의 코드 생성기도 아닙니다. Scala 파일을 작성하여 수정한 다음 편집하는 도구를 실행하는 것이 아닙니다. 엔드포인트 값은 소스이며, 서버, 클라이언트 및 문서는 컴파일 시점에 이 값들로부터 계산됩니다. 이는 설명과 실행 중인 코드가 동기화되지 않는 것을 방지합니다. 이는 생성된 파일이 수동으로 편집되고 소스가 분기되는 생성기 기반 워크플로우의 반복적인 실패 모드입니다.

프로젝트 설정

Tapir를 build.sbt에 추가하세요. 코어 모듈, OpenAPI 모듈, 서버 인터프리터 및 JSON 라이브러리가 필요합니다. 이 예제에서는 Akka HTTP의 활발히 유지보수되는 후속작인 Pekko HTTP와 JSON을 위한 circe를 사용합니다.

val tapirVersion = "1.11.7"

libraryDependencies ++= Seq(
  "com.softwaremill.sttp.tapir" %% "tapir-core" % tapirVersion,
  "com.softwaremill.sttp.tapir" %% "tapir-pekko-http-server" % tapirVersion,
  "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % tapirVersion,
  "com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % tapirVersion,
  "com.softwaremill.sttp.apispec" %% "openapi-circe-yaml" % "0.11.3"
)

tapir.softwaremill.com의 현재 Tapir 문서는 생태계가 빠르게 변화하므로 모든 모듈과 해당 버전을 나열합니다.

타입이 지정된 엔드포인트 정의

도메인 타입과 해당 JSON 코덱부터 시작하세요. circe는 케이스 클래스에서 코덱을 파생합니다.

import io.circe.generic.auto._
import sttp.tapir._
import sttp.tapir.json.circe._
import sttp.tapir.generic.auto._

case class Task(id: Int, title: String, done: Boolean)
case class NewTask(title: String)
case class ApiError(message: String)

이제 엔드포인트를 설명합니다. 각 엔드포인트는 endpoint 기본값에 조합자를 연결하여 구축된 값입니다. 체인을 문장으로 읽어보세요: 이것은 정수 경로 입력을 받아 404와 함께 ApiError를 반환하거나 200과 함께 Task를 반환하는 /tasks/{id}에 대한 GET 요청입니다.

val getTask: PublicEndpoint[Int, ApiError, Task, Any] =
  endpoint.get
    .in("tasks" / path[Int]("id"))
    .errorOut(statusCode(sttp.model.StatusCode.NotFound).and(jsonBody[ApiError]))
    .out(jsonBody[Task])

val listTasks: PublicEndpoint[Unit, Unit, List[Task], Any] =
  endpoint.get
    .in("tasks")
    .out(jsonBody[List[Task]])

val createTask: PublicEndpoint[NewTask, Unit, Task, Any] =
  endpoint.post
    .in("tasks")
    .in(jsonBody[NewTask])
    .out(statusCode(sttp.model.StatusCode.Created).and(jsonBody[Task]))

PublicEndpoint의 네 가지 타입 파라미터는 입력, 오류 출력, 성공 출력 및 필수 기능입니다. 아직 아무것도 실행되지 않습니다. 이것들은 순수한 설명이며, 바로 그렇기 때문에 동일한 값으로 서버, 클라이언트 및 문서를 생성할 수 있습니다.

몇몇 조합자가 대부분의 역할을 수행합니다. in은 경로 세그먼트, 쿼리 파라미터, 헤더 또는 본문과 같은 입력을 추가합니다. out은 성공 응답을 설명합니다. errorOut은 실패 응답을 설명하며, 단일 엔드포인트는 oneOf를 사용하여 여러 오류 사례를 모델링할 수 있습니다. 각 조합자는 엔드포인트의 타입을 좁히므로, 설명이 완료될 때쯤에는 해당 타입 시그니처가 계약에 대한 정확한 명세가 됩니다. 구현 코드 한 줄을 읽지 않고도 getTask의 시그니처를 읽고 이것이 Int를 받고, ApiError로 실패할 수 있으며, 그렇지 않으면 Task를 반환한다는 것을 알 수 있습니다.

이것이 Tapir의 실질적인 핵심입니다. 엔드포인트 설명은 컴파일러가 강제하는 문서입니다. 계약은 타입이기 때문에 팀원이 계약을 잘못 해석할 수 없으며, 계약을 위반하는 코드는 빌드되지 않습니다.

서버 로직 추가

설명은 serverLogic으로 로직을 연결할 때 작동하는 경로가 됩니다. 로직은 Future와 같은 이펙트 타입으로 래핑된 Either[ErrorType, SuccessType]를 반환하는 함수입니다. 간단한 인메모리 저장소는 예제를 자체 포함되게 유지합니다.

import scala.concurrent.Future
import scala.collection.concurrent.TrieMap
import sttp.tapir.server.ServerEndpoint

val store = TrieMap[Int, Task](
  1 -> Task(1, "Write the Tapir tutorial", done = false)
)

val getTaskServer: ServerEndpoint[Any, Future] =
  getTask.serverLogic { id =>
    Future.successful(
      store.get(id).toRight(ApiError(s"No task with id $id"))
    )
  }

val listTasksServer: ServerEndpoint[Any, Future] =
  listTasks.serverLogic(_ => Future.successful(Right(store.values.toList)))

val createTaskServer: ServerEndpoint[Any, Future] =
  createTask.serverLogic { newTask =>
    val id = store.size + 1
    val task = Task(id, newTask.title, done = false)
    store.put(id, task)
    Future.successful(Right(task))
  }

컴파일러는 여기서 계약을 강제합니다. getTaskApiError 타입의 오류를 약속하므로, toRightApiError를 제공해야 합니다. 잘못된 타입을 반환하면 코드가 컴파일되지 않습니다. HTTP 세부 정보, 404, JSON 인코딩은 이미 설명에 의해 처리됩니다.

Pekko 인터프리터를 사용하여 서버 엔드포인트를 실제 경로로 변환하고 서버를 시작합니다.

import sttp.tapir.server.pekkohttp.PekkoHttpServerInterpreter
import org.apache.pekko.http.scaladsl.Http
import org.apache.pekko.actor.typed.ActorSystem
import org.apache.pekko.actor.typed.scaladsl.Behaviors

implicit val system: ActorSystem[Any] = ActorSystem(Behaviors.empty, "task-api")
import system.executionContext

val routes = PekkoHttpServerInterpreter().toRoute(
  List(getTaskServer, listTasksServer, createTaskServer)
)

Http().newServerAt("localhost", 8080).bind(routes)

이제 API는 8080 포트에서 응답합니다. 단일 엔드포인트 설명을 건드리지 않고, 인터프리터라는 한 곳에서 프레임워크 관련 사항을 변경했습니다.

OpenAPI 문서 생성

엔드포인트가 값이기 때문에 OpenAPI 문서는 이 값들의 함수입니다. 주석이나 별도로 관리해야 할 사양 파일이 필요 없습니다.

import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter
import sttp.apispec.openapi.circe.yaml._

val docs = OpenAPIDocsInterpreter().toOpenAPI(
  List(getTask, listTasks, createTask),
  "Task API",
  "1.0"
)

println(docs.toYaml)

docs.toYaml은 완전한 OpenAPI 문서를 생성합니다. tapir-swagger-ui-bundle 모듈을 추가하면 Tapir는 서버에서 직접 대화형 Swagger UI를 제공합니다. 사양과 API는 동일한 엔드포인트 값에서 파생되므로, 사양은 API에 대해 거짓말을 할 수 없습니다.

이 OpenAPI 파일은 다른 도구로 연결되는 다리 역할도 합니다. Apidog로 가져오면 탐색 가능한 API 참조, 수동 확인을 위한 요청 콘솔 및 생성된 모의 서버를 모두 Scala 코드가 이미 생성한 사양을 벗어나지 않고 얻을 수 있습니다. 이는 Scala 서비스가 발전하는 동안 프론트엔드 팀원들에게 작업할 대상을 제공하는 실용적인 방법입니다.

여기서 주목할 만한 워크플로우가 있습니다. Tapir는 Scala 소스로부터 사양을 생성하므로 사양은 항상 최신 상태입니다. 이 사양을 사용하는 모의 도구는 실제 서버가 어디에도 배포되기 전에 프론트엔드에 작동하는 엔드포인트를 제공합니다. 계약은 한 방향으로 흐릅니다. Scala 타입이 계약을 정의하고, OpenAPI가 이를 전달하며, 모의 서버와 프론트엔드가 이를 사용합니다. 누구도 계약을 두 번 수동으로 작성하지 않으므로, 두 사본이 불일치할 일이 없습니다. 다른 팀이 의존하는 API를 제공하는 Scala 팀의 경우, 이것이 OpenAPI 내보내기를 빌드에 연결해야 하는 주요 이유입니다.

API 테스트

Tapir 엔드포인트는 두 가지 수준에서 테스트할 수 있습니다.

첫 번째 수준은 스텁 인터프리터를 사용하여 HTTP 없이 엔드포인트 로직을 확인합니다. 서버 엔드포인트로부터 테스트 백엔드를 구축하고 sttp 클라이언트를 통해 요청을 보냅니다. 포트가 바인딩되지 않기 때문에 빠릅니다.

import org.scalatest.flatspec.AsyncFlatSpec
import org.scalatest.matchers.should.Matchers
import sttp.client3._
import sttp.client3.testing.SttpBackendStub
import sttp.tapir.server.stub.TapirStubInterpreter
import sttp.client3.circe._
import io.circe.generic.auto._

class TaskApiSpec extends AsyncFlatSpec with Matchers {

  val backend = TapirStubInterpreter(SttpBackendStub.asynchronousFuture)
    .whenServerEndpointRunLogic(getTaskServer)
    .whenServerEndpointRunLogic(createTaskServer)
    .backend()

  "GET /tasks/1" should "return the seeded task" in {
    basicRequest
      .get(uri"http://test.com/tasks/1")
      .response(asJson[Task])
      .send(backend)
      .map { resp =>
        resp.code.code shouldBe 200
        resp.body.map(_.title) shouldBe Right("Write the Tapir tutorial")
      }
  }

  "GET /tasks/999" should "return a 404 with an error body" in {
    basicRequest
      .get(uri"http://test.com/tasks/999")
      .response(asJson[ApiError])
      .send(backend)
      .map { resp => resp.code.code shouldBe 404 }
  }
}

두 번째 수준은 전체 통합 테스트입니다. 실제 서버를 포트에 바인딩하고 HTTP 클라이언트로 호출합니다. 연결 상태를 확인하기 위해 이 방법을 아껴서 사용하고, 스텁 기반 테스트가 훨씬 빠르므로 대부분의 커버리지는 스텁 기반 테스트에 의존하세요. 빠르고 독립적인 확인과 얇은 실제 계층 사이의 분할은 자동화된 테스트 전반의 좋은 관행을 반영합니다.

Tapir 디자인의 유용한 속성은 다음과 같습니다. 엔드포인트 설명이 공유되므로, getTask에서 요청을 구축하는 테스트는 프로덕션 서버가 거부할 요청을 보낼 수 없습니다. 계약은 동일한 값에 의해 양쪽에서 강제됩니다. 수동 탐색과 모의 서버를 통해 해당 계약을 왕복하려면 Apidog를 다운로드하여 Tapir가 생성한 OpenAPI 파일을 가져오십시오.

자주 묻는 질문

Scala의 Tapir는 무엇인가요?

Tapir는 Typed API descRiptions의 약자로, HTTP 엔드포인트를 타입이 지정된 값으로 설명하는 Scala 라이브러리입니다. 하나의 설명으로부터 서버 경로, 클라이언트 및 OpenAPI 문서를 파생하므로, 계약이 한 번 정의되면 세 가지 모두에서 일관성을 유지합니다.

Tapir와 함께 Akka HTTP를 사용해야 하나요?

아니요. Tapir는 엔드포인트 설명을 서버 인터프리터와 분리합니다. 다른 인터프리터 모듈을 선택하여 Pekko HTTP, http4s, Netty, Vert.x 또는 기타 환경에서 동일한 엔드포인트를 실행할 수 있습니다. 설명은 변경되지 않습니다.

Tapir는 어떻게 OpenAPI 문서를 생성하나요?

Tapir의 OpenAPI 모듈은 엔드포인트 값을 읽어 완전한 OpenAPI 문서를 생성합니다. 문서와 서버가 모두 동일한 설명에서 파생되므로, 문서가 실제 API와 동기화되지 않을 일이 없습니다.

서버를 시작하지 않고 Tapir API를 테스트하는 방법은 무엇인가요?

Tapir 스텁 인터프리터를 사용하세요. 이는 서버 엔드포인트로부터 sttp 테스트 백엔드를 구축하므로, 포트를 바인딩하지 않고도 요청을 보내고 응답을 확인할 수 있습니다. 이 테스트는 빠르며, 실제 연결을 확인하기 위한 소수의 전체 통합 테스트를 유지합니다.

Tapir의 결과물을 다른 API 도구와 함께 사용할 수 있나요?

예. Tapir가 생성하는 OpenAPI 문서는 표준 사양입니다. 이를 Apidog와 같은 도구로 가져와 대화형 API 참조, 요청 콘솔 및 생성된 모의 서버를 얻을 수 있으며, 이는 프론트엔드 개발자와 API를 공유하는 데 유용합니다.

Apidog에서 API 설계-첫 번째 연습

API를 더 쉽게 구축하고 사용하는 방법을 발견하세요

Scala Tapir로 HTTP API 구축 및 테스트 방법