본문 바로가기
프로그래밍/Kotlin

[Kotlin] (번역, 요약) Clean Code with Kotlin

by tempus 2021. 9. 1.
반응형

해당 글은 https://magdamiu.com/2021/08/23/clean-code-with-kotlin-2/ 를 요약과 개인적인 생각이 들어있습니다. (잘못된 부분이나 내용 개선에 관해서 피드백 환영합니다.)


 

kotlin_logo

이번 글에서는 우리는 Clean Code는 무엇인지 요약하고 의미 있는 이름을 정의하는 것의 중요성과 깨끗한 함수와 클래스를 작성하는 방법을 강조할 것입니다.


그리고 아래와 같은 3가지에 대해 자세히 알아볼 겁니다.

  • immutablility의 장점
  • Kotlin의 오류 처리 방법
  • 테스트 작성의 모범 사례

이 글이 끝나면 Clean Code가 무엇을 의미하는지 더 잘 이해하고 코드에 적용할 수 있는 팁과 트릭을 배우게 될 것입니다.


What is Clean Code?

  • 클린 코드는 읽을 수 있고 이야기를 들려준다. - 책 "클린코드" 내용 중 일부
  • 코드를 읽는 독자에게 의도를 명확하게 표현하라, 읽을 수 없는 코드는 똑똑하지 않다. - 책
    "Practices of the Agile Developer: Working in the Real World" 내용 중 일부

How to measure if we have Clean Code?

비공식적인 측정값은 WTFs / min입니다. wtf/min이 적어 행복한 팀이 있고 그렇지 못한 팀이 있습니다. 여기서 wtf란 끔찍한 기능을 의미합니다.

 


WTF_img


🔖Meaningful names

이름을 만들고 찾을 때 코드를 읽을 수 있도록 하려면 다음 3가지에 대해 답해야 합니다.

  • Why it exists?
  • What does it do?
  • How it is used?

ex_table_img

예시)

📗 TODO : 파일 경로를 분할하여 최신 파일과 해당 디렉터리를 가져오라

👎 UnClean Code

data class GetFile(val d: String, val n: String)

val pattern = Regex("(.+)/([^/]*)")

fun files(ph: String): PathParts {
  val match = pattern.matchEntire(ph)
        ?: return PathParts("", ph)

  return PathParts(match.groupValues[1],
      match.groupValues[2])
}

👍 Clean Code

data class PathParts(val directory: String, val fileName: String)

fun splitPath(path: String) =
    PathParts(
        path.substringBeforeLast('/', ""),
        path.substringAfterLast('/'))

📗 "if-null" 체크를 피하고 "throw"와 함께 "elvis"를 대신 사용해라

👎 UnClean Code

class Book(val title: String?, val publishYear: Int?)

fun displayBookDetails(book: Book) {
    val title = book.title
    if (title == null)
        throw IllegalArgumentException("Title required")
    val publishYear = book.publishYear
    if (publishYear == null) return

    println("$title: $publishYear")
}

👍 Clean Code

class Book(val title: String?, val publishYear: Int?)

fun displayBookDetails(book: Book) {
    val title = book.title ?: throw IllegalArgumentException("Title required")
    val publishYear = book.publishYear ?: return

    println("$title: $publishYear")
}

📗 Warning : 명백한 인수 이름을 사용하고 "it"을 자주 사용하는 것을 피하라

👎 UnClean Code

users.filter{ it.job == Job.Developer }
    .map{ it.birthDate.dayOfMonth }
    .filter{ it <= 10 }
    .min()

👍 Clean Code

users.filter{user -> user.job == Job.Developer}
    .map {developer -> developer.birthDate. dayOfMonth}
    .filter {birthDay -> birthDay <= 10}
    .min()

Kotlin을 사용하면 불변성에 대해 더 자주 YES라고 말할 수 있습니다.

  • var보다는 val을 더 선호
  • 변경 가능한 속성보다 읽기 전용 속성을 선호
  • 변경 가능한 컬렉션보다 읽기 전용 컬렉션을 사용하는 것을 선호
  • copy()를 제공하는 데이터 클래스를 사용하는 것을 선호

⚙️Functions

Kotlin에서 function을 작성하기 위한 규칙은 다음과 같습니다.

함수는 작아야 합니다.


그렇다면 함수가 길었을 때 단점은 무엇인가?

  • 테스트하기 힘듬
  • 읽기 힘듬
  • 재사용 힘듬
  • duplication을 유발함
  • 디버그하기 힘듬
  • 변경하기 힘듬

그 이외의 규칙들은 다음과 같습니다.

  • 많은 arguments => list나 object를 통해 group으로 묶습니다.
  • 함수의 들여쓰기 수준 => 최대 2
  • 함수에는 Side Effect가 없어야 합니다.
  • if, else, while 문에는 한 줄의 코드가 있는 블록이 포함됩니다.
  • 설명이 포함된 긴이름이 수수께끼 같은 짧은 이름보다 낫습니다.
  • 명령 쿼리 분리 원칙 적용
  • 읽기에 충분히 쉬워질 때까지 코드를 변경하고 재구성

📗 Sample 1

👎 Bad

fun parseProduct(response: Response?): Product? {
    if (response == null) {
        throw ClientException("Response is null")
    }
    val code: Int = response.code()
    if (code == 200 || code == 201) {
        return mapToDTO(response.body())
    }
    if (code >= 400 && code <= 499) {
        throw ClientException("Invalid request")
    }
    if (code >= 500 && code <= 599) {
        throw ClientException("Server error")
    }
    throw ClientException("Error $code")
}

👍 Good

fun parseProduct(response: Response?) = when (response?.code()){
    null -> throw ClientException("Response is null")
    200, 201 -> mapToDTO(response.body())
    in 400..499 -> throw ClientException("Invalid request")
    in 500..599 -> throw ClientException("Server error")
    else -> throw ClientException("Error ${response.code()}")
}

📗 Sample2

👎 Bad

for (user in users) {
    if(user.subscriptions != null) {
        if (user.subscriptions.size > 0) {
            var isYoungerThan30 = user.isYoungerThan30()
            if (isYoungerThan30) {
                countUsers++
            }
        }
    }
}

👍 Good

var countUsersYoungerThan30WithSubscriptions = 0
for (user in users) {
    if (user.isYoungerThan30WithSubscriptions) {
        countUsersYoungerThan30WithSubscriptions++;
    }
}

📰Classes

클래스는 작아야 합니다. 단 하나의 책임과 하나의 변경 이유가 있어야 합니다. 클래스는 원하는 시스템 동작을 달성하기 위해 몇몇 다른 클래스와 협력합니다.


클래스는 뉴스 기사와 비슷하다 :

  • 이름은 간단하고 이해하기 쉬어야 합니다.
  • 위에서부터 높은 수준의 개념과 알고리즘을 포함해야 합니다.
  • 아래로 이동하면 세부 정보가 나타납니다.
  • 결국에는 우리는 가장 낮은 수준의 기능을 찾습니다.

클래스는 결합도(Coupling)가 낮을수록 응집도(Cohesion)가 높을수록 좋다.

  • 결합도 : 서로 다른 모듈 간에 상호 의존하는 정도 또는 연관된 관계를 의미
  • 응집도 : 클래스/모듈의 요소가 기능적으로 관련되어 있는 정도

OPP에서 SW 개발 원칙

DRY - 같은 일을 두 번 하지 말라

KISS - 단순하게 하라

YAGNI - 정말 필요할 때까지 해당 기능을 만들지 마라


SOILD 원칙

  1. S (SRP) : 내가 만든 클래스는 하나의 기능만을 제공해라
  2. O (OCP) : 클래스들은 확장성에는 열려있지만 수정에 대해서는 닫혀 있어야 한다. (즉 수정이 없이 확장될 수 있도록 한다.)
  3. L (LSP) : 하위 타입으로 치환되더라도 성능이 떨어지면 안된다.
  4. I (ISP) : 인터페이스는 고객이 관심 있는만큼만 보여주면 된다.
  5. D (DIP) : 의존 관계를 맺을 때 변화하기 쉬운 것보다는 변화가 없는 것에 의존하라 (구체적인 클래스보다 인터페이스나 추상 클래스와 관계를 맺어라)

💣Error handling

우리가 예상치 못한 시나리오를 어떻게 다루어야 할까? 이때 우리는 아래와 같이 해야 합니다.

  • 사용자 정의 오류보다 표준 오류를 선호하자
  • 오류 처리는 중요하지만 로직을 애매하게 하면 잘못된 것이다.
  • Kotlin에서 우리는 확인되지 않은 예외만 있다.
  • 결과가 없는 경우 null 또는 실패 결과를 선호하자

Kotlin에서는 함수가 아무것도 반환하지 않을 때 Nothing을 사용할 수 있습니다.

fun computeSqrt(number: Double): Double {
    if(number >= 0) {  
        return Math.sqrt(number)
    } else {  
        throw RuntimeException("No negative please")
    }
}

✅Testing


  • 모든 JUnit test 함수는 하나당 하나의 assert statement를 가지고 있어야 한다.
  • Given, When, Then (BDD) 을 가지고 테스트를 작성하라
  • 하나의 테스트 함수 당 오직 하나의 테스트 컨셉을 가지고 있어야 한다.
  • 가장 좋은 방법은 assert 수를 최소화하는 것이다.
  • 테스트의 실패에는 오직 한 가지 이유만이 있어야 한다.

테스트를 작성할 때 FIRST 규칙을 준수해야 합니다.

  • First (빠르게) : 테스트는 빠르고 신속하게 실행되어야 한다.
  • Independent (독립적으로) : 테스트는 다른 테스트에 의존적이면 안된다.
  • Repeatable (반복적으로) : 특정 인프라 없이 모든 환경에서도 반복 가능해야 한다.
  • Self-vaildating(자체 검증) : 테스트의 결과는 실패 또는 성공 뿐이다.
  • Timely (적시에) : production 코드가 생산되기 전에 적시에 작성해야 한다.

처음부터 테스트 코드를 제대로 작성하지 않고 프로덕션을 만든다면 해당 프로덕션의 기능이 더 추가되고 발전할수록 난처해질 것입니다. 결국 이는 기술 부채로 이어질 가능성이 큽니다.


💬Comments

규칙 : 잘못된 코드에 주석을 추가하지 말고 설명이 잘 될 때까지 다시 작성하라!


코드를 작성할 때 처음부터 완벽하기는 어렵기에 항상 다시 리팩터링을 해야 합니다. 코드에 너무 많은 주석을 달지 말고 명확해질 때까지 리팩터링 해야 합니다.


이건 개인적인 차이가 있는 것 같습니다. 어느 분은 주석이 없으면 읽기가 불편하다고 클래스와 함수마다 주석을 다시는 분들도 있고 어떤 분은 주석은 쓰지 않을수록 더 깔끔한 코드라고 생각하시는 분이 있습니다. 나는 후자가 좀 더 내 스타일에 맞는 것 같습니다.

(왜냐하면 귀찮으니깐 ㅎㅎ)

👩🏽‍💻Code Review Best Practices

코드 리뷰 이점

  1. 코드의 품질과 일관성 향상에 도움
  2. 지식 및 모범 사례 교환
  3. 코드 베이스를 익힐 수 있다.
  4. 코드에 대한 새로운 관점
  5. 코드 작성에 대한 새로운 팁과 요령을 익힐 수 있다.

내 개인적인 생각은 결국 클린 코드를 만들기 위해서는 꾸준한 연습과 코드 리뷰가 필수적인 것 같습니다.

반응형

댓글


loading