프로젝트에서 이미 있던 코드를 복사해서 붙여넣고 있다면, 무언가가 잘못된 것이다.

-이펙티브 코틀린 저자-

Knowledge를 반복하여 사용하지 말라

여기서 언급하는 knowledge는 일반적으로 표현하는 ‘지식’과 약간 다르게 ‘의도적인 정보’를 나타내는 개념으로, <실용주의 프로그래머> 라는 책에서 언급된다. 이 책에서는 ‘Don’t Repat Yourself’라는 규칙을 ‘DRY 규칙’이라고 표현한다. 또는 WET 안티패턴이라고도 한다.

프로그램에서 중요한 knowledge를 크게 두가지 뽑는다면 다음과 같다

  1. 로직: 프로그램이 어떠한 식으로 동작하는지, 어떻게 보이는 지
  2. 공통 알고리즘: 원하는 동작을 하기 위한 알고리즘

둘의 가장 큰 차이점은 시간에 따른 변화로 로직은 시간이 지나면서 계속 변하지만, 공통 알고리즘은 한 번 정의된 이후 크게 변하지 않는다는 점이다.

모든 것은 변화하기 마련이고, 개발자는 이를 대비해야 한다. 따라서 공통 knowledge가 있다면, 이를 추출해서 이러한 변화에 대비해야한다. 하지만 극단적으로 비슷해보이는 코드를 모두 추출하려고 하는 것은 좋지 않다. 언제나 균형이 중요하며, 이에 대한 것은 수많은 시간과 연습이 필요하다.

일반적인 알고리즘을 반복해서 구현하지 않기

같은 알고리즘을 여러 번 반복해서 구현하지 않는 것이 중요하다. 왜냐하면 단순하게 코드가 짧아지는 것 이외에도 다음과 같은 장점을 갖기 때문이다

  • 코드 작성 속도가 빨라짐
  • 함수 이름만 보고 무엇을 하는지 확실하게 알 수 있음(=가독성)
  • 실수를 줄일 수 있음
  • 최적화 시 이를 사용하는 코드 전역 모두에 최적화 혜택을 받음

일반적인 구현은 표준 라이브러리(stdlib)에 구현이 되어있으므로 이를 활용하는 것이 좋다. 그 이외의 경우는 확장 함수 등을 사용하여 자신만의 유틸 클래스를 만들어 관리하는 것이 좋다.

일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들기

델리게이션 패턴

소프트웨어 공학에서 델리게이션 패턴은 객체지향 설계 패턴으로 상속과 같은 같은 코드의 재사용을 위해 객체를 조합하는 것을 말한다.

델리게이션 패턴 예제코드를 살펴보자

class Rectangle(val width: Int, val height: Int) {
    fun area() = width * height
}

class Window(val bounds: Rectangle) {
    // Delegation
    fun area() = bounds.area()
}

Window 클래스의 area 함수 구현은 내부적으로 Rectangle 객체의 area() 것으로 위임(델리게이션)한 것을 확인할 수 있다.

코틀린에서는 델리게이션 패턴을 위한 by라는 특별한 문법을 지원한다. 예제코드를 살펴보자.

interface ClosedShape {
    fun area(): Int
}

class Rectangle(val width: Int, val height: Int) : ClosedShape {
    override fun area() = width * height
}

class Window(private val bounds: ClosedShape) : ClosedShape by bounds

Window 클래스 정의에 사용된 by절은 bounds 프로퍼티를 Window에 내부적으로 저장하고, 컴파일러는 bounds가 갖는 ClosedShape의 모든 메서드들을 자동으로 생성하게 된다.

fun main() {
    val rectangle = Rectangle(100,200)
    println(Window(rectangle).area()) // 20000 출력
}

코틀린에서는 다음과 같이 by절을 이용한 표준 델리게이션 패턴을 제공한다. 자세한 내용은 공식문서의 delegated properties 항목을 살펴보자.

  • lazy 프로퍼티
  • observable 프로퍼티
  • vetoable 프로퍼티
  • notNull

lazy 프로퍼티

코틀린의 stdlib는 lazy 프로퍼티 패턴을 쉽게 구현할 수 있게 lazy 함수를 제공한다.

val value by lazy { createValue() }

기본적으로는 스레드에 안전하게 동작하나, 동시에 여러 스레드에서 접근하여 가장 먼저 초기화 되는 값을 사용하고 싶다면 LazyThreadSafetyMode.PUBLICATION을 lazy() 함수의 인자로 넣으면 된다.

observable 프로퍼티

프로퍼티 위임을 사용하면, 프로퍼티에 변화가 있을 때 이를 감지하는 observable 패턴도 쉽게 만들 수 있다.

var items: List<Item> by Delegates.observable(listOf()){ _, _, _ ->
    adapter.notifyDataSetChanged()
}

이렇게 코드를 작성하면 일반적으로 데이터의 변화가 감지할 때 RecyclerView의 항목들을 갱신할 수 있다.

vetoable 프로퍼티

프로퍼티가 변경되는 경우 특정 로직을 수행하여 조건을 충족하지 못하면 프로퍼티의 변경을 취소할 수 있다.

20대의 젊음을 그리워하며, 나이(age)를 먹지 않는 예제코드를 만들었다.


var age by Delegates.vetoable(27){ property, oldValue, newValue ->
    newValue < 30
}

27살+5살 = 29살
repeat(5){
    println("age = $age")
    age++
}
// 결과
// age = 27
// age = 28
// age = 29
// age = 29
// age = 29

notNull 프로퍼티

notNull은 일시적으로 프로퍼티가 초기화 되기 전까지 null 상태를 허용하는 패턴이다. 예제를 살펴보자

var value by Delegates.notNull<Int>()
// value = 30 
println(value) // 초기화 하지 않고 접근 시 IllegalStateException 발생

lateinit과 비교하면 다음과 같은 차이점을 갖는다.

  • notNull 사용시에는 각 프로퍼티 별로 추가적인 객체를 생성하게 된다.
  • 이 객체는 작지만, 많은 프로퍼티 선언시에는 주의해야 한다.
  • Dagger와 같이 자바 필드의 타입을 보고 외부 injection을 하는 경우 함께 쓰기 어렵다.
  • lateinit 사용은 비용이 적지만, 원시 타입에는 사용할 수 없다. Delegates.notNull은 원시타입도 선언 가능하다.

일반적인 알고리즘을 구현할 때 제네릭을 사용하기

타입 파라미터를 사용하면 함수에 타입을 전달할 수 있는데, 이를 제네릭 함수라고 부른다. 타입 파라미터는 컴파일러에 타입과 관련된 정보를 제공하여 컴파일러가 타입을 조금 더 정확하게 추측할 수 있도록 한다. 따라서 프로그램이 더 안전해지고, 안드로이드 스튜디오와 같은 IDE도 이를 기반으로 여러가지 유용한 제안(Suggestion)을 해주므로 개발자는 프로그래밍이 편리해진다.

제네릭은 일반적으로 <T>와 같이 표현하지만, <T:CharSequence> 로 사용하면 T는 CharSequence의 서브타입만 지정 가능하다.

fun <T:CharSequence> sayHello(name:T){
    println("Hi, $name")
}

val name:String = "Charles"
sayHello(name) // Hi, Charles

sayHello(100) // 컴파일 오류

다음과 같이 특정 타입 조건으로 제한을 걸 수도 있다.

fun <T:CharSequence> sayHello(name:T) where T:StringBuilder{ // 조건은 복수개 지정 가능
    println("Hi, $name")
}

val name:String = "Charles"
sayHello(name) // 컴파일 오류 

val builder:StringBuilder = StringBuilder("Charles")
sayHello(builder) // Hi, Charles

타입 파라미터의 섀도잉을 피하기

class Forest(val name: String) {
    fun addTree(name: String) {...}
}

위 코드처럼 프로퍼티와 파라미터(name)가 같은 이름을 가질 수 있다. 이를 섀도잉(shadowing)이라고 한다. 섀도잉이 발생한 코드는 동작에는 문제가 없으나 이해하기 어려우므로 섀도잉이 발생하지 않도록 주의해야 한다.

제네릭 타입과 variance 한정자를 활용하기

variance 한정자의 이해

  • 타입 파라미터는 기본적으로 불변성(한정자 지정 없음)이다.
  • List 및 Set의 타입 파라미터는 공변성(out 한정자)이다.
  • Map에서 값의 타입을 나타내는 타입 파라미터는 공변성(out 한정자) 이다.
  • Array, MutableList, MutableSet, MutableMap의 타입 파라미터는 불변성(한정자 지정 없음)이다.
  • 함수 타입의 파라미터 타입은 반변성(in 한정자)이다. 반환 타입은 공변성(out 한정자)이다.
  • 반환만 되는 타입에는 공변성(out 한정자)를 사용한다.
  • 허용만 되는 타입에는 반변성(in 한정자)를 사용한다.

공통 모듈 추출 및 재사용

모듈화를 통해 여러 플랫폼에서 코드를 재사용하자


후원하기

카테고리: Kotlin

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.