이 글은 2024년 5월 23일에 일본어로 먼저 발행된 기사를 번역한 글입니다.
LY Corporation은 높은 개발 생산성을 유지하기 위해 코드 품질 및 개발 문화 개선에 힘쓰고 있습니다. 이를 위해 다양한 노력을 하고 있는데요. 그중 하나가 Review Committee 활동입니다.
Review Committee에서는 머지된 코드를 다시 리뷰해 리뷰어와 작성자에게 피드백을 주고, 리뷰하면서 얻은 지식과 인사이트를 Weekly Report라는 이름으로 매주 공유하고 있습니다. 이 Weekly Report 중 일반적으로 널리 적용할 수 있는 주제를 골라 블로그에 코드 품질 개선 기법 시리즈를 연재하고 있습니다.
이번에 블로그로 공유할 Weekly Report의 제목은 '설명의 핵심은 첫 문장에 있다'입니다.
설명의 핵심은 첫 문장에 있다
클래스나 함수가 복잡하거나 직관적이지 않을 때 문서화 주석을 작성하면 이해를 도울 수 있습니다. 다음 코드의 문서화 주석은 String을 받아 List<List<String>>을 반환하는 함수의 작동 방식을 설명하고 있습니다.
/**
* 주어진 [englishText]를 마침표(`'.'`)로 분할하고, 분할된 문자열에서 빈 문자열(`""`)을 제거합니다.
* 그 다음 각 분할된 문자열을 다시 공백(`' '`) 또는 쉼표(`','`)로 분할한 후 빈 리스트를 제거합니다.
* 마지막으로 그 결과(문자열의 중첩 리스트)를 반환값으로 반환합니다.
*/
fun ...(englishText: String): List<List<String>> {
val sentences = englishText
.split(SENTENCE_SEPARATOR)
.asSequence()
val wordsInSentences = sentences
.map { it.split(WORD_SEPARATOR_REGEX).filter(String::isNotEmpty) }
return wordsInSentences
.filter(List<String>::isNotEmpty)
.toList()
}
...
private val SENTENCE_SEPARATOR: String = "."
private val WORD_SEPARATOR_REGEX: Regex = """[ ,]+""".toRegex()
이 문서화 주석이 이해하기 쉽다고 말하기는 어렵습니다. 어떻게 개선할 수 있을까요?
시작이 중요하다
문서화 주석은 첫 문장만 읽어도 개요를 이해할 수 있도록 작성해야 하지만 위 문서화 주석은 그렇지 않습니다. 설명을 끝까지 읽지 않으면 작동 방식을 이해하기 어렵습니다.
첫 문장만으로 이해할 수 있는 문서화 주석을 작성하려면 다음 두 가지 사항에 주의해야 합니다.
- 가장 중요한 요소를 선택한다.
- 코드보다 높은 추상화 수준으로 설명한다.
이 방침에 따라 앞서 언급한 문서화 주석을 개선해 봅시다. 먼저 기존 문서화 주석에 기술된 요소를 나열해 보겠습니다.
- 문자열
englishText가 입력된다. - 마침표로 문자열을 분할한다.
- 빈 문자열을 제거한다.
- 공백 또는 쉼표로 분할한다.
- 빈 리스트를 제거한다.
- 문자열의 중첩 리스트를 반환한다.
이 중에서 가장 중요한 요소를 먼저 선택해 보겠습니다. 이 함수의 목적은 반환값을 얻는 것이므로 '문자열의 중첩 리스트를 반환한다'가 가장 중요하다고 볼 수 있습니다. 그런데 현재 '문자열의 중첩 리스트'라는 표현의 추상화 수준은 코드와 비슷한 수준입니다. 이를 개선하기 위해 '중첩 리스트'라는 것의 의미를 생각해 봅시다. 바깥쪽 리스트는 englishText를 마침표로 분할한 것이므로 '영어 문장'을 나타내며, 안쪽 리스트는 공백이나 쉼표로 분할한 것에 해당하므로 '영어 단어'를 나타낸다고 할 수 있습니다. 이를 바탕으로 문서화 주석의 첫 문장은 다음과 같이 개선할 수 있습니다.
/**
* 영어 문자열 [englishText]를 문장 단위로 분할하고, 다시 단어 단위로 분할해서 얻은 리스트의 리스트를 반환합니다.
'어떤 문자로 분할할지'나 '어떤 것을 제외할지'와 같은 세부 사항은 첫 번째 중요한 포인트 뒤에 추가합니다.
* 여기서 '문장'은 마침표(`'.'`)로 구분된 부분 문자열을 의미하고,
* '단어'는 공백(`' '`) 또는 쉼표(`','`)로 구분된 부분 문자열을 의미합니다.
* 또한 빈 문장이나 빈 단어는 반환값에서 제외됩니다.
만약 코너 조건이나 경계 조건이 많다면 인수와 반환값 예시를 보여주는 것도 좋습니다.
* 예를 들어 `" a bc. .d,,."`가 주어지면 `[["a", "bc"], ["d"]]`를 반환합니다.
*/
개선한 전체 문서화 주석은 다음과 같습니다.
/**
* 영어 문자열 [englishText]를 문장 단위로 분할하고, 다시 단어 단위로 분할해서 얻은 리스트의 리스트를 반환합니다.
*
* 여기서 '문장'은 마침표(`'.'`)로 구분된 부분 문자열을 의미하고,
* '단어'는 공백(`' '`) 또는 쉼표(`','`)로 구분된 부분 문자열을 의미합니다.
* 또한 빈 문장이나 빈 단어는 반환값에서 제외됩니다.
* 예를 들어 `" a bc. .d,,."`가 주어지면 `[["a", "bc"], ["d"]]`를 반환합니다.
*/
위와 같이 작성하면 첫 문장만 읽어도 개요를 파악할 수 있고, 더 자세한 내용을 알고 싶을 때에는 이어지는 문장을 읽으면 됩니다.
문서화 주석 외 주석도 마찬가지
문서 화 주석 외 주석(인라인 주석 등)에서도 '무엇을 먼저 설명할지'가 중요합니다. 예를 들어 다음 임시방편 코드의 설명은 개선할 여지가 있습니다.
val someValue = device.someValue
device.someFunction()
...
// 강제적으로 `someValue`를 이전 값으로 리셋합니다.
// 기기 X에서는 `someFunction`이 호출되면 `someValue`의 값을 변경하는데
// 이 동작은 foo API의 사양을 위반하기 때문입니다.
device.someValue = someValue
위 주석은 처음에 '무엇을 하는지'를 설명하고 있는데 임시방편 코드에서는 '무엇을 하는지'가 그다지 중요하지 않습니다. 이런 경우에는 존재 이유를 먼저 쓰는 것이 좋습니다.
// 이 코드는 기기 X 고유의 버그를 회피하기 위한 것입니다.
// 기기 X는 `someFunction` 내에서 `someValue`를 변경하지만 이는 foo API의 사양을 위반합니다.
device.someValue = someValue
또 다른 예로 TODO 주석도 살펴보겠습니다. TODO 주석은 '앞으로 어떻게 하고 싶은지'나 '어떤 상태가 이상적인지'가 가장 중요한 경우가 많습니다. 그러니 이를 먼저 기술한 후 '현재 상태가 좋지 않은 이유'나 '왜 즉시 개선할 수 없는지' 등을 기술하는 편이 좋습니다. 예를 들어 다음 두 TODO 주석 중에서는 후자가 더 낫습니다.
// TODO: `var abc`와 `var xyz`는 모두 `fun foo`에 할당되므로
// `foo`를 리팩토링하기 전까지는 변경하기 어렵다.
// 본래 `abc`는 `xyz`에서 계산하여 구할 수 있으므로 삭제할 수 있다.
var abc :...
var xyz :...
// TODO: `var abc`의 값을 `var xyz`에서 구하도록 하고 `abc`를 삭제한다.
// 다만 이를 위해서는 먼저 `fun foo`를 리팩토링해야 한다.
// (`foo`가 `abc`와 `xyz`에 할당하기 때문이다)
var abc :...
var xyz :...
한 줄 요약: 주석을 작성할 때는 무엇을 먼저 설명할지 신중하게 선택한다.
키워드:
comment,documentation,short summary