이번 게시글은 Effective Kotlin — Chapter2. Readability에 대한 내용입니다.

로버트 마틴은 클린 코드라는 책에서 이런 말을 했습니다.

개발자가 코드를 작성하는 데는 1분 걸리지만, 이를 읽는 데는 10분이 걸린다.
로버트 마틴 — 클린 코드에서

즉 개발자는 어떤 코드를 작성 하는 것보다 읽는 데 더 많은 시간을 소모합니다. 그렇기에 프로그래밍은 쓰기보다 읽기가 중요하고, 항상 가독성을 생각하며 코드를 작성해야 합니다.

1. 가독성을 목표로 설계해라

가독성은 사람에 따라 다르게 느낄 수 있습니다. 하지만 일반적으로 많은 사람의 경험과 인식에 대한 과학으로 만들어진 어느 정도의 규칙이 있습니다.

가독성을 비교하기 위한 예시를 보겠습니다.

동일한 내용의 A, B 코드가 있습니다. 그리고 A는 표준적인 스타일이고, B는 Kotlin만의 기능을 사용한 코드 입니다.

이 두 코드를 비교했을 때, 해석하기에는 A코드가 당연히 눈에 더 잘 들어올 것입니다. 물론 Kotlin 스타일대로 코드를 풀어내고 싶어 B와 같은 방식을 선호할 수 있지만, 그건 본인만의 생각일 뿐입니다.

Kotlin 스럽게 만드는 것은 더 나은 가독성을 준다는 가정하에 의미가 있을 뿐, 그렇지 않다면 그건 오히려 Kotlin 스럽지 않은 코드라고 말할 수 있습니다.

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

연산자 오버로딩은 굉장히 강력한 기능이지만, 위험 할 수 있습니다.

가령 숫자 값에 대한 factorial 함수를 만들고자 할 때는 다음과 같이 만들 수 있습니다.

확장 함수를 이용하여 굉장히 편리하게 사용 할 수 있습니다.

중고등학교 수학 시간에 팩토리얼은 다음과 같이 ! 사용해 표기한다는 것을 배웠을 것입니다. 10 * 6!

코틀린은 이런 연산자를 지원하지 않지만, 다음과 같이 연산자 오버로딩을 활용하면, 만들 수 있습니다.

이렇게 만들어서 될까요?? 당연하게도 안됩니다. 기존에 존재하는 operator function과 완전히 다른 의미로 사용 되기 때문에 혼란을 가져 올 수 있습니다.

분명하지 않은 경우

위의 예는 사실상 너무나도 명확하게 사용하면 안된다는 것이 보입니다. 하지만 애매한 경우가 있습니다.

가령 함수를 n번 실행하게 만드는 함수를 만든다고 했을 때, 연산자 오버로딩을 사용한다면 다음과 같이 만들 수 있습니다.

이 것에 대해서는 좀 햇갈립니다. 괜찮은거 같기도… 아닌거 같기도 하고… 이렇게 분명하지 않은 경우에는 연산자 오버로딩을 하기 보단 infix 확장함수를 사용하는 것이 보다 더 명확합니다.

연산자 오버로딩은 그 이름의 의미에 맞게 사용해야 합니다. 연산자 의미가 명확하지 않다면, 연산자 오버로딩을 사용하지 않는 것이 좋습니다.

3. Unit?을 리턴하지 말라

Unit?을 리턴하는 함수는 Unit과 null타입을 리턴 할 수 있습니다. 사실상 이건 Boolean을 리턴한 것과 같은 의미를 지닙니다. 이런 트릭은 코드를 작성할 때는 멋있게 보일 수도 있겠지만, 읽을 때는 그렇지 않습니다.

Unit?로 Boolean을 표현하는 것은 오해의 소지가 있으며, 예측하기 어려운 오류를 발생 시킬 수 있습니다.

가령 Unit?로 리턴하는 함수를 만들어 놓고, 아무 인지도 못한 체 호출 부 처럼 만들었다면, getData()가 null이 아님에도 불구하고, view.showData()에서 null을 리턴하여 view.showError() 가 실행 될 수도 있습니다.

이와 같은 경우, 읽는 사람 입장에서는 이런 오류는 찾아내기 어렵습니다.

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

코틀린은 개발자가 타입을 지정하지 않아도, 타입을 지정해서 넣어 주는 굉장히 수준 높은 타입 추론 시스템을 갖추고 있습니다.

이는 개발 시간을 줄여 줄 뿐만 아니라 유형이 명확할 때는 코드가 짧아지므로 코드의 가독성이 크게 향상됩니다. 하지만 유형이 명확하지 않을 때는 남용하면 좋지 않습니다.

위 코드는 타입을 숨기고 있습니다. 가독성을 위해서 코드를 설계할 때 읽는 사람에게 중요한 정보를 숨겨서는 안됩니다.

그래서 타입이 명확하지 않은 경우엔 직접 명시하여, 읽는 사람이 타입을 굳이 찾아보지 않아도 바로 알 수 있게 만들어야 합니다.

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

코틀린의 프로퍼티는 자바의 필드와 비슷해 보이지만 큰 차이가 있습니다.

// 코틀린 프로퍼티
var name : String? = null
// 자바 필드
String name = null;

둘 다 데이터를 저장한다는 점에서는 동일하지만 프로퍼티에는 더 많은 기능이 있습니다. 일단 기본적으로 프로퍼티는 var은 setter/getter를 가지고 있으며, val 은 getter를 가지고 있습니다.

또한 아래와 같이 커스텀도 가능합니다.

프로퍼티에는 setter/getter 함수를 포함하고 있으므로, 다음과 같이 아예 함수를 프로퍼티로 교체 할 수 있습니다. 하지만 이는 절대 좋은 방법이 아닙니다.

가장 큰 이유는 가독성 측면에서의 문제입니다. 프로퍼티는 필드 + setter/getter입니다. 즉 프로퍼티 자체가 객체의 상태를 나타내기 위함이고, 그 상태를 setter/getter까지 캡슐화 시킨 것이 프로퍼티입니다.

그런 프로퍼티를 객체 상태에 대한 용도가 아닌 객체의 행동의 용도로 만든다는 것 자체가 넌센스 입니다.

프로퍼티를 사용할 것이냐, 함수로 처리 할 것이냐는 간단합니다. 만약 함수로 처리한다면 get 또는 set을 붙일 것인가? 만약 아니라면 이를 프로퍼티로 만들지 않아야 합니다.

반대로 객체의 상태를 함수를 통해 get/set 하는 것 역시 하지 말아야 합니다.

많은 사람들은 경험적으로 프로퍼티는 상태를 나타내고, 함수는 행동을 나타낸다고 생각합니다.

6. 이름 있는 Argument를 사용해라

코드에서 Argument의 의미가 명확하지 않은 경우가 있습니다. 다음 예를 살펴보겠습니다.

val text = (1..10).joinToString("|")

“|”은 무엇을 의미하는 것일까요? joinToString() 을 많이 사용해본 개발자만 seperator 지정이라는 것을 알지, 그렇지 않다면 함수 정의부에 들어가서 확인해야 합니다.

그래서 파라미터가 명확하지 않은 경우에는 이를 직접 지정해서 명확하게 만들어 주는 것이 좋습니다

val text = (1..10).joinToString(seperator="|")

그렇다면 이름 있는 Argument는 언제 사용하는 것이 좋을까요?

  • 디폴트 Argument인 경우
    함수안에 디폴트 Argument를 가질 경우, 항상 이름을 붙여서 사용하는 것이 좋습니다. 순서와 상관없이 Argument를 지정 할 수 있어, 오류를 줄일 수 있습니다.
  • 같은 타입의 파라미터가 많은 경우
    동일 타입의 파라미터가 많은 경우, 실수를 할 여지가 발생할 수 있습니다. 그렇기에 이름 있는 Argument로 지정해줌으로써 실수할 여지를 없앨 수 있습니다.
  • 여러 람다를 받는 경우
    함수에 여러 람다를 받는 경우, 이름 있는 Argument로 지정해줌으로써 명확성을 가질 수 있습니다.

7. 코딩 컨벤션을 지켜라

코틀린 문서의 Coding Conventions을 확인해서 코딩 컨벤션을 지키는 것이 좋습니다. 물론 이런 컨벤션이 모든 프로젝트에 최적인 것은 아니지만, 코틀린 커뮤니티에 속한 사람이라면 이러한 컨벤션을 지키는 것이 좋습니다.

컨벤션을 지킬 때 도움이 되는 두가지 도구가 있습니다.

  • InteliJ Formatter : 공식 코딩 컨벤션 스타일에 맞춰서 코드를 변경해줍니다. Preferences… > Editor > Code Style > Kotlin > Set from… 를 통해 코드 스타일을 지정 할 수 있습니다.
  • ktlink : 많이 사용되는 코드를 분석하고 컨벤션 위반을 알려 주는 linter입니다.

Effective Kotlin Chapter 2. Readability에 대한 요약은 여기까지 입니다.

다음 게시글에서 Chapter3. 재사용성에 대한 요약으로 작성하도록 하겠습니다.

--

--