본문 바로가기

Reading Record/이펙티브 코틀린

[이펙티브 코틀린] Item 31 - 32. 문서 규약과 추상화 규약

Item 31. 문서로 규약을 정의하라

  • 함수가 무엇을 하는지 명화갛게 설명하고 싶다면 KDoc 주석을 붙여주는 것이 좋음
/**
* 이 프로젝트에서 짧은 메시지를 사용자에게 출력할 때 사용하는 기본 방식
* @param message 사용자에게 보여 줄 메시지
* @param length 메시지의 길이가 어느정도 되는지 나타내는 enum 값
*/
fun Context.showMessage(
    message: String,
    duration: MessageLength = MessageLength.LONG,
) {
    val toastDuration = when (duration) {
        MessageLength.SHORT -> Length.LENGTH_LONG
        MessageLength.LONG -> Length.LENGTH_LONG
    }
    
    Toast.makeText(this, message, toastDuration).show()
}
  • 행위가 문서화되지 않고 요소의 이름이 명확하지 않다면 이를 사용하는 사용자가 의도했던 추상화 목표가 아닌 현재 구현에 의존할 수 있기 때문에 예상되는 행위를 문서로 설명하면 좋음

규약

  • 어떤 행위를 설명하면 사용자는 이를 일종의 약속으로 취급하며 이를 기반으로 스스로 자유롭게 생각하던 예측을 조정
  • 이처럼 예측되는 행위를 요소의 규약이라고 부름
  • 잘 짜여진 규약은 클래스를 만든 사람은 클래스가 어떻게 사용될지 걱정 안해도 됨

규약 정의하기

  • 이름 : 일반적인 개념과 관련된 메소드는 이름만으로 동작을 예측할 수 있음
  • 주석과 문서 : 필요한 모든 규약을 적을 수 있는 방법
  • 타입 : 자주 사용되는 타입의 경우에는 타입만 보아도 어떻게 사용하는지 알 수 있지만 일부 타입은 문서에 추가로 설명해야할 필요가 있음

주석

  • 로버트 마틴 : 주석없이 읽을 수 없는 코드를 짜야함
  • 하지만 모든 일을 극단적으로 진행하는건 좋지 않고 주석을 적절하게 활용하면 더 많은 내용의 규약을 설명할 수 있음
  • listOf 와 같이 코틀린 표준 라이브러리에 적힌 주석은 코드를 이해하는데 도움이 됨

KDoc

주석을 사용하여 기능을 문서화 할 때 주석을 표시하는 공식 형식

/** 로 시작해서 */ 로 끝남

주석 구조

  • 첫번째 줄 : 기능 요약 설명
  • 두번째 줄 : 기능 자세한 설명
  • 그 뒤에 모든 줄 : @ tag로 시작, 파라미터를 설명하는데 사용
    • @param <name> : 함수의 값 매개변수 또는 클래스를 문서화
    • @return : 함수의 리턴 값
    • @constructor : 클래스의 기본 생성자
    • @receiver : 익스텐션 함수의 리시버
    • @property <name> : 명확한 이름을 갖고 있는 클래스의 프로퍼티를 문서화
    • @throw <class>, @exception <class> : 메소드 내부에서 발생할 수 있는 예외 사항
    • @sample <identifier> : 정규화된 형식 이름을 사용해서 함수의 사용 예를 문서화
    • @author : 요소의 작성자를 지정
    • @since : 요소에 대한 버전을 지정
    • @supress : 이를 지정하면 만들어진 문서에서 해당 요소가 제외

타입 시스템과 예측

  • 클래스와 인터페이스에도 여러 가지 예측이 들어가는데 클래스가 어떤 동작을 할 것이라 예측되면 서브 클래스도 이를 보장해야함
    • 이를 리스코프 치환 원칙이라고 부름
    • 해당 원칙은 객체 지향 프로그램에서 중요한데, S가 T의 서브 타입이라면 별도의 변경 없어도 T 타입 객체를 S 타입 객체로 대체할 수 있어야함

조금씩 달라지는 세부 사항

  • 구현의 세부 사항은 항상 달라질 수 있지만 최대한 많이 보호하는 것이 좋음
  • 즉 캡슐화가 많이 적용될 수록 사용자가 구현에 신경을 많이 쓸 필요없고 자유를 많이 갖게 됨

정리

  • 외부 API를 구현할 때는 규약을 잘 정의해야함
  • 이러한 규약은 이름, 문서, 타입을 통해 구현할 수 있음

Item 32. 추상화 규약을 지켜라

  • 규약은 개발자들의 단순한 합의이기 때문에 한쪽에서 규약을 위반할 수 있음
class Employee {
    private val id: Int = 2
    override fun toString() = "User(id=$id)"
    private fun privateFunction() {
        println("Private function called")
    }
}

fun callPrivateFunction(employee: Employee) {
    employee::class.declaredFunctions
        .first { it.name == "privateFunction" }
        .apply { isAccessible == true }
        .call(employee)
}

fun changeEmployeeId(employee: Employee, newId: Int) {
    employee::class.java.getDeclaredField("id")
        .apply { isAccessible = true }
        .set(employee, newId)
}

fun main() {
    val employee = Employee()
    callPrivateFunction(employee)
    changeEmployeeId(employee, 1)
    println(employee)
} 
  • 위와 같이 리플렉션을 활용하면 우리가 원하는 것을 열고 사용할 수 있는데 이런 코드는 private 프로퍼티와 private 함수의 이름과 같은 세부적인 정보에 매우 크게 의존하고 있기 때문에 변경에 매우 취약할 수 있음

상속된 규약

  • 클래스르 상속받거나 인터페이스를 확장할때는 규약을 지키는 것은 더더욱 중요
  • 예를 들어 equals와 hashCode 메서드를 가진 Any 클래스를 상속 받는데 이러한 메소드는 모두 우리가 반드시 존중하고 지켜야하는 규약이 있음
    • 만약 규약을 지키지 않는다면 객체가 제대로 동작 안할 수 있음
    • 아래와 같이 equals가 제대로 구현되지 않는다면 중복을 허용하게 되어 제대로 동작하지 않음
class Id(val id: Int) {
    override fun equals(other: Any?): Boolean = other is Id && other.id == id
}

fun main() {
    val set = mutableSetOf(Id(1))
    set.add(Id(1))    
    set.add(Id(1))
    println(set.size) // 3
}

정리

  • 프로그램 안정성을 위해 규약을 최대한 지키는 것이 좋음
  • 만약 규약을 깨야한다면 문서화를 잘해두는 것이 좋음