도서/기술

[책] 객체에서 함수로 - 2장 함수로 HTTP 다루기

egg528 2025. 2. 23. 15:22

 

함수로서의 웹 서비스


 함수는 입력값을 바탕으로 결과값을 만들어낸다. 함수라고 하면 메서드만을 생각할 수 있지만 범위를 확장해보면 서비스 또한 Request라는 인자를 바탕으로 Response라는 결과를 만들어내는 거대한 함수로 생각할 수 있다. 결국 함수라는 관점에서 웹 서비스는 거대한 함수이고, 이 거대한 함수는 작은 함수들의 결합하여 완성된다.

 

 

 

간단한 요청 만들어보기


data class ToDoList(
    val listName: ListName,
    val items: List<ToDoItem>
)

data class ListName(
    val name: String
)

data class User(
    val name: String
)

data class ToDoItem(
    val description: String
)

enum class ToDoStatus {
    ToDo,
    InProgress,
    Done,
    Blocked
}

fun extractListData(request: Request): Pair<User, ListName> = TODO()

fun fetchListContent(listId: Pair<User, ListName>): ToDoList = TODO()

fun renderHtml(list: ToDoList): HtmlPage = TODO()

fun createResponse(html: HtmlPage): Response = TODO()

  간단한 시작으로 TODO 리스트를 달라는 Request를 받아 결과물이 담긴 Respose를 반환하는 함수를 만들어보자. 여기에 필요한 로직을 차례대로 생각해보면 이와 같이 4개의 함수 시그니처와 data class들을 추출할 수 있다. 위 코드를 도출하는 과정에서 중요한 점은 뚱뚱한 함수 하나보다 작은 여러 함수를  합성하는 관점을 가지고 입력/결과 관점에서 함수를 만들어 보는 것이다. 

 

 

fun getToDoList(request: Request): Response =
    createResponse(
        renderHtml(
            fetchListContent(
                extractListData(request)
            )
        )
    )

fun getToDoList(request: Request): Response =
    request
        .let(::extractListData)
        .let(::fetchListContent)
        .let(::renderHtml)
        .let(::createResponse)

 이렇게 구성한 4개의 함수를 활용해 getToDoList를 구성하는 방법에는 여럿이 있다. 하지만 더 나은 방법이 존재한다. 무엇이 더 나은 방법인지 왜 더 나은 방법인지는 설명하지 않아도 알아차릴 수 있을 것이다. 사실 위 코드를 보면 왜 ListName이나 User 클래스를 둔 걸까 하는 의문이 있었지만 결국 Pair를 사용할 때 더 명확한 의사 전달이 가능하고, Pair를 사용하기 때문에 let을 활용해 더 깔끔한 코드를 작성할 수 있었다. 

 

 

 

data class Zettai(val lists: Map<User, List<ToDoList>>) : HttpHandler {

	val routes = routes(
        "/todo/{user}/{list}" bind GET to ::showList
    )

    override fun invoke(req: Request): Response = routes(req)

    private fun showList(request: Request): Response =
        request
            .let(::extractListData)
            .let(::fetchListContent)
            .let(::renderHtml)
            .let(::createResponse)

    fun extractListData(request: Request): Pair<User, ListName> {
        val user = request.path("user").orEmpty()
        val listName = request.path("listName").orEmpty()
        if (user.isBlank() || listName.isBlank()) {
            return Pair("unknown", "unknown")
        }
        return Pair(user, listName)
    }

    fun fetchListContent(listId: Pair<User, ListName>): ToDoList =
        lists[listId.first]
            ?.firstOrNull { it.listName.name == listId.second }
            ?: ToDoList("unknown", emptyList())

    fun renderHtml(list: ToDoList): HtmlPage =
        HtmlPage(
            """
            <html>
            <head><title>${list.listName.name}</title></head>
            <body>${renderItems(list.items)}</body>
            </html>
            """.trimIndent()
        )

    fun renderItems(items: List<ToDoItem>) =
        items.joinToString("") { it.description }

    fun createResponse(html: HtmlPage): Response =
        Response(Status.OK).body(html.raw)
}


// 사용자 스토리
fun main() {
    val items = listOf("write chapter", "insert code", "draw diagrams")
    val toDoList = ToDoList(ListName("book"), items.map(::ToDoItem))
    val lists = mapOf(User("uberto") to listOf(toDoList))
    val app: HttpHandler = Zettai(lists)
    app.asServer(Jetty(8080)).start()
    println("Server started at http://localhost:8080/todo/uberto/book")
}

 이렇게 완성한 간단한 웹 서비스이자 함수 코드이다. 결국 이 웹 서비스는 (user, list) -> todo list html 시그니처의 함수와 동일하다. 그리고 이 함수는 작은 단위의 또 다른 함수들의 합성으로 이루어진다.