반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

[이펙티브 코틀린] Item20. 일반적인 알고리즘을 반복해서 구현하지 말라 본문

Kotlin/이펙티브 코틀린

[이펙티브 코틀린] Item20. 일반적인 알고리즘을 반복해서 구현하지 말라

어리둥절범고래 2023. 11. 5. 17:14
반응형

많은 개발자는 같은 알고리즘을 여러 번 반복해서 구현합니다.

여기서 말하는 알고리즘은 비즈니스 로직을 포함하는 특정 프로젝트에 국한된 것이 아닌, 수학적인 연산, 수집 처리처럼 별도의 모듈 또는 라이브러리로 분리할 수 있는 부분을 의미합니다.

예를 들어 Iterable.sorted와 같이 이미 있는 것을 활용하면, 단순하게 코드가 짧아진다는 것 이외에도 다양한 장점이 있습니다.

 

  • 코드 작성 속도가 빨라진다. 호출을 한 번 하는 것이 알고리즘을 만드는 것보다 빠르다.
  • 구현을 따로 읽지 않아도, 함수의 이름 등만 보고도 무엇을 하는지 확실하게 알 수 있다.
  • 직접 구현할 때 발생할 수 있는 실수를 줄일 수 있다.
  • 제작자들이 한 번만 최적화하면, 이러한 함수를 활용하는 모든 곳에 최적화의 헤택을 받는다.

 

표준 라이브러리 살펴보기

 

일반적인 알고리즘은 대부분 이미 다른 사람들이 정의해 놓았습니다.

가장 대표적인 라이브러리는 표준 라이브러리인 stdlib입니다.

stdlib는 확장 함수를 활용해서 만들어진 굉장히 거대한 유틸리티 라이브러리입니다.

 

오픈 소스 프로젝트에서 일부 발췌한 다음 코드를 살펴봅시다.

override fun saveCallResult(item: SourceResponse) {
    var sourceList = ArrayList<SourceEntity>()
    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(sourcesList)
}

앞의 코드에서 forEach를 사용하는 것은 사실 좋지 않으며, for 반복문을 사용하는 것과 아무런 차이가 없습니다.

또한 코틀린으로 작성된 코드에서 더 이상 찾아볼 수 없는 자바빈(JavaBean)패턴입니다.

이러한 형태보다는 팩토리 메서드를 활용하거나, 기본 생성자를 활용하는 것이 좋습니다.
그래도 위와 같은 패턴을 써야겠다면, 다음과 같이 최소한 apply를 활용해서 모든 단일 객체들의 프로퍼티를 암묵적으로 설정하는 것이 좋습니다.

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

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

 

 

 

나만의 유틸리티 구현하기

 

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

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

이는 잘 알려진 수학적 개념이고, product라는 이름이 숫자를 곱할 거라는 것은 대부분의 개발자들이 예측할 수 있습니다.
동일한 결과를 얻는 함수를 여러 번 만드는 것은 잘못된 일입니다.
모든 함수는 테스트되어야 하고, 기억되어야 하며, 유지보수되어야 한다. 따라서 함수를 만들 때에는 이러한 비용이 들어갈 수 있다는 것을 전제해야 합니다.

따라서 필요 없는 함수를 중복해서 만들지 않게, 기존에 관련된 함수가 있는지 탐색하는 과정이 필요합니다.


많이 사용되는 알고리즘을 추출하는 방법으로는 톱레벨 함수, 프로퍼티 위임, 클래스 등이 있습니다.
확장함수는 이러한 방법들에 비교해서 다음과 같은 장점을 갖고 있습니다.

  • 함수는 상태를 유지하지 않으므로, 행위를 나타내기 좋다. 특히 부가 작용(side-effect)이 없는 경우에 더 좋다.
  • 톱레벨 함수와 비교해서, 확장 함수는 구체적인 타입이 있는 객체에만 사용을 제한할 수 있다.
  • 수정할 객체를 아규먼트로 전달받아 사용하는 것보다는 확장 리시버로 사용하는 것이 가독성 측면에서 좋다.
  • 확장 함수는 객체에 정의한 함수보다 객체를 사용할 때, 자동 완성 기능 등으로 제안이 이루어지므로 쉽게 찾을 수 있다.

 

 


정리

 

  • 일반적인 알고리즘을 반복해서 만들지 마라. 대부분 stdlib에 이미 정의되어 있을 가능성이 높다. 따라서 stdlib를 공부할 필요성이 있다.
  • stdlib에 없는 일반적인 알고리즘이 필요하거나, 특정 알고리즘을 반복해서 사용해야 하는 경우에는 프로젝트 내부에 직접 정의하라. 일반적으로 이런 알고리즘들은 확장 함수로 정의하는 것이 좋다.
반응형