LY Corporation Tech Blog

LY Corporation과 LY Corporation Group(LINE Plus, LINE Taiwan and LINE Vietnam)의 기술과 개발 문화를 알립니다.

This post is also available in the following languages. Japanese, English

코드 품질 개선 기법 18편: 함수만 보고 관계는 보지 못한다

이 글은 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)

Pageindex에 따라 순서가 정해지며, 뒤따르는 Page가 존재한다면(해당 Page가 마지막 페이지가 아니라면) hasNexttrue입니다. 이 Page를 사용해 모든 Item의 메타데이터를 저장하는 함수 saveAllItemMetadata를 다음과 같이 구현했습니다. 이 함수는 page.hasNexttrue면 다음 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
    }
}

이 함수는 whilefor로 중첩된 루프가 있어 가독성이 좋지 않습니다. 그래서 다음과 같이 내부 루프를 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