상세 컨텐츠

본문 제목

[이펙이브 코틀린 2장]가독성

이펙티브 코틀린 정리

by Tabris4547 2023. 8. 14. 17:51

본문

728x90

우리가 자료를 만들 때

같은 내용이라도 좀 더 보기좋게 꾸밀려고 노력합니다.

전달을 어떻게 하냐에 따라 같은 내용이라도

사람이 느끼는 내용이 각기 다르기 때문입니다.

컴퓨터 코드는 컴퓨터가 해석하기 때문에

가독성이 필요없어 보일 수 있습니다.

하지만 코드도 결국 사람이 설계하는거고

다른 사람과 함께 작업을 한다면

가독성이 팀웍에 영향을 줄 수도 있습니다.

"나는 혼자 작업할거임"이라해도 가독성은 중요합니다.

왜냐하면 자기 코드를 자기가 보고

오류를 잡아내고 수정을 해야하니까요.

 

아이템 11 

가독성을 목표로 설계한다

 

클린코드를 쓴 로버트마틴은

"개발자가 코드를 작성하는데는 1분 걸리지만

이를 읽는데는 10분이 걸린다"라는 말을 합니다.

코드의 오류를 잡아내거나

추후의 수정을 할 때

코드를 보는 시간이 더 많아지기 때문입니다.

그런 점 때문에 프로그래밍은 쓰기보다 읽기가 중요하며

그래서 가독성이 중요합니다.

 

인식부하 감소

 

신호등이 초록색이면 

"길을 건넌다"라고 인식합니다.

우리는 살면서 "초록색이면 멈춘다"라는 신호등을 만난 적 없고

어릴때부터 "초록색이면 건넌다"라고 배워왔기 때문에

자연스럽게 머릿속에 그렇게 인식합니다.

이런 "경험"과 "인식에 대한 과학"이 코드를 읽을 때에도 영향을 줍니다.

//구현A

if(person!=null&&person.isAdult){
    view.showPerson(person)
}
else {
    view.showError()
}

//구현B

person?.takeIf{it.isAdult}
    ?.let(view::showPerson)
    ?.:view.showError()

코틀린으로 작성한 코드입니다.

구현A,B 두 개의 코드는

동작하는 바는 같습니다.

하지만 구현A가 우리 눈에 더 잘 들어옵니다.

심지어 코틀린을 잘 모르는 사람이라도

프로그래밍에 대한 기초적인 지식이 있다면

A코드는 무슨 말을 하는지 이해하기 쉽습니다.

반면에 B는 읽기가 어렵습니다.

let,takeIf에 대한 함수 레퍼런스를 알아야하고

{it} 이건 또 뭐고....

또한 let은 '람다식'의 결과를 리턴하는데

만약 let에 대해 익숙하지 않다면

오류가 발생했을 때 어떤 지점을 수정할지 파악하기도 힘듭니다.

이렇듯 "코드를 읽고 얼마나 빠르게 이해할 수 있는가"여부가

가독성이 있는지를 판단하는 코드입니다.

책에서는 이에 대해

"인지 부하를 줄이는 방법으로 코드를 작성하세요"라고 되어있어

사람들이 일반적으로 알기 쉬운 방향으로 코드를 짤 것을 권고합니다.

 

 

극단적이 되지 않기

 

그럼 B에서 보인 함수는 쓰면 안되나?

그렇지만은 않습니다.

2023.07.18 - [노베이스도 이해하는 공학이야기] - 함수형 프로그래밍은 왜 필요할까?

 

함수형 프로그래밍은 왜 필요할까?

프로그래밍을 배울 때 함수를 사용한다는 것은 다들 잘 배우셨을 겁니다. 그렇다면 함수형 프로그래밍이라는 말을 들어보셨나요? 이건 대체 무슨 소리야?하실 분들을 위해 왜 함수형 프로그래

door-of-tabris.tistory.com

제가 예전에 함수형 프로그래밍에 대해서 정리한 글이 있는데요.

처음에 함수형 프로그래밍을 배울 때

"굳이 이렇게 쓰는 이유가 뭘까?"에 대해 고민을 했었습니다.

함수형 프로그래밍처럼 코드를 작성하면 간결해보이긴한데

함수 자체를 모르면 코드 분석이 어렵게 느껴집니다.

하지만 많이 쓰이는 함수들을 익혀두고 사용한다면

오히려 더 간단하게 보일 때도 많습니다.

코틀린의 forEach는 for문 대신 사용하는 함수인데

의미를 알고있다면 오히려 for문으로 작성한 것보다 더 좋을때가 많습니다.

책에서 말하길

"비용을 지불할만한 가치가 없는 코드에 비용을 지불하는 경우"가 문제가 된다합니다.

코틀린에서 자주 사용되고 편한 함수들은

"시간비용을 지불할만한 가치"가 있기 때문에 알아두고 사용하기 좋습니다.

그러니 극단적으로

"간결한게 있지만 가독성을 헤치니깐 안쓸꺼임!"이렇게 하실 필요는 없습니다.

 

 

아이템 12

연산자 오버로드를 할 때는 의미에 맞게 사용하라

 

연산자 오버로딩을 써보신 적 있나요?

예를들면 C++에서 문자열을 합치는데는

복잡한 과정이 필요하지만, 

간단하게 파이썬처럼

"나는"+"나는"+"장풍을 했다!" 이렇게해도

문자열이 합쳐지게 만들도록하는 것이 연산자 오버로드입니다.

하지만 큰 힘에는 큰 책임이 따른다고 하죠.

이 책에서는 팩토리얼의 케이스를 설명하면서

연산자 오버로드에 대한 주의사항을 말합니다.

 

operator fun Int.not()=factorial()

print(10*!6)

 

6팩토리얼을 6! 이렇게 표시할 수 있습니다.

이와 비슷하게, int의 not을 나타내는 !을 팩토리얼로 만들어서

다음과 같이 사용해본다고 가정해봅시다.

동작이 된다 치더라도

보는 사람으로 하여금 혼란을 줍니다.

"6을 부정한다고??저게 무슨 말이야???"

아까 위의 문자열을 합칠 때 +를 쓰는 경우에는

"문자열을 +하니깐 서로 합친다는 거구나"라는 게 파악이 되지만

이 경우에는 논리연산의 not이 먼저 떠오르기 때문에

코드를 처음 보는 사람 입자에서는 혼란이 옵니다.

저건 마치 신호등의 원래 초록색 자리에 빨간불이 켜지도록 만들어놓고

"이 신호등은 빨간불에 건너면 됩니다"라는 것과 마찬가지죠.

 

아이템13

Unit?을 리턴하지 말라

 fun keyCorrect(key:String):Boolean
 {

 }


fun verifyKey(key:String):Unit?
{

}

 

Unit?은 Unit또는 null이라는 값을 가질 수 있습니다.

그래서 Boolean대신 사용할 수 있습니다.

함수의 내용이 같다고 가정할 때,

return을 Boolean에서 Unit?으로 바꿔도 결과에는 문제가 없습니다.

하지만 저렇게 사용한다면 코드를 읽는 사람입장에서는

결과값이 뭔지 확실하게 알 수 없어 오해를 할 수 있습니다.

Unit? 이렇게 쓰면 멋있어보일수는 있겠지만

오해를 불러일으킬 여지가 많기 때문에

Unit?으로 리턴하지 말 것을 권장합니다.

 

아이템14

변수타입이 명확하지 않은 경우 확실하게 지정하라

val num=10
val name="Kane"

C,C++등 변수 타입을 지정해야하는 언어를 다루다가

파이썬 같은 변수타입 지정이 필요없는 언어를 다룰 때

편하다는 느낌을 많아 받았습니다.

그냥 변수명 지정하고 값만 넣어주면 알아서 해주니깐 말이죠.

코틀린도 저런 식으로 변수를 선언해줘도 

수준높은 타입추론 시스템 덕분에 에러가 없습니다.

하지만...

val data1:String=getSomeData()

val data2=getSomeData()

data1은 변수타입을 명확하게 할 때

data2는 변수타입을 명시하지 않을때입니다.

data1은 변수타입을 지정해줬기 때문에

getSomeData()로 String타입이 올 것이라는 걸 알 수 있지만

data2의 경우에는 그렇지 못합니다.

그러면 이후에 코드를 작성하는 과정에서

명확하지 않기 때문에 다른 변수타입으로 인식하기 쉽습니다.

이런 오해를 줄이기 위해서 코틀린에서 코드를 작성할 때

변수 타입을 명확하게 해주는 것이 중요합니다.

 

아이템15

리시버를 명시적으로 참조해라

 

클래스에 대해서 배울 때, 지금 클래스를 참조하는 this에 대해서 배웁니다.

처음에 this를 왜 써야하는지 의문이 들었습니다.

class example{
    var name:String=""
    
    fun setName(name:String)
    {
        this.name=name
    }
}

 

그러다가 아래와 같은 예시를 접하고

왜 this를 써야하는지 이해하기 시작했습니다.

setName의 변수를 name이 아닌 다른 이름을 해도 되지만

일반적으로 이름을 받는 거에 변수를 name으로 쓰기 때문에

위와 같이 작성하는 것이 가독성면에서 좋습니다.

만약에 name=name이렇게 지정해버리면

컴퓨터도 사람도 이해하기 힘들기 때문에

this를 활용해서

"이 클래스의 name에 지금 함수의 name의 값을 넣는다"

라는 표현을 할 수 있습니다.

이런 this같은 걸 "리시버"라고 하며,

이 리시버를 통해 가독성을 높일 수 있습니다.

 

여러개의 리시버

class Node(val name:String){
    fun makeChild(childName:String)=
        create("$name.$childName")
            .apply{print("Created ${name}")}
    fun create(name:String):Node?=Node(name)
}

fun main(args: Array<String>) {
    val node=Node("parent")
    node.makeChild("child")

}

 

이 코드를 작동하면 어떤 결과가 나올까요?

일반적으로는 "Created parent.child"가 출력되리라 예상하지만

실제로는 "Created parent"가 출력됩니다.

왜 그런지 리시버를 좀 더 명시적으로 붙여보면

class Node(val name:String){
    fun makeChild(childName:String)=
        create("$name.$childName")
            .apply{print("Created ${this.name}")}//컴파일오류!
    fun create(name:String):Node?=Node(name)
}

우리가 예상하는 동작을 위해 apply안에 this로 명시해주면

컴파일 오류가 발생합니다.

이는 apply함수 내부의 this타입이 Node?라 직접 사용이 안됩니다.

class Node(val name:String){
    fun makeChild(childName:String)=
        create("$name.$childName")
            .apply{print("Created ${this?.name}")}//수정
    fun create(name:String):Node?=Node(name)
}

unpack하고 호출한 예 입니다.

책에서는 apply보다는 also를 사용하는 걸 권장합니다.

also를 사용하는 편이

nullable값을 처리하는 데에 훨씬 좋은 선택지이기 때문입니다.

class Node(val name:String){
    fun makeChild(childName:String)=
        create("$name.$childName")
            .also{print("Created ${it?.name}")}//수정
    fun create(name:String):Node?=Node(name)
}

fun main(args: Array<String>) {
    val node=Node("parent")
    node.makeChild("child")

}

 

아이템16

프로퍼티는 동작이 아니라 상태를 나타내야한다

 

var name:String?=null
get()=fieild.toUpperCase()
set(value){
    if(!value.isNullorBlanck()) {
        field=value
    }
}

자바의 필드와 비슷해보이는 코틀린의 프로퍼티.

둘 다 데이터를 저장한다는 점에서는 같지만

기본적으로 프로퍼티는 사용자 정의 세터와 세터를 가질 수 있다는 점이 다릅니다.

이렇게 프로퍼티 관련 함수를 구성할 때,

동작하는 것이 아닌 상태를 나타내면서

프로퍼티 자체를 변경하지 않도록 주의합니다.

 

 

 

 

아이템 17

이름있는 아규먼트를 사용하라

sleep(100)

sleep은 일반적으로

프로그림을 잠시 멈추는 함수로 사용됩니다.

그럼 이 코드는 얼마나 멈추는 걸 의미할까요?

100초?100ms?100분?

통상적으로 milliSecond(1/1000초)인 경우가 많지만

그걸 모르면 의미를 파악하기 힘들며

설령 알고있더라도 나중에 헷갈리기 쉽습니다.

이럴 때 이름있는 아규먼트를 통해 가독성을 높일 수 있습니다.

sleep(timeMillis=100)
 
 sleep(Millis(100))
 
 sleep(100.ms)

다음과 같이 아규먼트를 통해 시간단위를 표시한 경우입니다.

초기 sleep(100)과 의미하는 바는 같지만

단위를 명시해줬기 때문에

우리는 몇 초간 멈추는지 바로 알 수 있습니다.

 

같은 타입의 파라미터가 많을 경우

fun sendEmail(to:String,message:String)
{
    
}

이 함수는 파라미터가 많습니다. 이 함수를 사용할 때

sendEmail("문자열","문자열")이렇게 넣으면 되지만

실수로 to에 갈 껄 message에 넣는 식의 오류를 범할 수 있습니다.

이럴때는 이런 식으로 코드를 짤 수 있습니다.

 

fun main(args: Array<String>) {
    
    sendEmail(to="123@naver.com",message="hello")

}

이렇게 명시를 해준다면

나중에 이 코드를 다시 봐도 

"내가 어디에 뭘 넣었구나"라는 걸 한눈에 볼 수 있습니다.

 

 

아이템18

코딩 컨벤션을 지켜라

 

코딩컨벤션은 읽고 관리하기 쉬운 코드를 짜기 위한

일종의 "코딩 스타일 규약"입니다.

들여쓰기는 어떻게해야하는지

함수/클래스 명을 작성할 때는 어떻게 해야하는지

{}를 어떻게 나눠야하는지

변수이름은 어떻게 해야하는지 등등

"이렇게 해야 서로 알아보기 쉽다"라는 일종의 약속입니다.

이 코딩컨벤션은 중요하지만 많은 개발자가 지키지 않는다고 합니다.

왜냐하면 컨벤션을 지키지 않아도 작업하는데 크게 문제가 없어보이기 때문입니다.

그렇지만 우리가 다른 사람과 소통할 때

이왕이면 서로 합의된 규칙을 따르는 게 좋잖아요?

그리고 내가 내 코드를 다시 읽을 때

일반적으로 따르는 규칙을 적용해야 스스로도 편하잖아요?

그런점에서 코딩컨벤션을 준수하면서 프로그래밍을 차근차근 해보는 거 어떨까요?

 

정리하면서

 

여러분들은 혹시

한달 전, 일년 전에 본인이 짠 코드를 본 적 있나요?

분명 그 당시에는 어떻게 짯는지 다 알거 같았는데

다시 보니깐 "이게 뭔 코드여...?"했던 적 있으신가요?

이런 적이 있다면, 지금이라도! 가독성있게 코드를 짜는 습관을 가져봐요!

728x90

관련글 더보기

댓글 영역