시배's Android
Effective Kotlin | 4장. 추상화 본문
아이템26. 함수 내부의 추상화 레벨을 통일하라
- 계층이 잘 분리되면 무엇이 좋을까요?
- 어떤 계층에서 작업할 때 그 아래의 계층은 이미 완성되어 있으므로, 해당 계층만 생각하면 된다는 것입니다.
- 즉, 전체를 이해할 필요가 없어지는 것입니다.
추상화 레벨
- 높은 레벨로 갈수록 물리 장치로부터 점점 멀어집니다.
- 프로그래밍에서는 일반적으로 높은 레벨일수록 프로세서로부터 멀어진다고 표현합니다.
- 높은 레벨일수록 걱정해야 하는 세부적인 내용들이 적습니다.
- 높은 레벨일수록 단순함을 얻지만, 제어력을 잃습니다.
- 예를 들어 C언어는 메모리 관리를 직접 할 수 있습니다. 반면, 자바는 가비지 컬렉터가 자동으로 메모리를 관리해 줍니다.
추상화 레벨 통일
- 함수도 높은 레벨과 낮은 레벨을 구분해서 사용해야 한다는 원칙이 있습니다.
- 이를 추상화 레벨 통일 SLA 원칙이라고 부릅니다.
class CoffeeMachine {
fun makeCoffee() {
// 수백 개의 변수를 선언
// 복잡한 로직을 처리
// 낮은 수준의 최적화도 여기에서 잔뜩
}
}
->
class CoffeeMachine {
fun makeCoffee() {
boilWater()
brewCoffee()
pourCoffee()
pourMilk()
}
}
- 아래의 makeCoffee 함수는 읽고 이해하기 쉬우며, 누군가가 낮은 레벨을 이해해야 한다면, 해당 부분의 코드만 살펴보면 됩니다.
- 간단한 추상화를 추출해서 가독성을 크게 향상시킨 것입니다.
- 함수는 작아야 하며, 최소한의 책임만을 가져야 한다라는 일반적인 규칙입니다.
- 재사용과 테스트가 쉬워집니다.
프로그램 아키텍처의 추상 레벨
- 추상화를 구분하는 이유는 서브시스템의 세부 사항을 숨김으로써 상호 운영성과 플랫폼 독립성을 얻기 위함입니다.
- 모듈을 분리하면 계층 고유의 요소를 숨길 수 있습니다.
- 계층화가 잘 된 프로젝트는 어떤 계층 위치에서 코드를 보아도 일관적인 관점을 얻을 수 있습니다.
아이템27. 변화로부터 코드를 보호하려면 추상화를 사용하라
함수와 클래스 등의 추상화로 실질적인 코드를 숨기면, 사용자가 세부 사항을 알지 못해도 괜찮다는 장점이 있습니다.
상수
- 리터럴은 아무것도 설명하지 않습니다.
- 코드에서 반복적으로 등장할 때 문제가 됩니다.
- 상수 프로퍼티로 변경하면 해당 값에 의미 있는 이름을 붙일 수 있으며, 상수의 값을 변경해야 할 때 훨씬 쉽게 변경할 수 있습니다.
ex) MIN_PASSWORD_LENGTH = 7
함수
- 많이 사용되는 알고리즘은 확장 함수로 만들어서 사용할 수 있습니다.
fun Context.toast(
message : String,
duration : Int = Toast.LENGTH_LONG
) {
Toast.makeText(this, message, duration).show()
}
- 토스트를 출력하는 방법이 변경되어도, 확장 함수 부분만 수정하면 되므로 유지보수성이 향상됩니다.
- 만약 다른 형태의 방식으로 출력해야 한다면, 어떻게 해야 할까요?
- 함수의 이름을 직접 바꾸는 것은 위험할 수 있습니다.
- 다른 모듈이 이 함수에 의존하고 있다면, 다른 모듈에 큰 문제가 발생할 것입니다.
- 파라미터는 한꺼번에 바꾸기가 쉽지 않으므로, 메시지의 지속시간을 나타내기 위한 Toast.LENGTH_LONG이 계속 사용되고 있다는 문제도 있습니다.
- 함수는 추상화를 표현하는 수단이며, 함수 시그니처는 이 함수가 어떤 추상화를 표현하고 있는지 알려 줍니다.
- 의미 있는 이름은 굉장히 중요합니다.
- 함수는 매우 단순한 추상화지만, 제한이 많습니다.
- 상태를 유지하지 않습니다.
클래스
- 클래스가 함수보다 더 강력한 이유는 상태를 가질 수 있으며, 많은 함수를 가질 수 있다는 점 때문입니다.
- 의존성 주입 프레임워크를 사용하면, 클래스 생성을 위임할 수도 있습니다.
- mock 객체를 활용해서 해당 클래스에 의존하는 다른 클래스의 기능을 테스트할 수 있습니다.
- 메시지를 출력하는 더 다양한 종류의 메서드를 만들 수도 있습니다.
- 여전히 한계는 존재합니다. final이라면, 해당 클래스 타입 아래에 어떤 구현이 있는지 알 수 있습니다.
- open 클래스를 활용하면 조금은 더 자유를 얻을 수 있습니다.
인터페이스
- 라이브러리를 만드는 사람은 내부 클래스의 가시성을 제한하고, 인터페이스를 통해 이를 노출하는 코드를 많이 사용합니다.
- 인터페이스 뒤에 객체를 숨김으로써 실질적인 구현을 추상화하고, 사용자가 추상화된 것에만 의존하게 만들 수 있는 것입니다.
- coupling을 줄일 수 있는 것입니다.
- faking이 mocking보다 간단하므로 테스트할 때 좋습니다.
- 선언과 사용이 분리되어 있으므로, 변경에 자유롭습니다.
추상화가 주는 자유
추상화를 하는 방법
- 상수로 추출
- 동작을 함수로 래핑
- 함수를 클래스로 래핑
- 인터페이스 뒤에 클래스를 숨긴다
- 특수한 객체로 래핑한다
추상화의 문제
- 코드를 읽는 사람이 해당 개념을 배우고, 잘 이해해야 합니다.
- 추상화도 비용이 발생합니다.
- 추상화는 많은 것을 숨길 수 있는 테크닉입니다. 생각할 것을 어느 정도 숨겨야 개발이 쉬워지는 것도 사실이지만 너무 많은 것을 숨기면 결과를 이해하는 것 자체가 어려워집니다.
어떻게 균형을 맞춰야 할까?
- 팀의 크기
- 팀의 경험
- 프로젝트의 크기
- feature 세트
- 도메인 지식
위와 같은 요소들을 고려해야 합니다.
- 많은 개발자가 참여하는 프로젝트는 이후에 객체 생성과 사용 방법을 변경하기 어렵습니다. 따라서 추상화 방법을 사용하는 것이 좋습니다. 최대한 모듈과 부분을 분리하는 것이 좋습니다.
- DI 프레임워크를 사용하면, 생성이 얼마나 복잡한지는 신경 쓰지 않아도 됩니다.
- 테스트를 하거나, 다른 애플리케이션을 기반으로 새로운 애플리케이션을 만든다면 추상화를 사용하는 것이 좋습니다.
- 프로젝트가 작고 실험적이라면, 추상화를 하지 않고도 직접 변경해도 괜찮습니다.
아이템28. API 안정성을 확인하라
- API가 변경되고, 개발자가 이를 업데이트했다면, 여러 코드를 수동으로 업데이트 해야합니다.
- 사용자가 새로운 API를 배워야 합니다.
- 일반적으로 버전을 활용해서 라이브러리와 모듈의 안정성을 나타냅니다.
- 일반적으로는 시멘틱 버저닝을 사용합니다.
- MAJOR : 호환되지 않는 수준의 API 변경
- MINOR : 이전 변경과 호환되는 기능을 추가
- PATCH : 간단한 버그 수정
- 안정적인 APi에 새로운 요소를 추가할 때, 아직 해당 요소가 안정적이지 않다면, 다른 브랜치에 해당 요소를 두는 것이 좋습니다.
- 일부 사용자가 이를 사용하도록 허용하려면, 일단 Experimental 메타 어노테이션을 사용해서 사용자들에게 알려 주는 것이 좋습니다.
- 안정적인 API의 일부를 변경해야 한다면, 전환하는 데 시간을 두고 Deprecated 어노테이션을 활용해서 사용자에게 미리 알려 줘야 합니다.
- ReplaceWith를 붙여 대안이 있다면 자동 전환을 할 수 있도록 하는 것도 좋습니다.
아이템29. 외부 API를 랩해서 사용하라
불안정하다고 판단되는 외부 라이브러리 API를 랩해서 사용하면 다음과 같은 자유와 안정성을 얻을 수 있습니다.
- 문제가 있다면 래퍼만 변경하면 되므로, API 변경에 쉽게 대응할 수 있습니다.
- 프로젝트의 스타일에 맞춰서 API의 형태를 조정할 수 있습니다.
- 특정 라이브러리에서 문제가 발생하면, 래퍼를 수정해서 다른 라이브러리를 사용하도록 코드를 쉽게 변경할 수 있습니다.
- 필요한 경우 쉽게 동작을 추가하거나 수정할 수 있습니다.
단점으로는 다음과 같은 것들이 있습니다.
- 래퍼를 따로 정의해야 합니다.
- 다른 개발자가 프로젝트를 다룰 때, 어떤 래퍼들이 있는지 따로 확인해야 합니다.
- 래퍼들은 프로젝트 내부에서만 유효하므로, 문제가 생겨도 질문할 수 없습니다.
아이템30. 요소의 가시성을 최소화하라
- 클래스의 상태를 나타내는 프로퍼티를 외부에서 변경할 수 있다면, 클래스는 자신의 상태를 보장할 수 없습니다.
class CounterSet<T>(
private val innerSet : MutableSet<T> = setOf()
) : MutableSet<T> by innterSet {
var elementsAdded : Int = 0
private set
override fun add(element : T) : Boolean {
elementsAdded++
return innerSet.add(element)
}
override fun addAll(elements: Collection<T>) : Boolean {
elementsAdded += elements.size
return innerSet.add(element)
}
}
- 일반적으로 코틀린에서는 접근자의 가시성을 제한해서 모든 프로퍼티를 캡슐화하는 것이 좋습니다.
- 객체 상태를 보호하는 것이 더 중요해집니다.
- 가시성이 제한될수록 클래스의 변경을 쉽게 추적할 수 있으며, 프로퍼티의 상태를 더 쉽게 이해할 수 있습니다.
가시성 한정자 사용하기
- public : 어디에서나 볼 수 있습니다.
- private : 클래스 내부에서만
- protected : 클래스와 서브클래스 내부에서만
- internal : 모듈 내부에서만
아이템31. 문서로 규약을 정의하라
규약
- 어떤 행위를 설명하면 사용자는 이를 약속으로 취급하며, 이를 기반으로 스스로 자유롭게 생각하던 예측을 조정합니다.
- 규약을 정의해두면, 클래스를 만드는 사람과 사용하는 사람 모두 미리 정의된 규약에 따라 독립적으로 작업할 수 있습니다.
규약 정의하기
규약을 정의하는 방법은 다양합니다.
- 이름 : 이름만으로 동작을 예측할 수 있다.
- 주석과 문서: 모든 규약을 적을 수 있는 방법이다.
- 타입: 타입은 객체에 대한 많은 것을 알려준다. 리턴 타입과 아규먼트 타입은 큰 의미가 있으며, 자주 사용되는 타입의 경우에는 타입만 봐도 어떻게 사용하는지 알 수 있지만, 일부 타입은 문서에 추가로 설명해야 한다.
KDoc 형식
- 주석으로 함수를 문서화할 때 사용되는 공식적인 형식을 KDoc이라고 부르며, /**로 시작해서 */로 끝납니다.
- 또한 이 사이의 모든 줄은 일반적으로 *으로 시작합니다.
- KDoc 주석의 구조는 다음과 같습니다.
- 첫 번째 부분은 요소에 대한 요약 설명
- 두 번째 부분은 상세 설명
- 이어지는 줄은 모두 태그로 시작하며, 추가적인 설명을 위해 사용된다.
- 사용할 수 있는 태그는 다음과 같습니다.
- @param : 함수 파라미터 또는 클래스, 프로퍼티, 함수 타입 파라미터
- @return: 함수의 리턴 타입 값
- @constructor: 클래스의 기본 생성자
- @receiver: 확장 함수의 리시버
- @property : 이름을 갖고 있는 클래스의 프로퍼티, 기본 생성자에 정의된 프로퍼티에 사용
- @throws , @exception : 메서드 내부에서 발생할 수 있는 예외
- @sample : 정규화된 형식 이름을 사용한 함수의 사용 예
- @see : 특정한 클래스 또는 메서드에 대한 링크
- @author: 요소의 작성자
- @since: 요소에 대한 버전
- @supress: 이를 지정하면, 만들어지는 문서에서 해당 요소가 제외된다. 공식 API에 포함할 필요가 없는 요소에 지정
아이템32. 추상화 규약을 지켜라
class Employee {
private val id = 2
override fun toString() = "User(id=$id)"
private fun privateFunction() {
println("Private function called")
}
}
fun callPrivateFunction(employee : Employee) {
employee::class.declaredMemberFunctions
.first { it.name == "privateFunction" }
.apply { isAccessible = true }
.call(employee)
}
- 무언가를 할 수 있다는 것이 그것을 해도 괜찮다는 의미는 아닙니다.
- 현재 코드는 private 프로퍼티와 private 함수의 이름과 같은 세부적인 정보에 매우 크게 의존하고 있습니다.
- 이러한 이름은 규약이라고 할 수 없기 때문에, 언제든지 변경될 수 있습니다.
- 만약 규약을 지키지 않는다면, 객체가 제대로 동작하지 않을 수 있습니다.
ex) 원래 세트는 중복을 허용하지 않는데, equals가 제대로 구현되지 않았을 경우 중복을 허용해 버립니다.
'Book > Effective Kotlin' 카테고리의 다른 글
Effective Kotlin | 6장. 클래스 설계 (0) | 2024.03.14 |
---|---|
Effective Kotlin | 5장. 객체 생성 (0) | 2024.03.11 |
Effective Kotlin | 3장. 재사용성 (0) | 2024.02.21 |
Effective Kotlin | 2장. 가독성 (0) | 2024.02.13 |
Effective Kotlin | 1장. 안정성 (1) | 2024.02.05 |