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

이번 글에서는 우리는 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란 끔찍한 기능을 의미합니다.

🔖Meaningful names
이름을 만들고 찾을 때 코드를 읽을 수 있도록 하려면 다음 3가지에 대해 답해야 합니다.
- Why it exists?
- What does it do?
- How it is used?

예시)
📗 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 원칙
- S (SRP) : 내가 만든 클래스는 하나의 기능만을 제공해라
- O (OCP) : 클래스들은 확장성에는 열려있지만 수정에 대해서는 닫혀 있어야 한다. (즉 수정이 없이 확장될 수 있도록 한다.)
- L (LSP) : 하위 타입으로 치환되더라도 성능이 떨어지면 안된다.
- I (ISP) : 인터페이스는 고객이 관심 있는만큼만 보여주면 된다.
- 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
코드 리뷰 이점
- 코드의 품질과 일관성 향상에 도움
- 지식 및 모범 사례 교환
- 코드 베이스를 익힐 수 있다.
- 코드에 대한 새로운 관점
- 코드 작성에 대한 새로운 팁과 요령을 익힐 수 있다.
내 개인적인 생각은 결국 클린 코드를 만들기 위해서는 꾸준한 연습과 코드 리뷰가 필수적인 것 같습니다.
'프로그래밍 > Kotlin' 카테고리의 다른 글
| [Kotlin] Data Class 이해하기 (0) | 2022.05.23 |
|---|---|
| [Kotlin] lateinit과 lazy란? - 지연 초기화를 통한 성능 개선 (0) | 2021.12.24 |
| [Kotlin] 코틀린(kotlin)의 장단점은 무엇일까? (0) | 2021.11.19 |
| [Kotlin] Kotlin Collection 관련 유용한 함수 (0) | 2021.08.28 |
댓글