F#으로 웹서버 API 만들기

틈틈이 살펴 볼 예정입니다.

8개의 좋아요

1. 프로젝트 생성하기

 dotnet new web -lang f#  --no-https

프로젝트를 생성하면 매우 단순한 샘플 코드가 작성되어 있습니다.

open System
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.Hosting

[<EntryPoint>]
let main args =
    let builder = WebApplication.CreateBuilder(args)
    let app = builder.Build()

    app.MapGet("/", Func<string>(fun () -> "Hello World!")) |> ignore

    app.Run()

    0 // Exit code

F# 은 Delegate를 암시적으로 생성할 수 없습니다. 좀 더 코드를 줄여 본다면 다음과 같이 작성 할 수 있습니다.

#nowarn "0020"
...
app.MapGet("/", Func<_>(fun () -> "Hello World!")) 

하지만 동일한 C# 코드에 비해 장황해 지기 때문에 F# 커뮤니티는 오랜 기간 독립적인 웹 프레임워크를 구축했습니다.

asp.net core의 강력한 미들웨어 기능은 Giraffe 와 같은 거대한 커뮤니티 주도 프레임워크를 작성 할 수 있게 해 주었습니다. 다만 별개의 미들웨어로 개발된 Giraffe는 Asp.Net Core 버전 업데이트의 혜택을 누리기 어렵습니다.

Oxpecker 저자는 Giraffe의 유지 관리 상태에 대해 비판하였는데 오픈소스의 마지막을 지켜보는 거 같아 매우 안타깝네요,

2개의 좋아요

2. Computation Expressions 기반 웹서버 구성하기

dotnet add package FSharp.AspNetCore.WebAppBuilder

f# 개발자는 CE(계산표현식) 작성을 즐깁니다. 계산표현식은 C#의 linq 쿼리식가 유사하지만 장점이 더 많습니다. 개발자는 CE 내부에서 사용할 키워드를 만들 수 있습니다. 이는 키워드에 매우 제약을 두는 로슬린 컴파일러를 우회하여 DSL을 작성하는 방법을 열어줍니다.

기존 샘플 코드는 다음과 같이 리펙토링 할 수 있습니다.

open FSharp.AspNetCore.Builder

[<EntryPoint>]
let main _ =
    let app = webApp {
        get "/" (fun () -> "Hello World!")
    }

    app.Run()
    0 // Exit code

Asp.Net Core 의 Endpoint는 비동기로 작성 되어야 합니다.

일반적인 비동기CE와 Results 팩토리를 사용해서 코드를 수정해 봅니다.

open FSharp.AspNetCore.Builder
open Microsoft.AspNetCore.Http

[<EntryPoint>]
let main _ =
    let app = webApp {
        get "/" (fun () -> task {
            return Results.Ok("Hello World")
        })
    }

    app.Run()
    0 // Exit code

task CE는 F# 6에 추가되었으며 덕분에 C# API와 상호 호환성이 매우 좋아졌습니다. F# 6 이전 방식으로 작성한다면 다음과 같이 작성할 수 있습니다.

open FSharp.AspNetCore.Builder  
open Microsoft.AspNetCore.Http  

[<EntryPoint>]  
let main _ =  
   let app = webApp {  
       get "/" (fun () -> Async.StartAsTask <| async {  
           return Results.Ok("Hello World")  
       })  
   }  

   app.Run()  
   0 // Exit code

async CE를 사용하는 경우 퍼포먼스 차이가 발생합니다. F# 6 이후 task를 쓰는것이 올바른 작성 방법입니다.

3개의 좋아요

3. 외부 REST API 서비스 호출하기

외부 서비스를 호출하는 코드를 작성해 보겠습니다. 마이크로 서비스를 구성하면 API Gateway를 작성하게 되고 대부분 로직을 외부 서비스에 위임하여 처리하게 됩니다.

외부 서비스를 호출할 때 사용할 명명된 클라이언트를 DI로 준비합니다.

open Microsoft.Extensions.DependencyInjection
...

[<EntryPoint>]
let main _ =
    let app = webApp {
        services (fun services _ ->
            services.AddHttpClient("reqres", fun (http: HttpClient) ->
                http.BaseAddress <- Uri("https://reqres.in")
            ).AddAsKeyed()
        )
...

AddAsKeyed()는 .NET 9에 들어왔는데요. 사용하는게 유지 보수에 유리할 수 있습니다.

POST 호출을 다른 마이크로 서비스에 위임하는 코드를 추가해 보겠습니다.

open System.Net.Http.Json
...

[<EntryPoint>]
let main _ =
    let app = webApp {
        ...
        
        post "/api/v2/users" (fun (httpContext: HttpContext) -> backgroundTask {
            use httpClient = httpContext.RequestServices.GetRequiredKeyedService<HttpClient>("reqres")
            let! response = httpClient.PostAsJsonAsync("/api/users", {|
                Hello = "World"
            |})
            let! stream = response.Content.ReadAsStreamAsync()
            return Results.Stream(stream, contentType= $"{response.Content.Headers.ContentType}")
        })
    }

F#의 backgroundTaskBuilder는 호출하는 비동기 메소드를 ConfigureAwait(false)로 처리합니다. 따라서 UI Thread와 무관한 작업이라면 backgroundTask {}로 작성하는게 좋습니다.

api.http 파일을 만들어 쉽게 테스트해 볼 수 있습니다.

POST http://localhost:5013/api/v2/users
Content-Type: application/json

{}
{
  "hello": "World",
  "id": "359",
  "createdAt": "2025-04-02T11:13:30.828Z"
}
2개의 좋아요