이 글은 2024년 3월 21일에 일본 어로 먼저 발행된 기사를 번역한 글입니다.
LY Corporation은 높은 개발 생산성을 유지하기 위해 코드 품질 및 개발 문화 개선에 힘쓰고 있습니다. 이를 위해 다양한 노력을 하고 있는데요. 그중 하나가 Review Committee 활동입니다.
Review Committee에서는 머지된 코드를 다시 리뷰해 리뷰어와 작성자에게 피드백을 주고, 리뷰하면서 얻은 지식과 인사이트를 Weekly Report라는 이름으로 매주 공유하고 있습니다. 이 Weekly Report 중 일반적으로 널리 적용할 수 있는 주제를 골라 블로그에 코드 품질 개선 기법 시리즈를 연재하고 있습니다.
이번에 블로그로 공유할 Weekly Report의 제목은 '함수만 보고 관계는 보지 못한다'입니다.
함수만 보고 관계는 보지 못한다
코드를 작성하다 보면 종종 루프를 중첩해야 할 때가 있습니다. '페이지'나 '청크(chunk)'로 분할된 데이터를 처리하는 것이 그 대표적인 예 중 하나입니다.
여러 Item
으로 청크가 생성되고, 다시 이 청크가 모여 페이지를 구성하는 상황을 생각해 보겠습니다. 페이지의 데이터 모델 Page
는 다음과 같이 정의했습니다.
class Page(val index: UInt, val chunk: List<Item>, val hasNext: Boolean)
각 Page
는 index
에 따라 순서가 정해지며, 뒤따르는 Page
가 존재한다면(해당 Page
가 마지막 페이지가 아니라면) hasNext
는 true
입니다. 이 Page
를 사용해 모든 Item
의 메타데이터를 저장하는 함수 saveAllItemMetadata
를 다음과 같이 구현했습니다. 이 함수는 page.hasNext
가 true
면 다음 Page
를 가져와서 그 안에 포함된 Item
의 메타데이터를 저장합니다.
fun saveAllItemMetadata() {
var index = 0u
while (true) {
val page = requestPage(index) ?: return
for (item in page.chunk) {
val itemMetadata = calculateItemMetadata(item)
repository.saveMetadata(item.id, itemMetadata)
}
if (!page.hasNext) {
return
}
index = page.index + 1u
}
}
이 함수는 while
과 for
로 중첩된 루프가 있어 가독성이 좋지 않습니다. 그래서 다음과 같이 내부 루프를 private
함수로 추출했다고 가정해 보겠습니다.
fun saveAllItemMetadata() {
var index = 0u
while (true) {
val page = requestPage(index) ?: return
saveMetadataInPage(page)
if (!page.hasNext) {
return
}
index = page.index + 1u
}
}
private fun saveMetadataInPage(page: Page) {
for (item in page.chunk) {
val itemMetadata = calculateItemMetadata(item)
repository.saveMetadata(item.id, itemMetadata)
}
}
이렇게 추출해도 가독성이 크게 개선되었다고 볼 수는 없습니다. 더 나은 리팩토링 방법은 없을까요?
관계에서 숲을 보기
앞서 설명한 리팩토링으로 가독성이 크게 개선되지 않은 이유 중 하나는 함수의 경계와 의미 단위의 경계가 일치하지 않기 때문입니다. saveAllItemMetadata
가 수행하는 일을 요약하면 다음 두 가지로 나타낼 수 있습니다.
- 모든
Item
조회하기 - 해당
Item
의 메타데이터 저장하기
saveMetadataInPage
를 추출하면 '모든 Item
조회하기'라는 코드가 두 군데로 나뉩니다. 그 결과 'saveAllItemMetadata
가 무엇을 하는지'를 이해하기가 오히려 더 어려워졌습니다. 단순히 추출하기 쉽다는 이유로 내부 구조를 그대로 추출하면 이런 상황이 발생할 수 있습니다. 따라서 리팩토링으로 추출할 때는 추출의 용이성보다는 어떻게 개선될지에 초점을 맞춰야 하며, 이를 위해서는 추출할 때 '코드가 무엇을 하는지' 그 의미 단위에 주의를 기울여야 합니다.
위 사례의 경우 내부 루프를 추출하는 대신 Item
의 열을 조회하는 함수를 생성하는 것이 좋습니다. Kotlin에서는 Sequence<Item>
또는 Iterator<Item>
와 같은 클래스로 표현할 수 있습니다. 다음 requestItemSequence
에서는 Item
의 열을 Sequence<Item>
로 반환합니다.
fun saveAllItemMetadata() {
for (item in requestItemSequence()) {
val itemMetadata = calculateItemMetadata(item)
repository.saveMetadata(item.id, itemMetadata)
}
}
fun requestItemSequence(): Sequence<Item> = sequence {
var page: Page? = requestPage(0u)
while (page != null) {
yieldAll(page.chunk)
page = if (page.hasNext) requestPage(page.index + 1u) else null
}
}
이렇게 하면 중첩된 루프를 하나의 루프로 보이게 할 수 있습니다. 결과적으로 requestItemSequence
의 내용을 이해할 필요 없이 saveAllItemMetadata
를 읽는 것만으로 '각 Item
의 메타데이터를 생성하고 저장한다'는 흐름을 파악할 수 있게 되었습니다.
이와 같은 리팩토링 접근 방식은 루프 중첩뿐 아니라 조건 분기 중첩이나 데 이터 구조 중첩에도 적용할 수 있으니, 추출할 때는 기존 구조를 유지하는 것이 좋을지 아니면 재구성하는 것이 좋을지 고려해 보시길 바랍니다.
한 줄 요약: 추출할 때는 '코드가 무엇을 하는지' 그 의미 단위에 주의를 기울인다.
키워드:
extraction
,boundaries of concept
,nested loop