Kotlin ‐ 문서로 규약을 정의하라[Effective Kotlin Item 30] - thought-corner/Backend-PlayGround GitHub Wiki

문서로 규약을 정의하라

fun Context.showMessage(
    message: String,
    duration: MessageLength = MessageLength.LONG
) {
    val toastDuration = when (duration) {
        MessageLength.SHORT -> Toast.LENGTH_SHORT
        MessageLength.LONG -> Toast.LENGTH_LONG
    }
    Toast.makeText(this, message, toastDuration).show()
}
  • 메시지 표시 방법을 자유롭게 변경할 수 있도록 showMessage() 함수를 추출했으나 이 기능을 문서화하지는 않았다.
  • 범용적으로 사용하기 위해 함수 이름을 showMessage()로 지정하고 메시지 타입 또한 구체적인 타입 대신 문자열로 받고 있지만 사용자들은 다른 의미로 받아들일 수 있다.
  • 코드뿐 아니라 KDoc 주석을 추가하면 함수의 기능과 사용법을 상세하게 설명할 수 있다.
/**
 * 사용자에게 짧은 메시지를 화면에 표시한다.
 *
 * 현재는 [Toast]로 구현되어 있으나, 호출하는 쪽은 표시 방식에 의존하지 않는다.
 * 따라서 추후 구현을 스낵바 등으로 교체해도 호출부는 영향을 받지 않는다.
 *
 * @param message 표시할 메시지 내용
 * @param duration 메시지 표시 시간. 기본값은 [MessageLength.LONG]
 */
fun Context.showMessage(
    message: String,
    duration: MessageLength = MessageLength.LONG
) {
    val toastDuration = when (duration) {
        MessageLength.SHORT -> Toast.LENGTH_SHORT
        MessageLength.LONG -> Toast.LENGTH_LONG
    }
    Toast.makeText(this, message, toastDuration).show()
}

규약

  • 추상화된 객체에 설명을 추가하면 사용자는 이를 규약으로 생각하고 어떤 방식으로 동작할 것인지 추측한다.
  • 그러한 예상되는 모든 동작을 요소 규약이라고 한다.
  • 규약이 잘 정의되면 작성자는 클래스 사용 방식에 대해 걱정할 필요가 없으며, 사용자는 내부적으로 구현된 방식에 대해 신경 쓸 필요가 없다.
  • 사용자는 실제 구현을 알지 못해도 규약에 의존할 수 있다.

규약의 정의

  • 이름 : 이름에 일반적인 개념이 포함되어 있다면 이름만 보고 어떻게 동작할 것인지 쉽게 알 수 있다. 예를 들어, sum() 메서드를 볼 때 어떻게 동작하는지 알기 위해 주석을 읽을 필요가 없다.
  • 주석과 문서 : 필요한 모든 것을 설명할 수 있는 가장 강력한 방법이다.
  • 타입 : 타입은 객체에 대해 많은 것을 알려준다. 각 타입은 잘 정의된 메서드들을 정의하며, 일부 타입은 문서에 그 책임이 정해져 있다. 함수를 볼 때 반환 타입과 인수 타입에 대한 정보는 매우 중요하다.

주석이 필요한지?

  • 가독성 있는 코드를 작성하는 데 집중해야 한다는 것에는 전적으로 동의한다. 하지만 요소에 주석을 추가하면 해당 요소를 더 잘 설명할 수 있으며, 규약도 정의할 수 있다는 점을 인지해야 한다.
  • 주석은 유용하고 중요한 경우도 많다.

KDoc 포맷

  • 주석을 사용하여 함수를 문서화할 때 주석을 표기하는 공식적인 형식을 KDoc이라고 한다.
  • 모든 KDoc 주석은 /**로 시작하고 */로 끝나며, 주석 내 모든 줄은 일반적으로 *로 시작한다.
  • KDoc 주석의 구조는 다음과 같다.
    • 문서 텍스트의 첫 번째 단락은 요소에 대한 요약이다.
    • 두 번째 단락은 요약을 자세하게 설명한다.
    • 이후에 각각의 줄은 태그로 시작된다. 태그는 구성 요소를 지정하여 어떤 역할을 하는지 설명한다.
태그 용도 비고
@param name 파라미터 설명 타입 파라미터도 @param T 형태로 가능
@return 반환값 설명 Unit 반환이면 생략
@throws Class 던지는 예외 설명 @exception과 동일
@exception Class @throws의 별칭
@receiver 확장 함수의 리시버 설명
@constructor 클래스의 주 생성자 설명 클래스 KDoc에 사용
@property name 주 생성자에 선언된 프로퍼티 설명 클래스 KDoc에 사용
@sample fqName 예시 코드를 함수 참조로 삽입 정규화된 함수명 지정
@see ref 관련 요소 참조(링크)
@author 작성자
@since 도입된 버전
@suppress 생성 문서에서 제외 공개 API지만 문서엔 숨길 때

타입 시스템과 기대치

  • 타입 계층은 객체를 나타내는 중요한 정보이다.
  • 공개 함수의 규약은 반드시 적절한 설명이 필요하다.
// ❌ Bad - 반환 타입이 String이지만 실제로는 null 같은 의미의 빈 값을 섞어 쓴다
// 호출부에서는 "있다"고 기대하지만 실제론 빈 문자열이 올 수도 있다.
fun findUser(id: Long): String? {
    return repository.get(id)?.name
}
// ⭕ Good - "없을 수 있다"를 타입(String?)으로 정직하게 드러낸다
fun findUser(id: Long): String? {
    return repository.get(id)?.name
}

구현 누출

  • 구현 세부사항은 항상 누출된다.
  • 프로그래밍 안에서 구현 세부사항이 누출되는데 예를 들어, 리플렉션을 사용하여 함수를 호출하면 일반 함수를 사용하는 것보다 성능이 훨씬 떨어진다.
  • 추상화를 할 때도 구현 사항이 누출될 수 있으나 최대한 숨기는 것이 좋다. 캡슐화를 통해 이를 보호하는데, 이 때 캡슐화는 '내가 허용하는 것만 할 수 있고 그 이상은 할 수 없다'라고 설명할 수 있다.