Developing Myself Everyday

아이템 19 - knowledge를 반복해서 사용하지 말라


프로그래밍의 가장 큰 규칙은 아래와 같습니다

 

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

 

 

Knowledge

프로그래밍에서 knowledge는 넓은 의미로 '의도적인 정보'를 뜻합니다. 알고리즘의 작동 방식, UI의 형태, 우리가 원하는 결과등이 모두 '의도적인 정보'입니다.

 

그 중 2가지를 뽑자면 아래와 같습니다.

  1. 비즈니스 Logic: 프로그램이 어떠한 식으로 동작하는지와 프로그램이 어떻게 보이는지
  2. 공통된 알고리즘: 원하는 동작을 하기 위한 알고리즘

둘의 가장 큰 차이점은 시간에 따른 변화입니다. 로직은 시간이 지나면서 변하지만, 알고리즘은 크게 변하지 않습니다.

 

knowledge가 반복되면 확장성을 막고, 쉽게 깨지게 만듭니다.

 

 

언제 코드를 반복해도 될까?

공통되는 코드를 추출하여 반복을 줄일 수 있지만, 모습은 유사해도 실질적으로 다른 knowledge를 나타내는 경우에는 추출하면 안됩니다.

 

잘못된 코드 추출로부터 보호하기 위해서는 단일 책임 원칙을 지키면 됩니다.

 

 

단일 책임 원칙(Single Responsibilty Principle. SRP)

SOLID중 하나인 SRP는 `클래스를 변경하는 이유는 단 한 가지여야 한다`는 의미 입니다. 이는 두 액터(변화를 만드는 존재)가 같은 클래스를 변경하는 일은 없어야 한다는 것입니다.

 

SRP는 두 가지 사실을 알려줍니다.

  • 서로 다른 곳에서 사용하는 knowledge는 독립적으로 변경할 가능성이 많으므로, 완전히 다른 knowledge는 별도로 취급하는 것이 좋습니다.
  • 다른 knowledge는 분리해 두는 것이 좋습니다. 그렇지 않으면, 재사용해서는 안 되는 부분을 재사용하려는 유혹이 발생할 수 있습니다.

 

 

 

아이템 20 - 일반적인 알고리즘을 반복해서 구현하지 말라


수학적인 연산, 수학 처리가 별도의 모듈이나 라이브러리에 존재한다면 이를 반복해서 구현하는 것은 불필요합니다.

 

그러므로 유틸리티 라이브러리들을 하나하나 살펴보는 것은 어려울 수 있지만, 매우 가치가 있는 일입니다.

 

상황에 따라서 표준 라이브러리에 없는 알고리즘이 필요한 경우에는, 이를 정의하는 것이 좋습니다. 이때는 여러번 사용되지 않더라도 정의하는 것이 좋습니다.

 

정리하면, 일반적인 알고리즘은 대부분 stdlib에 이미 정의되어 있으므로 이를 공부하고 반복해서 만들지 않는 것이 좋겠습니다.

 

 

 

아이템 21 - 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라


프로퍼티 위임

프로퍼티 위임은 프로퍼티의 접근자(getter, setter) 구현을 다른 객체에게 위임하는 방식입니다. 이를 통해 반복적으로 사용되는 프로퍼티의 행위를 추출해서 재사용할 수 있습니다.


예를 들어 프로퍼티를 get할 때마다 반복적으로 특정 행위를 수행해야 한다면, getter에 해당하는 동작을 다른 객체에게 위임하여 대신 구현하는 방식으로 여러 프로퍼티에 공통적으로 적용할 수 있습니다.

 

대표적인 예로는 지연 프로퍼티가 있습니다.

val value by lazy { createValue() }

 

lazy 함수는 해당 프로퍼티를 처음 접근하려는 요청이 들어올 때 초기화를 수행하며, 이후에는 초기화된 값을 반환하는 동작을 제공합니다. 이 동작은 lazy 함수에 의해 생성된 객체가 프로퍼티의 getter에 해당하는 동작을 위임받아서 대신 구현함으로써 이루어 집니다.

 

 

간단한 프로퍼티 델리게이트

어떻게 이런 코드가 가능한지 알아보기 위해 간단한 프로퍼티 델리게이트를 만들어 보겠습니다. 예를 들어 일부 프로퍼티가 사용될 때, 간단한 로그를 출력하고 싶다고 가정해 보겠습니다.

 

아래의 프로퍼티는 타입이 다르지만, 내부적으로 동일한 처리를 합니다.

var token: String? = null
	get() {
    	print("token returned value $field")
        return field
    }
    set(value) {
    	print("token changed from $field to $value")
        field = value
    }

var attempts: String? = null
	get() {
    	print("attempts returned value $field")
        return field
    }
    set(value) {
    	print("attempts changed from $field to $value")
        field = value
    }

 

 

아래의 객체는 `getValue()`로 프로퍼티의 getter를 `setValue()`로 프로퍼티의 setter를 만듭니다. 객체를 만든 뒤에는 by 키워드를 사용하여 연결해주면 됩니다.

var token: String? by LoggingProperty(null)
var attempts: Int by LoggingProperty(0)

private class LoggingProperty<T>(var value: T) {
    operator fun getValue(
        thisRef: Any?,
        prop: KProperty<*>
    ): T {
        print("${prop.name} returned value $value")
        return value
    }

    operator fun setValue(
        thisRef: Any?,
        prop: KPRoperty<*>,
        newValue: T
    ) {
        val name = prop.name
        print("name changed from $value to $newValue")
        value = newValue
    }
}

 

 

by는 아래와 같이 컴파일 됩니다. 

@JvmField
private val 'token$delegate' = LoggingProperty<String>(null)
var token: String?
    get() = 'token$delegate'.getValue(this, ::token)
    set(value) {
        'token$delegate'.setValue(this, ::token, value)
    }

 

위의 코드를 보면 알 수 있는 것은, getValue와 setValue는 단순히 값만 처리하게 바뀌는 것이 아니라, 컨텍스트(this)와 프로퍼티 참조도 함께 사용하는 형태로 바뀝니다.

 

위임 객체는 컨텍스트(this)를 통해 프로퍼티를 소유한 객체에 접근할 수 있고, 프로퍼티 참조는 프로퍼티에 대한 정보를 제공합니다.

 

이러한 특성 덕분에 아래와 같이 getValue 메서드가 여러개 있더라도 컨텍스트를 활용하므로, 상황에 따라 적절한 메서드가 선택됩니다.

class SwipeRefreshBinderDelegate(val id: Int) {
    private var cache: SwipeRefreshLayout? = null

    operator fun getValue(
        activity: Activity,
        prop: KProperty<*>,
    ): SwipeRefreshLayout {
    return cache?: activity
        .findViewById<SwipeRefreshLayout>(id)
        .also { cache = it }
    }

    operator fun getValue(
        fragment: Fragment,
        prop: KProperty<*>
    ): SwipeRefreshLayout {
        return cache?: fragment.view
        .findViewById<SwipeRefreshLayout>(id)
        .also { cache = it }
    }
}

 

 

Map과 확장함수를 이용한 프로퍼티 위임

Map는 아래와 같이 확장 함수가 정의되어 있어서 이를 활용할 수도 있습니다.

inline operator fun <V, V1 : V> Map<in String, V>.getValue(
    thisRef: Any?,
    property: KProperty<*>
): V1 = getOrImplicitDefault(property.name) as V1
val map: Map<String, Any> = mapOf(
    "name" to "Marcin",
    "kotlinProgrammer" to true
)
val name by map
println(name) // 출력: Marcin

 

 

자주 사용되는 프로퍼티 델리게이터

코틀린 stdilb에서 자주 사용되는 프로퍼티 델리게이터는 아래와 같습니다.

 

- lazy

- Delegates.observable

프로퍼티 값이 변경될 때마다 콜백을 호출합니다. 

import kotlin.properties.Delegates

var name: String by Delegates.observable("Unknown") { property, oldValue, newValue ->
    println("${property.name} 변경: $oldValue -> $newValue")
}

fun main() {
    name = "Alice"  // 출력: name 변경: Unknown -> Alice
    name = "Bob"    // 출력: name 변경: Alice -> Bob
}

 

- Delegates.vetoable

프로퍼티 값이 변경되기 전에 조건을 검사합니다.

import kotlin.properties.Delegates

var age: Int by Delegates.vetoable(18) { property, oldValue, newValue ->
    if (newValue >= 0) {
        true  // 변경 허용
    } else {
        println("나이는 음수가 될 수 없습니다!") 
        false // 변경 거부
    }
}

fun main() {
    age = 25  // 변경 허용
    println(age) // 출력: 25

    age = -5   // 변경 거부, 출력: 나이는 음수가 될 수 없습니다!
    println(age) // 출력: 25
}

 

- Delegates.notNull

프로퍼티에 null 값을 허용하지 않도록 강제합니다. 일반적으로 lateinit의 대체로 사용할 수 있습니다.

import kotlin.properties.Delegates

var username: String by Delegates.notNull()

fun main() {
    // println(username) // 초기화되지 않아 예외 발생: Exception: Property username should be initialized before get.

    username = "JohnDoe"
    println(username) // 출력: JohnDoe
}

 

 

 

아이템 22 - 일반적인 알고리즘을 구현할 때 제네릭을 사용해라


타입 아규먼트를 사용하면 함수에 타입을 전달할 수 있습니다. 이러한 타입 아규먼트를 사용하는 함수를 제네릭 함수라고 부릅니다.

 

타입 파라미터는 컴파일러에 타입과 관련된 정보를 제공하여 컴파일러가 타읍을 조금이라도 더 정확하게 추측할 수 있게 해 줍니다.

 

제네릭은 기본적으로 List<String> 또는 Set<User>처럼 구체적인 타입으로 컬렉션을 만들 수 있게 클래스와 인터페이스에 도입된 기능입니다.

 

 

제네릭 제한

타입 파라미터의 중요한 기능 중 하나는 구체적인 타입의 서브타입만 사용하게 타입을 제한하는 것입니다.

class ListAdapter<T: ItemAdapter>()

위의 코드에서는 T를 ItemAdapter의 서브타입으로 제한하고 있습니다. 이를 통해 T 타입을 기반으로 반복 처리가 가능하고, 반복 처리 때 사용되는 객체가 Int라는 것을 알 수 있습니다.

 

 

일반적으로 타입 파라미터를 사용해서 type-safe 제네릭 알고리즘과 제네릭 객체를 구현합니다. 타입 파라미터는 구체 자료형의 서브타입을 제한할 수 있고, 이를 통해 특정 자료형이 제공하는 메서드를 안전하게 사용할 수 있습니다.

 

 

 

아이템 23 - 타입 파라미터의 섀도잉을 피하라


다음 코드와 같이 지역 파라미터가 외부 스코프에 있는 프로퍼티를 가리는 것을 섀도잉이라고 부릅니다.

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

 

이러한 현상은 클래스 타입 파라미터와 함수 타입 파라미터 사이에서도 발생합니다.

interface Tree
class Birch: Tree
class Spruce: Tree

class Forest<T: Tree> {

	fun <T: Tree>addTree(tree: T) {
    	// ...
    }
}

 

이런 식으로 코드를 작성하면 타입 파라미터가 독립적으로 작동하게 되지만, 코드만 봐서는 둘이 독립적으로 동작한다는 것은 빠르게 알아내기 힘듭니다.

 

따라서 addTree가 클래스 타입 파라미터를 사용하게 하는 것이 좋습니다.

interface Tree
class Birch: Tree
class Spruce: Tree

class Forest<T: Tree> {

	fun addTree(tree: T) {
    	// ...
    }
}

 

만약 의도적으로 독립적인 파라미터를 만드려면 이름을 아에 다르게 다는 것이 좋습니다.

 

 

 

아이템 24 - 제네릭 타입과 variance 한정자를 활용하라


variance에 대해
covariant - 공변성
invariance - 무(불)공변성
contravariant - 반공변성

 

 

아래와 같은 제네릭 클래스가 있을 때, variance 한정자가 없으므로 기본적으로 invariance 입니다. invariance라는 것은 제네릭 타입으로 만들어지는 타입들이 서로 관련성이 없다는 의미입니다. 예를 들어 Cup<Int> 와 Cup<Number>는 서로 어떠한 관련성도 갖지 않습니다.

class Cup<T>

 

 

만약에 어떠한 관련성을 원한다면 out 또는 in 이라는 variance 한정자를 붙입니다.

 

 

out

out은 타입 파라미터를 covariant로 만듭니다. 이는 A가 B의 서브 타입일 때, Cup<A>가 Cup<B>의 서브타입이라는 의미입니다.

open class Animal {
    fun eat() = println("Animal is eating")
}

class Cat : Animal() {
    fun meow() = println("Cat is meowing")
}

class Cup<out T>(private val value: T) { // T를 공변적으로 선언
    fun getValue(): T = value // T를 반환하는 함수는 사용할 수 있음
}

fun feedAnimal(cup: Cup<Animal>) {
    val animal: Animal = cup.getValue() // 공변성 덕분에 Animal 타입으로 안전하게 사용 가능
    animal.eat()
}

fun main() {
    val catCup = Cup(Cat()) // Cat은 Animal의 서브 타입
    feedAnimal(catCup)      // Cup<Cat>을 Cup<Animal>로 전달 가능 (공변성)
}

 

 

in

in 한정자는 반대 의미입니다. in 한정자는 타입 파라미터를 contravariant로 만듭니다. 이는 A가 B의 서브 타입일 때 Cup<A>가 Cup<B>의 슈퍼타입이라는 것을 의미합니다.

open class Animal {
    fun eat() = println("Animal is eating")
}

class Cat : Animal() {
    fun meow() = println("Cat is meowing")
}

class Cup<in T> { // T를 반변적으로 선언
    fun accept(value: T) {
        println("Accepting a value of type ${value::class.simpleName}")
    }
}

fun feedCupWithCat(cup: Cup<Cat>) {
    cup.accept(Cat()) // Cup<Cat>은 Cat만 받을 수 있음
}

fun main() {
    val animalCup: Cup<Animal> = Cup() // Cup<Animal>은 Cup<Cat>의 슈퍼 타입
    feedCupWithCat(animalCup) // Cup<Animal>을 Cup<Cat>으로 사용 가능
}

 

 

out in의 차이에 대해 명확히 이해하려면 데이터의 흐름 방향을 기준으로 생각하면 됩니다.

 

1. out은 "밖으로 나가는 것"

  • 데이터가 "나가는 방향"을 의미합니다.
  • 즉, 타입 파라미터는 반환(출력)에 사용될 수 있으며, 데이터를 "내보낼" 수 있습니다.
  • 데이터를 내보내는 용도로만 사용되므로 안전하게 공변성을 허용할 수 있습니다.

2. in은 "안으로 들어오는 것"

  • 데이터가 "들어오는 방향"을 의미합니다.
  • 즉, 타입 파라미터는 입력으로 사용될 수 있으며, 데이터를 "받아들일" 수 있습니다.
  • 데이터를 받아들이는 용도로만 사용되므로 반변성이 가능합니다.

 

 

함수 타입

코틀린 함수 타입의 모든 파라미터 타입은 contravariant입니다. 또한 모든 리턴 타입은 covariant입니다.

 

함수 타입을 사용할 때는 이처럼 자동으로 variance 한정자가 사용됩니다. 자주 사용되는 List는 convariant(out) 한정자가 사용됩니다. 

이는 variance 한정자가 붙지 않은 MutableList와는 다릅니다. 

 

이러한 이유는 이들이 불변성과 가변성에 따라 동작이 달라지기 때문입니다.

 

List는 불변 컬렉션으로 타입 파라미터가 출력 전용으로 사용됩니다. 그렇기 때문에 안전하게 공변성을 적용할 수 있습니다. 반면에 MutableList는 가변 컬렉션이기 때문에 타입 안정성을 보장할 수가 없습니다. A가 B의 하위 타입이라도, MutableList<A> MutableList<B>의 하위 타입이 아닙니다.

 

 

Variance 한정자의 안정성

자바의 배열은 covariant입니다. 이는 배열을 기반으로 제네릭 연산자는 정렬 함수 등을 만들기 때문입니다. 그런데 자바의 배열이 covariant라는 속성을 갖기 때문에 문제가 발생합니다.

 

아래의 코드는 컴파일 중에 아무런 문제가 없지만, 런타임 오류가 발생합니다.

Integer[] numbers = {1, 4, 2, 1};
Object[] objects = numbers;
objects[2] = "B" // 오류

 

numbers를 objects로 캐스팅해도 구조 내부에서 사용되는 실질적인 타입이 바뀌는 것은 아니기에 String 타입을 할당하면 오류가 발생합니다. 

 

코틀린에서는 이러한 결함을 해결하기 위해 Array를 invariant로 만들었습니다. (따라서 Array<Int>를 Array<Any> 등으로 바꿀 수 없습니다.)

 

 

 

아이템 25 - 공통 모듈을 추출해서 여러 플랫폼에서 재사용하라


다른 플랫폼에 동일한 제품을 구현한다면, 재사용할 수 있는 부분이 많을 것입니다. 특히 비즈니스 로직 부분읜 거의 동일합니다. 따라서 소스 코드를 공유할 수 있다면 큰 이득이 발생합니다.

 

 

풀스택 개발

일반적으로 백엔드와 웹은 분리해서 개발합니다. 하지만, 코틀린을 활용하면 공통 코드를 공유할 수 있습니다.

 

모바일 개발

일반적으로 안드로이드와 IOS는 거의 대부분 동일한 동작을 하지만 서로 다른 언어와 도구를 사용하여 개발해야 합니다.

만약, 코틀린 멀티 플랫폼 기능을 활용하면, 로직은 한 번만 구현하고 이를 재사용할 수 있습니다.

 

라이브러리

공통 모듈을 정의할 수 있다는 것은 라이브러리에 있어서 강력한 도구입니다. 특히 플랫폼에 크게 의존하지 않습니다.

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!