본문 바로가기
🔓 영구 노트

코틀린 일급 컬렉션은 멋지지

by 파랭이가 룰루랄라 2023. 12. 14.

일급 컬렉션은 하나의 컬렉션만 wrapping 하고, 그 외 다른 멤버 변수가 없는 클래스를 의미합니다. 일급 컬렉션은 비즈니스 로직을 캡슐화하여 관리하고, 불변성을 보장하여 데이터의 안정성을 향상합니다.

개념적인 부분은 위와 같고, 예시 코드를 살펴보겠습니다.

// value는 1~13 값 저장, pattern은 스페이드, 클로버, 하트, 다이아몬드 저장
data class Card(val value: Value, val pattern: Pattern)

위와 같은 data class가 정의되어 있다고 할 때

// 일급 컬렉션 미적용
var deck: MutableList<Card> = mutableListOf()

// 일급 컬렉션 적용
data class Deck(
    private val cards: MutableList<Card>
) : List<Card> by cards {
    ...
}

일급 컬렉션 적용에 보이는 것 같이 collection을 wrapping 하면서 그 외 다른 멤버 변수를 없는 상태를 일급 컬렉션이라고 합니다.

wrapping을 하면서 얻는 이점은 크게 2가지가 있습니다.

  • 상태와 행위를 하나의 클래스에서 관리
  • collection의 불변성 보장

collection의 불변성 보장

kotlin에서는 불변성을 보장하기 위해 mutable prefix가 붙은 자료형들을 제공해 줍니다.

List, MutableList 인터페이스 차이

첨부한 사진에서 볼 수 있듯이 List는 조회와 관련된 함수들이 제공되고, MutableList는 추가, 삭제와 같이 데이터 수정 관련 함수를 제공하는 것으로 코틀린에서는 컬렉션에 대한 불변성을 보장해 줍니다.

 

컬렉션 레벨에서 불변성을 보장해도 데이터 수정은 필수적으로 필요한 경우가 있습니다. 다음 코드는 코틀린에서 일급 컬렉션이 어떻게 불변성을 보장하는지 보여주기 위한 예시입니다.

class GameManager {
    // 일급 컬렉션을 사용하지 않는 경우
    val deck1: MutableList<Card> = getCards()

    // 일급 컬렉션을 사용하는 경우
    val deck2: Deck = Deck(getCards())

 

위와 같이 일급 컬렉션을 사용하지 않고, 배열을 사용한 deck1과 일급 컬렉션을 사용한 deck2를 선언해 줍니다.

    deck1.add(Card(...))
    deck2.add(Card(...)) // Unresolved reference: add

 

deck1은 MutableList로 선언되어 있기 때문에 원하는 카드를 원하는 만큼 추가, 삭제할 수 있지만 deck2는 Deck 클래스 내부에서는 MutableList로 선언되어 사용되고, 외부로는 List를 확장한 함수만 제공하기 때문에 데이터 추가, 삭제를 할 수 없습니다.

 

이것이 코틀린 일급 컬렉션이 멋진 이유이기도 한데요. 단 1줄의 코드(List<Card> by cards)로 클래스 내부에서는 수정가능하지만 클래스 외부에서는 수정 불가능하도록 구현할 수 있기 때문입니다.

    private fun getCards(): MutableList<Card> {
        return Pattern.values().flatMap { pattern ->
                return@flatMap Value.values().map { value ->
                        return@map Card(value, pattern)
                    }
            }.toMutableList()
    }
}

 

위 함수는 Card를 초기화해 줍니다.

상태와 행위를 하나의 클래스에서 관리

상태와 행위를 관리할 수 있다는 것은 필요한 클래스에서 매번 구현하는 것이 아니라 일급 컬렉션만 선언하면 관리 포인트를 1곳으로 모을 수 있다는 것을 뜻합니다.

 

다음 코드는 Deck의 행위를 정의한 것입니다.

data class Deck(
    private val cards: MutableList<Card>
) : List<Card> by cards {

    fun shuffle(): List<Card> {
        cards.shuffle()
        return cards
    }

    fun drawCard(): Card {
        val first = cards.first()
        if (cards.remove(first)) {
            return first
        }
        throw RuntimeException("카드가 없습니다. $first")
    }
}

 

카드 섞기, 카드 뽑기와 같이 deck 클래스가 마땅히 수행해야 할 행위들을 클래스 내부에 구현되어 관리하고 있고, 추가적인 요구사항이 발생한다고 하더라도 다른 클래스 구현을 볼 필요 없이 함수만 추가적으로 구현해 주면 됩니다.

(관리 포인트가 1곳이기 때문에 가능한 것으로 동일한 구현이 산재되어 있다면 요구사항 추가가 단순 노동으로 변질될 수 있습니다)

 

현재 Deck 클래스에 저장된 cards는 내부에서는 MutableList이지만 외부에서는 List입니다. 이는 클래스 외부에서는 절대로 cards의 상태를 변경할 수 없다는 뜻이 됩니다.

마무리

코틀린에서 불변성을 보장하는 방법(List, MutableList)에 대해 간략하게 알아보고, 일급 컬렉션이 주는 이점에 대해 알아봤습니다.

 

이를 잘 활용하여 비즈니스 로직에서 중요하게 계산되어야 하는 데이터가 있다면 한번 생각하여 적용해 보면 좋을 것 같습니다.

댓글