이펙티브 코틀린 Item 20: 일반적인 알고리즘을 반복해서 구현하지 마라

woga·2023년 6월 11일
0

코틀린 공부

목록 보기
23/54

많은 개발자는 같은 알고리즘을 여러 번 반복해서 구현한다. 여기서 말하는 알고리즘은 수학적 연산, 수집 처리처럼 별도의 모듈 또는 라이브러리로 분리할 수 있는 부분을 말한다.

복잡한 알고리즘도 있겠지만 아래처럼 숫자를 특정 범위에 맞추는 간단한 알고리즘도 있을 수 있다.

val percent = when {
    numberFromUser > 100 -> 200
    numberFromUser < 0 -> 0
    else -> numberFromUser
}

이 알고리즘은 stdlib의 coerceIn 확장 함수로 이미 존재한다. 그래서 따로 구현하지 않아도 된다.

val percent = numberFromUser.coerceIn(0, 100)

이렇게 의미 있는 것을 활용하면 단순하게 코드가 짧아진다는 것 이외에도 다양한 장점이 있다.

  • 코드 작성 속도가 빨라진다
  • 구현을 따로 읽지 않아도 함수명만 보고도 뭘 하는지 확실하게 알 수 있다
  • 직접 구현 시 발생할 수 있는 실수를 줄일 수 있다
  • 제작자들이 한 번만 최적화하면 이런 함수를 활용하는 모든 곳이 최적화의 혜택을 받을 수 있다

표준 라이브러리 살펴보기

일반적인 알고리즘은 대부분 다른 사람들이 이미 정의해 놨다. 대표적인 라이브러리는 표준 라이브러리인 stdlib이다.

stdlib은 확장 함수를 활용해서 만들어진 거대한 유틸리티 라이브러리다. stdlib의 함수들을 하나하나 보는 건 어려울 수 있지만 그럴만한 가치가 있는 일이다. 자세히 보지 않으면 계속 같은 함수를 만들게 될 것이다.

예를 들어 오픈소스 프로젝트에서 발췌한 다음 코드를 확인한다.

override fun saveCallResult(item: SourceResponse) {
    var sourceList = ArrayList<SourceEntry>()
    item.sources.forEach {
        var sourceEntity = SourceEntity()
        sourceEntity.id = it.id
        sourceEntity.category = it.category
        sourceEntity.country = it.country
        sourceEntity.description = it.description
        sourceList.add(sourceEntity)
    }
    db.insertSources(sourceList)
}

앞의 코드에서 forEach를 쓰는 건 사실 좋지 않다. 이런 코드는 반복문을 쓰는 것과 아무 차이가 없다.

현재 코드에선 어떤 자료형을 다른 자료형으로 매핑하는 처리를 한다. 따라서 map()을 쓰면 된다. 또한 현재 코드에선 SourceEntity를 설정하는 부분이 어설프다. 이는 코틀린으로 작성된 코드에선 더 이상 볼 수 없는 자바빈 패턴이다. 이런 형태보다는 팩토리 메서드를 활용하거나 기본 생성자를 활용하는 게 좋다.

그래도 위와 같은 패턴을 써야겠다면 최소한 apply를 활용해 모든 단일 객체들의 프로퍼티를 암묵적으로 설정하는 게 좋다.

override fun saveCallResult(item: SourceResponse) {
    val sourceEntities = item.sources.map(::sourceToEntry)
    db.insertSources(sourceList)
}

private fun sourceToEntry(source: Source) = SourceEntity()
    .apply {
        id = source.id
        category = source.category
        country = source.country
        description = source.description
    }

나만의 유틸리티 구현하기

상황에 따라 표준 라이브러리에 없는 알고리즘이 필요할 수 있다. 예를 들어 컬렉션에 있는 모든 숫자의 곱을 계산하는 라이브러리가 필요하다면 널리 알려진 추상화이므로 범용 유틸리티 함수로 정의하는 게 좋다.

fun Iterable<Int>.product() = fold(1) { acc, i -> acc * i }

여러 번 쓰이지 않아도 이렇게 만드는 게 좋다. product라는 이름이 숫자를 곱할 거라는 건 대부분 개발자들이 예측할 수 있기 때문이다. 이후 다른 개발자가 컬렉션의 숫자를 곱하는 함수를 만들어야 할 때 이렇게 이미 구현돼 있다면 기쁠 것이다.

같은 결과를 얻는 함수를 여러 번 만드는 건 잘못된 일이다. 모든 함수는 테스트돼야 하고 기억돼야 하며 유지보수돼야 한다. 따라서 함수를 만들 때는 이런 비용이 들어갈 수 있다는 것을 전제해야 한다.

따라서 필요 없는 함수를 중복해서 만들지 않게 기존에 관련된 함수가 있는지 탐색하는 과정이 필요하다. product도 확장 함수로 구현돼 있다.

많이 쓰이는 알고리즘을 추출하는 방법으론 톱레벨 함수, 프로퍼티 위임, 클래스 등이 있다. 확장 함수는 이런 방법들과 비교해서 아래와 같은 장점이 있다.

  • 함수는 상태를 유지하지 않으므로 행위를 나타내기 좋다

  • 톱레벨 함수와 비교해서 확장 함수는 구체적인 타입이 있는 객체에만 사용을 제한할 수 있어서 좋다

  • 수정할 객체를 아규먼트로 전달받아 쓰는 것보다는 확장 리시버로 사용하는 게 가독성 측면에서 좋다

  • 확장 함수는 객체에 정의한 함수보다 객체를 사용할 때 자동완성 기능 등으로 제안이 이뤄져 쉽게 찾을 수 있다

profile
와니와니와니와니 당근당근

0개의 댓글