해당 글은 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 |
댓글