Kotlin - [이펙티브 코틀린] 코드설계(1)
이 글은 이펙티브 코틀린 책을 참고하여 쓰여졌습니다.
개발자가 코드를 작성할 때 동작이 명확하고 한눈에 기능이 들어오도록 만드는 것이 중요합니다.
이때 코틀린을 이용하여 코드를 깔끔하고 의미있게 만드는 방법을 하나씩 알아보겠습니다.
1. Knowledge를 반복하여 사용하지 말라
여기서 표현한 knowledge는 의도적인 정보로 코드, 데이터로 표현할 수 있습니다.
크게 두가지로 나뉘는데 로직, 공통 알고리즘입니다.
로직 | 프로그램이 동작하는 방향 |
공통 알고리즘 | 원하는 동작을 하기 위한 알고리즘 |
코드에서 일부 디자인, 동작들이 변경될 때 모든 부분을 수정하기는 쉽지않습니다.
ex) 공통사용 UI (Button)...
이로 인해 코드는 공통으로 사용하고 이는 관리가 필요한데 이때 피필요한 것이 단일책임원칙입니다.
단일 책임 원칙이란 서로 다른 업무의 사람이 같은 클래스를 변경하지 않도록 하는 것입니다.
class Student {
val point = 10
fun isPassed(): Boolean = point > 10
fun testEnglish(point: Int) {} //영어시험시 사용
fun testComputer(point: Int) {} //컴퓨터시험시 사용
}
다음 예제를 보면 학생클래스에 여러 함수가 만들어진 것을 확인할 수 있습니다.
영어학과에서 영어시험시 사용되는 함수를 만들었고,
컴퓨터학과에서 컴퓨터시험시 사용되는 함수를 만들었습니다.
이 때 영어학과에서는 필요없는 컴퓨터시험 함수를 가지고 있는 상황입니다.
그리고 시험 통과상황을 판별하는 isPassed함수가 있는데 영어학과, 컴퓨터학과 각각 시험 통과 요구점수가 다를 수 있어 함부로 수정할 경우 문제가 발생할 수 있습니다.
따라서 기본적인 Student 클래스를 따로 만든 후 EnglishStudent, ComputerStudent에서 해당클래스를 상속받아 사용하는 것이 코드 유지보수 측면에서 유리합니다.
2. 일반적인 알고리즘을 반복해서 구현하지 말라
개발자는 여러 알고리즘을 구현하지만 간단한 알고리즘은 stdlib를 참조하여 사용할 수 있습니다.
예를 들어 어떤 숫자의 범위를 제한하는 알고리즘을 만들 때 coerceIn함수를 사용하여 제한할 수 있습니다.
그 외에 간단하게 사용할 수 있는 함수로 map함수가 있습니다.
data class User(val name: String, val grade: Int, val age: Int)
val userList = arrayListOf<User>()
userList.add(User(name = "first", grade = 1, age = 10))
userList.add(User(name = "second", grade = 2, age = 11))
userList.add(User(name = "third", grade = 3, age = 12))
val userNameListByFor: ArrayList<String> = arrayListOf()
for (user in userList) {
userNameListByFor.add(user.name)
}
val userNameListByMap: List<String> = userList.map { it.name }
위의 예제코드를 보면 User 리스트의 데이터에서 이름을 가져와서 리스트로 만들고 있습니다.
이때 map함수를 참조하면 반복문을 사용하는 방식보다 간단하게 구현할 수 있습니다.
3. 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라
프로퍼티 위임(Property Delegation)을 활용하면 일반적인 프로퍼티의 행위를 추출해서 사용가능합니다.
data class User(val name: String, val age: Int)
fun createStudent(): User {
println("createStudent")
return User(name = "student", age = 20)
}
val student by lazy { createStudent() }
println("log - 1")
println("log - student name == ${student.name}")
var observableText by Delegates.observable("first") { property, oldValue, newValue ->
println("property == $property\noldValue == $oldValue\nnewValue == $newValue")
}
println("observableText == $observableText")
observableText = "second"
/*
log - 1
createStudent
log - student name == student
observableText == first
property == property observableText (Kotlin reflection is not available)
oldValue == first
newValue == second
*/
위의 예시에서는 프로퍼티 위임을 사용하기 위해 lazy와 observable 델리게이터를 사용했습니다.
lazy의 경우 해당 프로퍼티를 사용할 때 초기화해주고,
observable의 경우 해당 프로퍼티가 변경될 때 observable의 onChange함수를 호출해줍니다.
주석으로 처리된 결과값을 통해 현재 코드가 어떻게 동작하고 있는지 확인가능합니다.
4. 일반적인 알고리즘을 구현할 때 제네릭을 사용하라
함수의 아규먼트에 타입을 전달하는 경우가 있는데 이를 제네릭 함수 라고 부릅니다.
대표적인 제네릭 함수로 filter함수가 있습니다.
타입 파라미터의 경우 컴파일러에 타입과 관련된 정보를 제공하기 때문에 컴파일러가 타입을 추측할 수 있게 도와줍니다.
이때 제네릭 제한을 둘 수 있는데 : (콜론), in, out variance를 이용하여 제한할 수 있습니다.
5. 함수 내부의 추상화 레벨을 통일하라
프로그래밍을 할 때 복잡한 내용들의 핵심 기능을 간추려 사용하기 위해 추상화를 사용합니다. 이때 추상화 레벨을 구분하면 다른 사람이 해당 코드를 확인할 때 쉽고 빠르게 이해할 수 있도록 도와줍니다.
class CoffeeMachine() {
fun makeCoffee() {
//커피를 만드는 로직을 순서대로 작성
}
fun makeCoffeeBySLA() {
boilWater()
brewCoffee()
pourCoffee()
pourMilk()
}
fun boilWater() {}
fun brewCoffee() {}
fun pourCoffee() {}
fun pourMilk() {}
}
위의 예제코드를 보면 추상화 레벨을 구분한 것과 구분하지 않은 것의 차이점을 알 수 있습니다.
만약 makeCoffee 함수에 커피를 만드는 모든 코드가 들어가게 된다면 내부 동작이 수정될 때 모든 코드를 살펴보며 수정이 이루어져야 합니다.
하지만 makeCoffeeBySLA 함수의 경우 코드 내부가 함수로 구분되어져 있어 어떤 계층에서 수정이 이루어져야할 지 명확해집니다.
이처럼 함수는 작아야하며 최소한의 책임만 가지도록 설계되어야 합니다.
6. 변화로부터 코드를 보호하려면 추상화를 사용하라
코드를 작성 후 추상화로 실질적인 코드를 숨기면 사용자는 세부사항을 알 필요없이 사용할 수 있습니다. 이때 추가적인 요청으로 코드를 수정해야할 경우 기존 동작에 영향을 주지 않도록 수정하는 방법이 필요합니다.
크게 상수, 함수, 클래스, 인터페이스로 나눌 수 있습니다.
- 상수
일반적인 리터럴을 사용하게 되면 시간이 지났을 때 해당 리터럴 값의 의미를 파악하기 어려워지고 해당 값으로 변경이 이루어져야할 때 오랜 시간이 걸릴 수 있습니다.
fun isPasswordValid(text: String): Boolean {
if (text.length < 7) {
return false
} else {
return true
}
}
val MIN_PASSWORD_LENGTH = 7
fun isPasswordValid(text: String): Boolean {
if (text.length < MIN_PASSWORD_LENGTH) {
return false
} else {
return true
}
}
위의 코드를 살펴보면 isPasswordValid 함수가 두개 있습니다. 내부의 구현을 보면 패스워드의 길이가 7보다 적을 경우 false를 리턴합니다.
첫번째 isPasswordValid 함수에서 7은 비밀번호 최소 길이라는 내용을 코드의 if문을 통해 확인해야하며 만약 코드내에서 여러 곳에 패스워드 최소길이가 쓰인다면 7이 반복하여 나오게 될것입니다.
두번째 isPasswordValid 함수처럼 처리를 한다면 7의 기능을 상수 이름으로 확인할 수 있고 여러 곳에서 중복되어서 쓰이고 변경이 이루어질 때 상수의 값 하나만 변경하여 처리가능하여 유지보수성이 올라가게 됩니다.
- 함수
함수의 경우 코드내에서 이루어지는 공통 동작들을 정리하여 만들어둘 수 있습니다.
만약 알림을 알려주는 공통창으로 어떤 dialog를 쓰고 있었다면 따로 확장함수를 만들어 사용할 수 있습니다. 이때 공통 알림창에서 변경이 이루어지는 경우 해당 함수를 수정하여 전체 변경이 가능합니다.
함수의 경우 단순한 추상화로 사용되지만 하나의 동작을 나타내며 상태를 유지할 수 없다는 제한이 있습니다.
- 클래스
클래스의 경우 함수와 다르게 상태를 가질 수 있고 여러 개의 함수로 이루어질 수 있습니다.
내부의 프로퍼티를 의존성 주입을 통해 가져올 수 있고 mock객체를 활용하여 해당 클래스를 테스트할 수 있습니다.
클래스의 경우 현재 내부 동작을 확인할 수 있고 open 클래스의 경우 서브 클래스에서 현재 클래스의 내용들을 제공할 수있습니다.
이를 추상적이게 만들기 위해서는 인터페이스에 숨기는 작업이 필요합니다.
- 인터페이스
인터페이스는 어떤 기능들을 담고 있으며 내부 구현이 되어있지 않은 상태로 만들어집니다.
이를 클래스에서 상속받게 된다면 클래스는 인터페이스에 감싸진 형태로 동작을 할 수있으며 사용자가 인터페이스 형태로 받게 된다면 내부 동작을 신경쓰지 않고 인터페이스 기능으로만 사용할 수 있습니다.
이처럼 다양한 방법으로 코드를 추상화시키고 만들어나갈 수 있습니다.
하지만 추상화는 코드작성을 위해 기존 동작에서 추가적인 비용이 들어가고 많은 동작을 숨기게 된다면 결과를 이해하는데 힘이들기 때문에 적절한 사용이 필요합니다.
7. API 안정성을 확인하라
프로그래밍을 할 때 많은 API를 사용하여 개발하게 됩니다.
API는 다양한 버전으로 출시되며 보안적인 이슈로 Deprecated되는 경우도 여럿있습니다.
개발이 이루어질 때 안정적인 API를 사용하기 위해 최신 라이브러리로 업데이트를 하거나 문제가 발생할 경우 새로운 API를 도입하게 위해 프로젝트 내에 의견을 제시하여 수정하는 작업이 필요합니다.
8. 외부 API를 랩해서 사용하라
동작이 보장되지 않는다고 판단한 API는 클래스에 랩해서 사용하며 문제가 발생시에 래퍼를 변경하면 됩니다.
특정 라이브러리에서 문제가 발생한다면 래퍼에서 라이브러리를 변경할 수 있고 피룡한 경우 동작을 추가, 수정할 수 있습니다.
9. 요소의 가시성을 최소화하라
API는 간결하게 설계하는 것이 중요합니다.
적은 인터페이스의 경우 배우기 쉽기 때문에 유지보수가 쉬워집니다. 간결하여 요소 자체가 적어진다면 테스트해야할 내용 또한 줄어들게됩니다.
변경을 가할 때 기존의 것을 제거하기보다 새로운 것을 만드는 것이 쉽습니다. 기존의 것을 제거한다면 기존 인터페이스에서 사용되어지고 있던 코드가 정상동작을 하지 않아 수정해야 할 부분이 추가적으로 늘어나게 됩니다.
만약 이러한 제한이 필수적이라면 대체제를 제공하여 사용자가 수정하기 쉽도록 알려주어야 합니다.
가시성을 제한하기 위해 가시성 한정자(public, private, protected, internal)을 사용할 수 있습니다.
10. 문서로 규약을 정의하라
기본적으로 코드 작성시 주석을 사용하지 않아도 이해하기 쉽도록 적절한 네이밍을 이용하여 사용자에게 알려줄 수 있지만 추가적인 정보를 알려줄 필요가 있을 때 주석을 이용하여 알려줍니다.
주석 작성시 흔히 볼수있는 KDoc 형식으로 작성되어지는 것이 좋습니다.
문서로 규약을 지정하고 사용자가 쓸 타입으로 나오게 된다면 여러 곳에서 사용되어질 때 해당 동작이 일관성있어야합니다.
여기서 지켜야할 원칙은 리스코프 치환 원칙으로 클래스가 어떤 동작을 할 것이라 예측되면 서브클래스도 이를 보장해야한다는 내용입니다.
추가적인 코드설계 관련 내용들이 있어 다음 포스팅에 이어서 작성하겠습니다.
참고
- Effective Kotlin (Marcin Moskala)