본문 바로가기
오늘은 뭘 배울까?/책

[이펙티브 코틀린] 3장. 재사용성

by Kim Juhwan 2022. 11. 18.

1. Knowledge를 반복하여 사용하지 말라
2. 일반적인 알고리즘을 반복해서 구현하지 말라
3. 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라
4. 일반적인 알고리즘을 구현할 때 제네릭을 사용하라
5. 타입 파라미터의 섀도잉을 피하라
6. 제네릭 타입과 variance 한정자를 활용하라
7. 공통 모듈을 추출해서 여러 플랫폼에서 재사용하라

 

 

 


 

 

1. Knowledge를 반복하여 사용하지 말라 (111p ~ )

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

 

책을 읽다보면 느끼는 건데 저자는 어떻게 내가 하는 짓거리(?)들을 이렇게 잘 아는 걸까

재사용성 챕터의 첫 부분에 나오는 이 문장을 읽으며

코드를 복붙 해서 사용하는 지난날의 내 모습들이 주마등처럼 스쳐 지나갔다.

 

많은 개발자들은 코드를 반복해서 사용하지 말라고 한다.

DRY 규칙: Don't Repeat Yourself.

WET 안티 패턴: We Enjoy Typing, Waste Everyone's Time or Write Everything Twice.

SSOT: Single Source Of True

그리고 이 책의 저자는 Knowledge를 반복하여 사용하지 말라고 표현하고 있다.

 

모든 것은 변화한다.

시간이 흘러가며 물건도 낡아가고 사람도 모습이 변한다.

프로그래밍도 똑같다.

변화하기 때문에 변화에 유연하게 대응하려면 반복적인 코드를 줄여야 한다.

프로젝트 여기저기서 사용하고 있는 버튼이 있다고 하자.

어느 날 디자이너가 버튼의 디자인을 바꾼다고 했을 때, 프로젝트 내 버튼을 하나하나 다 찾아서 수정해야 한다면 너무 많은 공수가 들것이다.

그렇기 때문에 반복적인 코드를 줄여야 한다.

(이를 해결하기 위해서는 core-design 모듈에 drawable로 프로젝트 내에서 사용할 버튼을 만드는 방법 따위가 있을 것이다.)

 

반면, 반복을 줄이면 안 되는 상황도 있다.

반복을 줄이기 위해 코드를 추출해서 해결할 수 있다.

예를 들어 독립적인 안드로이드 앱 2개를 만든다고 했을 때

"빌드 도구 설정이 비슷하니까 추출해서 하나로 써야지~"라고 할 수도 있다.

근데 만약 구성 변경이 일부만 필요하게 되면,

즉 한 애플리케이션 쪽의 구성만 변경해야 하는 일이 온다면 골치 아파지는 것이다.

공수를 줄이기 위한 선택이 오히려 공수를 늘리는 상황을 불러오는 것이다.

 

앞선 예제로 알아보았듯, 코드를 반복하지 말라라는 규칙은 오용 또는 남용되지 말아야 한다.

그렇다면 어떻게 하면 오용, 남용을 피할 수 있을까? 🤔

이러한 잘못된 코드 추출로부터 우리를 보호할 수 있는 규칙

바로바로바로바로 단일책임원칙이다!

 

단일책임원칙은 "클래스를 변경하는 이유는 단 한 가지 여야 한다"는 의미를 가진다.

비유를 들기를 두 액터가 같은 클래스를 변경하는 일이 없어야 한다라고 표현되어있다.

여기서 액터는 변화를 만들어내는 존재이다.

현실세계로 빗대면 서로의 업무와 분야에 대해서 잘 모르는 개발자들로 비유된다.

이런 개발자들이 서로 같은 코드를 변경하는 건 정~말 위험하다.

이 각각의 개발자들이 비슷한 처리를 하더라도 미래에는 따로 변경될 가능성이 높으므로

공통된 코드로 추출하지 말고 따로 처리하는 것이 좋다.

 

2. 일반적인 알고리즘을 반복해서 구현하지 말라 (119p ~ )

이번 챕터에서는 알고리즘 뭣하러 만드냐! 이미 다 구현되어 있는데 네가 모르는 거다!라고 말하고 있다.

사실 이미 구현되어 있는 함수는 어마어마하게 많은데 우리가 쓰는 건 빙산의 일각일 뿐이다.

이미 있는걸 일일이 구현하는 것도 손해.

굉장히 잘 최적화되어있는 함수를 두고 발로 짠 알고리즘(= 내가 짠 알고리즘...)을 쓰는 것도 손해.

그래서 어떤 함수들이 있는지 공부하라고 나와있다.

 

사실 이 충고는 예전에 다른 책을 읽을 때도 나왔던 내용이다.

매일 15분씩만 투자해서 표준 라이브러리에 어떤 것들이 있는지 공부하라고 했었는데

그걸... 지키지... 않았... ㅈ...

동아리 프로젝트로 습관 만들기 앱을 제작하고 있는데 만들어서 이걸 습관으로 추가해놔야겠다... ^^..

 

TextUtils.isEmpty("Text") // 이거보단

"Text".isEmpty() // 이게 낫다.

아, 그리고 만약 표준 라이브러리에 없는 함수를 사용해야 하는 경우는

확장 함수로 정의해서 사용하는 것이 좋다고 한다.

첫 번째 방법대로 사용하려면 isEmpty가 어디에 속해있는지 알아야 사용할 수 있기 때문이다.

여러 라이브러리를 사용하고 있는 경우는 이를 아는 것이 꽤 어렵기 때문!

 

3. 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라 (125p ~ )

lazyDelegation(위임)에 대해서는 예전에 작성해둔 글이 있으므로 설명을 생략하려고 한다.

 

var key: String? by
	Delegates.observable(null) { _, old, new ->
    	Log.e("Key Changed from $old to $new")
    }

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

예를 들어, key라는 변수를 사용할 때마다 로그를 찍고 싶다면 위와 같이 작성하면 된다. 와우!

근데 로그를 남기고 싶은 변수마다 매번 이렇게 쓰기도 귀찮고.. 흠

그럼 아래와 같이 사용해보자!

 

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
    }
}

프로퍼티 위임을 활용해서 요로코롬 클래스를 작성해두면 편하게 사용이 가능하다 :)

 

@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)
    }

여기서 조금 더 나아가 보자면!

token 프로퍼티가 어떻게 컴파일되는지 까 보면 위와 같이 생겼다.

$도 들어가고 ::도 들어가고 잘 모르는 초급 개발자가 보면 머리가 어질어질하니까 중요한 것만 보자.

getValue와 setValue에 this가 들어간다. 이게 중요하다!

단순히 값만 바꾸는 게 아니라 context(this)를 넘긴다는 것이다.

이 context는 함수가 어떤 위치에서 사용되는지(= fragment or activity 등..)와 관련된 정보를 넘겨준다.

우린 이걸 활용해서 조금 더 활용을 해볼 수 있다.

 

Class SwipeRefreshBinderDelegate(val id: Int)
    private var cache: SwipeRefreshLayout? = null
    
    // Activity에서 getValue를 사용할 때
    operator fun getValue(
    	activity: Activity,
        prop: KProperty<*>
    ): SwipeRefreshLayout {
    	return cache ?: activity
    	.findViewById<SwipeRefreshLayout>(id)
        .also { cache = it }
    }
    
    // Fragment에서 getValue를 사용할 때
    operator fun getValue(
    	fragment: Fragment,
        prop: KProperty<*>
    ): SwipeRefreshLayout {
    	return cache ?: fragemnt.view
        .findViewById<SwipeRefreshLayout>(id)
        .also { cache = it }
    }
}

이렇게 넘어오는 context(this)의 값에 따라 어떠한 getValue를 사용할 것인지 분기 처리를 해줄 수 있다.

 

4. 일반적인 알고리즘을 구현할 때 제네릭을 사용하라 (130p ~ )

val list = mutableListOf<Int>()

제네릭에 대해서는 이전에 작성한 글이 있으므로 간단하게만 설명해보자면

우리는 위 코드에서 list에서 값을 꺼낼 때 항상 Int 타입의 값이 나온다는 것을 알 수 있다.

왜일까? 바로 <Int>라고 제네릭이 달려있기 때문이다.

제네릭은 구체적인 타입으로 컬렉션을 만들 수 있게 클래스와 인터페이스에 도입된 기능이다.

 

개인적으로 나는 이 기능을 사용자의 입장에서 자료형을 제한하기 위한 기능이라고 표현한다.

위 예시에 빗대면 list를 사용하는 입장에서 자료형이 제한되어 있으니 Int형임을 기대할 수 있는 것처럼 말이다.

 

public inline fun <T> mutableListOf(): MutableList<T> = ArrayList()

MutableList 코드 내부를 보면 이렇게 생겼다.

제네릭에 알파벳 T를 적어 여기에는 어떤 타입도 올 수 있음을 의미하고 있다.

 

나는 이 기능을 제공자의 입장에서 자료형을 제한하지 않기 위한 기능이라고 표현한다.

Int형에 맞는 함수를 따로, String형에 맞는 함수를 따로 만들 필요 없이

제네릭을 사용해서 모든 타입을 대입해서 쓸 수 있도록 하니까 말이다.

 

fun <T: Comparable<T>> Iterable<T>.sorted(): List<T> {
	...
}

fun <T, C : MutableCollecition<int T>>
Iterable<T>.toCollection(destination: C): C {
	...
}

class ListAdapter<T: ItemAdapter>(...) { ... }

그다음 제네릭 제한이라는 제목으로 책에서 뭔가 설명을 하고 있는데...

무엇을 말하고자 하는지 잘 감이 오지 않는다 😵‍💫

스터디 때 여쭤봐야지...

 

5. 타입 파라미터의 섀도잉을 피하라 (134p ~ )

class Forest(val name: String) {
	fun addTree(name: String) {
    	// 여기서 name을 쓰면 addTree의 name이 사용된다.
    }
}

섀도잉이란 지역 파라미터가 외부 스코프에 있는 프로퍼티를 가리는 것을 의미한다.

위 코드를 보면 addTree의 name이 Forest의 name을 가리기 때문에

주석 달린 부분에서 name을 사용하면 addTree의 name이 사용된다.

 

interface Singer
class IU: Singer
class 2PM: Singer

class Singer<T: Singer> {
    fun <T: Singer> addSinger(singer: T) {
    
    }
}

예제를 가져왔ㅅ..

매번 예제에 아이유를 넣었더니 아이유 좋아하냐는 문의가 들어와서 바꿔보겠습니다.

 

interface Singer
class Ive: Singer
class Aespa: Singer

class Singer<T: Singer> {
    fun <T: Singer> addSinger(singer: T) {
    
    }
}

이렇게 클래스 타입 파라미터와 함수 타입 파라미터 사이에서 섀도잉 현상이 발생했다고 하자.

(<T: Singer>가 중복으로 들어간 것을 설명하고 있는 것이다)

 

val singer = Singer<Ive>()
singer.addSinger(Ive())
singer.addSginer(Aespa())

섀도잉이 발생했기 때문에 위 코드는 에러가 뜨지 않는다.

Ive라고 타입 제한을 걸고 나서 Aespa를 추가했는데도 에러가 뜨지 않는 것이다!

이건 개발자가 의도하지 않은 동작일 확률이 높고 알아차리기도 쉽지 않다.

 

class Singer<T: Singer> {
    fun addSinger(singer: T) {
    
    }
}

그래서 이런 경우는 섀도잉이 발생하지 않게 클래스의 타입 파라미터인 T를 따르게 하는 것이 좋다.

 

6. 제네릭 타입과 variance 한정자를 활용하라 (136p ~ )

이번 장에서는 제네릭에 관한 이야기를 제대로 이해하지 못했다.. 전멸당함...

옮긴이의 말에 따르면 variance와 관련된 내용을 전혀 모른다면 이 책에서 읽기 가장 어려운 부분이 될 것이라고 한다.

(그게 나야~ 둠바 둠바 두비두밤...)

 

일단 한 번 읽었는데 이해 못 했고... 스터디 끝나고 다시 읽어봐야 할 것 같다.. ^^...

 

7. 공통 모듈을 추출해서 여러 플랫폼에서 재사용하라 (148p ~ )

다행히도 책이 다시 가벼운 이야기로 돌아왔다.

요즘에는 코틀린 멀티플랫폼 기능을 활용하면 백엔드 개발도 할 수 있고 iOS 개발도 할 수 있다고 한다.

예를 들어 안드로이드랑 iOS는 플랫폼과 사용 언어만 다를 뿐 안에 내용은 유사하니

코틀린으로 공통 코드를 뽑아내면 재사용할 수 있어 좋다고 한다.

 

한편으로는 우왕 내가 사용하는 코틀린이 이렇게나 강력해지다니! 내가 대세인 언어를 주 언어로 사용하고 있다니 기쁘군!

이라는 생각이 들면서

한편으로는 코틀린 하나로 다른 영역을 넘볼 수 있는 것 처럼 다른 개발자들도 안드로이드 개발 영역을 넘 볼 수 있기에

일자리를 뺏기려나...라는 생각도 든다 ㅋㅋㅋ

아니면 iOS 개발까지 다 시켜버리는 건 아닐지..! (안드 하나 하기도 빡세다구요 ㅠㅠ)

 

 

 


💡 느낀 점

  • 슬랙이 원래 게임이었는데 운영이 중단되고 커뮤니티 기능이 좋아서 현재 메신저로 변한 거라는 내용이 흥미로웠다... ㅋㅋㅋ
  • 6번 목차 부분이 가장 중요한 부분 같았는데 이해를 못 해서 아쉽다. 그래도 다시 읽을 때는 이번보다 더 이해가 되겠지!
  • 확장 함수 모르기 전에는 xxxUtils 안에 다 넣어놓고 쓰고 그랬는데.. 확장 함수를 애용해야겠다.

📘 참고한 자료

  • Effective Kotlin - 마르친 모스칼라 지음, 윤인성 옮김

 

 

반응형

댓글