함수로서의 웹 서비스
함수는 입력값을 바탕으로 결과값을 만들어낸다. 함수라고 하면 메서드만을 생각할 수 있지만 범위를 확장해보면 서비스 또한 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 시그니처의 함수와 동일하다. 그리고 이 함수는 작은 단위의 또 다른 함수들의 합성으로 이루어진다.
'도서 > 기술' 카테고리의 다른 글
[책] 객체에서 함수로 - 3장 도메인 정의 및 테스트 (0) | 2025.03.02 |
---|---|
[책] 객체에서 함수로 - 1장 애플리케이션 준비하기 (0) | 2025.02.16 |
[책] 데이터 지향 프로그래밍 - 15장 디버깅 (0) | 2025.02.09 |
[책] 데이터 지향 프로그래밍 - 14장 고급 데이터 유효성 확인 (1) | 2025.02.09 |
[책] 데이터 지향 프로그래밍 - 13장 다형성 (0) | 2025.02.09 |