본문 바로가기

코틀린

[Kotlin] mutable collection 사용 줄이기 (wordle 미션)

mutable collection 사용 줄이기

wordle에서 mutable collection의 사용을 지양하는 방향으로 리팩토링을 해보라는 피드백을 받았다. 우선 기존의 코드를 보자.

기존 코드 분석

코드를 보면 answerMap, result가 모두 mutable collection을 사용하고 있다. check()putTileIfSame() 외의 메서드는 생략했다.

 

class Words(private val values: List<Word>, today: LocalDate = LocalDate.now()) {

    private val answer: Word = findAnswer(today)
    private var answerMap: MutableMap<Char, Int> = mutableMapOf()

    fun check(word: Word): List<Tile> {
        answerMap = initAnswerMap()
        val result = MutableList(WORD_SIZE) { Tile.GRAY }

        repeat(WORD_SIZE) { putTileIfSame(result, word, it) }
        repeat(WORD_SIZE) { putTileIfContains(result, word, it) }
        return result
    }

    // 함수에서 MutableList인 result를 받아 값을 변경시키고 있다
    private fun putTileIfSame(result: MutableList<Tile>, word: Word, index: Int) {
        if (answer.isSameChar(word, index)) {
            calculateAnswerMap(word.value[index])
            result[index] = Tile.GREEN
        }
    }
}

 

로직을 간단히 설명하자면 check() 메서드에서 result 를 회색 타일로 채운 MutableList로 초기화한다. 그리고 putTileIfSame() 메서드를 호출하여 안에서 같은 문자인 경우 타일을 초록색으로 변경해주고 있다. result 내부의 값이 여러 메서드에서 변경되고 있는 것이다.

왜 mutable collection의 사용을 줄여야 할까?

코틀린은 ‘함수형 프로그래밍'을 지원하는 언어이기 때문이다.

함수형 프로그래밍의 핵심 개념

  • 함수가 일급 시민(first-class)이다.
  • 불변 객체를 사용해 프로그램을 작성한다.
  • 부수 효과(side effect)가 없다.
    • 입력이 같으면 항상 출력도 같다.
    • 객체의 상태를 변경하지 않는다.
    • 함수 외부나 다른 바깥 환경과 상호작용하지 않는 순수 함수를 사용한다.

함수형 프로그래밍의 이점

  • 명령형 코드에 비해 간결하다. 강력한 추상화를 통해 코드 중복을 줄일 수 있다.
  • 불변 데이터 구조를 사용하고 순수 함수를 그 데이터 구조에 적용하면 다중 스레드를 사용해도 안전하다.
  • 부수 효과가 없으므로 준비 코드 없이 독립적으로 테스트할 수 있다.

현재 코드는 가변 객체를 사용하며, mutable collection을 함수 내에서 변경하고 있다. 부수 효과를 줄이고 함수형 프로그래밍의 이점을 얻기 위해서는 현재 코드에서 mutable collection의 사용을 줄여야 한다.

적용

우선적으로 MutableList를 제거해보기로 했다. 이를 해결하기 위해 putTileIfSame()에서 mapIndexed를 통해 새로운 리스트를 반환하는 방향으로 리팩토링을 진행했다. 리팩토링한 코드는 아래와 같다.

 

class Words(private val values: List<Word>, today: LocalDate = LocalDate.now()) {

    private val answer: Word = findAnswer(today)
    private var answerMap: MutableMap<Char, Int> = mutableMapOf()

    fun check(word: Word): List<Tile> {
        answerMap = initAnswerMap().toMutableMap()
        return DEFAULT_RESULT.markGreen(word).markYellow(word)
    }

    private fun List<Tile>.markGreen(word: Word): List<Tile> {
        return mapIndexed { index, tile -> existOrGreen(tile, word, index) }
    }

    private fun existOrGreen(tile: Tile, word: Word, index: Int): Tile {
        return if (answer.isSameChar(word, index)) {
            calculateAnswerMap(word.value[index])
            Tile.GREEN
        } else {
            tile
        }
    }

    companion object {
        private const val WORD_SIZE = 5
        private val DEFAULT_RESULT = List(WORD_SIZE) { Tile.GRAY }
    }
}

 

 

먼저 putTileIfSame()을 확장함수로 변경하고, 그에 따라 메서드 이름도 markGreen()으로 변경했다. 그리고 mapIndexed를 통해 수신 객체의 index와 tile을 받아 existOrGreen()를 호출한다. existOrGreen() 함수로 알맞은 타일을 가져와 새로운 리스트로 반환한다.

리팩토링 결과 MutableList가 아닌 불변 객체 List 사용함으로써 함수 내부에서 객체의 상태를 변경하지 않아 부수 효과를 없앨 수 있었다.