목차

공부/Kotlin

일급 컬렉션 (First Class Collection)

천만일 2023. 11. 23. 21:15

NextStep의 TDD, 클린 코드 with Kotlin을 수강하며 일급 컬렉션에 대한 내용을 접했습니다.

적용하는 방법은 간단하지만, 개념은 간단하지 않아서 흥미로웠습니다.

일급 컬렉션??

먼저 컬렉션에 대해 알아볼까요?

컬렉션(Collection)은 ADT의 일종으로 데이터의 개수가 변할 수 있는 데이터의 집합입니다.

우리가 흔히 사용하는 List, Set, Map 등이 포함됩니다.

 

그렇다면 일급 컬렉션은 무엇일까요?

일급 컬렉션이란 컬렉션 이외에 다른 멤버 변수를 가지지 않는 클래스를 말합니다.

컬렉션을 한번 더 랩핑했다고 이해해도 무방할 것 같습니다.

다음은 일급 컬렉션을 적용한 예시입니다.

data class Todo(private val title: String, val isDone: Boolean)

class TodoList(private val todoList: List<Todo>)

 

다음은 제가 직접 경험한 일급 컬렉션의 장점에 대해 하나씩 알아보겠습니다.

컬렉션에 도메인 로직을 결합시키기 용이하다.

위에서 일급 컬렉션을 적용한 예시인 TodoList를 통해서 알아보겠습니다.

만약 완료된 Todo만을 조회하기 위해서는 어떻게 해야 할까요?

 

List와 같은 컬렉션 타입으로 데이터를 불러온 후에 처리 과정을 거쳐야 합니다.

개발자의 취향에 따라서, 처리 로직을 불러오기 전에 실행할 수도 있겠죠.

이처럼 List 타입의 변수를 사용한다면 List 타입을 처리해야 하는 곳곳에 컬렉션을 용도에 맞게 처리하는 로직이 작성될 것입니다.

아래처럼 단순히 완료된 것일 수도 있고, 더욱 복잡할 수도 있습니다.

List<Todo> todoList = listOf(
    Todo("a", false),
    Todo("b", true),
    Todo("c", false),
    Todo("d", true),
)

todoList.filter {
    it.isDone
}

 

 

일급 컬렉션을 사용하면 이처럼 도메인 로직에 대한 캡슐화를 할 수 있습니다.

List<Todo> 타입을 일급 컬렉션으로 리팩터링 하면 다음처럼 변경됩니다.

 

이제는 완료된 Todo를 불러오는 로직이 getDoneTodoList()로 추상화되었습니다.

이는 fliter 문이 여러 곳에 흩어지지 않고, TodoList 클래스 내부에 있어 코드의 이해도를 높여줍니다.

data class Todo(private val title: String, val isDone: Boolean)

class TodoList(private val todoList: List<Todo>) {
    fun getDoneTodoList() = todoList.filter {
        it.isDone
    }
}

 

이처럼 List<Todo>를 일급 컬렉션인 TodoList로 리팩터링 하면서, List<Todo>객체를 조작하는 것이 아닌 TodoList 객체에 메시지를 전달하여 로직을 실행하는 방식으로 개선되었습니다.

포장 클래스에 이름을 부여할 수 있다

다음 예시에서 getDoneTodoList()의 타입은 List<Todo>입니다.

분명 해당 값은 완료된 Todo의 모음인데, List 타입으로는 알 수 없기에 변수명을 통해 의미를 표현해야 합니다.

data class Todo(private val title: String, val isDone: Boolean)

fun getDoneTodoList():List<Todo> = listOf(
    Todo("a", false),
    Todo("b", true),
    Todo("c", false),
    Todo("d", true),
).filter {
    it.isDone
}

val doneTodoList = getDoneTodoList()

변수명은 개발자의 취향이 반영됩니다.

만약 완료된 Todo의 모음이 재사용되는 값이라면, 동일한 의미의 값임에도 다양한 변수명으로 선언될 수 있습니다.

이러한 경우 포장하게 되면 클래스명으로 의미를 표현할 수 있고 검색에도 용이합니다.

data class Todo(private val title: String, val isDone: Boolean)

class TodoList(private val todoList: List<Todo>) {
    fun getDoneTodoList() = DoneTodoList(
        todoList.filter {
            it.isDone
        }
    )
}

class DoneTodoList(private val todoList: List<Todo>)

fun test() {
    val doneTodoList: DoneTodoList = TodoList(
        listOf(
            Todo("a", false),
            Todo("b", true),
            Todo("c", false),
            Todo("d", true),
        )
    ).getDoneTodoList()
}

근데 불변은..?

일급 컬렉션을 사용할 때 유의할 점이 있습니다.

대부분의 현대 언어들은 객체를 인자로 넘길 때 참조를 넘깁니다.

따라서 다음과 같은 문제가 생길 수 있습니다.

test("불변 확실하니?") {
    val todo = arrayListOf<Todo>(
        Todo("a", false),
        Todo("b", true),
        Todo("c", false),
        Todo("d", true),
    )

    val todoList = TodoList(todo)

    todo.add(Todo("E", false))

    todoList.getCount() shouldBe 4 // expected:<4> but was:<5>
}

 

바로 가변 컬렉션을 일급 컬렉션에 넘기는 케이스입니다.

이 경우, 불변 컬렉션만 넘기도록 약속을 하는 방법도 있겠지만, 애초에 조금 더 명확하게 하는 것이 좋겠죠?

 

다음과 같이 인자로 받아온 컬렉션을 다시 한번 불변 컬렉션으로 감싸주는 방법이 있겠습니다.

변경된 TodoList로는 위의 테스트가 성공하는 것을 알 수 있습니다.

class TodoList(todoList: List<Todo>) {
    private val todoList: List<Todo>

    init {
        this.todoList = todoList.toList()
    }

    fun getCount() = todoList.count()
}

 

 

다음 글에서는 일급 컬렉션을 선언할 때 사용할 수 있는 코틀린 키워드를 알아보겠습니다!

'공부 > Kotlin' 카테고리의 다른 글

Kotlin의 data class  (1) 2023.11.03