grep

Engineering

코드 품질 개선 기법 27편: 티끌이 모여 태산이 되듯 의존성도 쌓이면

라인

2025년 12월 23일

원문에서 보기 ↗

이 글은 2024년 5월 30일에 일본어로 먼저 발행된 기사를 번역한 글입니다.

LY Corporation은 높은 개발 생산성을 유지하기 위해 코드 품질 및 개발 문화 개선에 힘쓰고 있습니다. 이를 위해 다양한 노력을 하고 있는데요. 그중 하나가 Review Committee 활동입니다.

Review Committee에서는 머지된 코드를 다시 리뷰해 리뷰어와 작성자에게 피드백을 주고, 리뷰하면서 얻은 지식과 인사이트를 Weekly Report라는 이름으로 매주 공유하고 있습니다. 이 Weekly Report 중 일반적으로 널리 적용할 수 있는 주제를 골라 블로그에 코드 품질 개선 기법 시리즈를 연재하고 있습니다.

이번에 블로그로 공유할 Weekly Report의 제목은 '티끌이 모여 태산이 되듯 의존성도 쌓이면'입니다.

다음 LatestNewsSnippetUseCase는 뉴스 기사 스니펫을 NewsSnippet이라는 데이터 모델로 제공하는 클래스입니다. NewsSnippet 데이터 모델은 리포지터리의 데이터를 조합하고 문자열을 포매팅해 생성합니다.

class LatestNewsSnippetUseCase(
    private val locale: Locale,
    private val articleRepository: NewsArticleRepository,
    private val sourceRepository: NewsSourceRepository,
    private val stringTruncator: StringTruncator,
    private val timeFormatter: TimeTextFormatter = TimeTextFormatterImpl(locale),
    private val modelFactory: (title: String, content: String, dateText: String, source: String) -> NewsSnippet =
        ::NewsSnippet
) {
    fun getLatestSnippet(): NewsSnippet {
        val article = articleRepository.getLatestArticle()
        val articleText = stringTruncator.truncate(article.contextText, ARTICLE_TEXT_LENGTH, locale)
        val dateText = timeFormatter.toShortMonthDayText(article.timestampInMillis)
        val sourceName = sourceRepository.getSource(article.sourceId).shortName
        return modelFactory(article.title, articleText, dateText, sourceName)
    }

    companion object {
        private const val ARTICLE_TEXT_LENGTH = 280
    }
}

리포지터리는 두 개(NewsArticleRepository, NewsSourceRepository)가 있고, 이들은 복잡한 클래스(예: 네트워크에 의존)라고 가정해 봅시다. 문자열 포매터도 두 개(StringTruncator, TimeTextFormatter)가 있는데 아주 단순하게 구현돼 있다고 가정하겠습니다(원래 문자열 처리는 복잡해지는 경향이 있지만 설명을 위해 단순화합니다).

생성자 매개변수 modelFactory는 NewSnippet 인스턴스를 생성하는 함수를 나타냅니다. 기본 인수로는 NewSnippet의 생성자 참조인 ::NewSnippet를 정의합니다. 즉 기본 인수의 동작은 modelFactory(...) 호출과 생성자 NewSnippet(...)의 호출이 동일합니다.

여기서 StringTruncator와 TimeTextFormatter, NewsSnippet 이 세 가지를 다음과 같이 정의하며, StringTruncator의 구현체 StringTruncatorImpl의 생성자는 인수를 받지 않는다고 가정하겠습니다.

interface StringTruncator {
    fun truncate(string: String, length: Int, locale: Locale, suffix: String = "…" /* U+2026 */): String
}

interface TimeTextFormatter {
    fun toShortMonthDayText(millis: Long): String
}

class NewsSnippet(val title: String, val content: String, val dateText: String, val source: String)

이 코드에서 개선할 점이 있을까요?

주입의 존재 이유

간단히 말해 modelFactory를 외부에서 전달할 필요가 없습니다. 또한 StringTruncator와 TimeTextFormatter도 싱글톤 등에 의존하지 않는다면 인터페이스를 분리하고 외부에서 구현체를 전달(의존성 주입)할 필요가 없습니다. 이 코드에서 외부에서 전달할 수 있도록 만들어야 하는 것은 환경에 따라 달라지는 값(Locale)과 복잡한 클래스(리포지터리)뿐입니다.

따라서 코드를 더 단순화하려면 다음과 같이 재작성하는 것이 좋습니다.

class LatestNewsSnippetUseCase(
    private val locale: Locale,
    private val articleRepository: NewsArticleRepository,
    private val sourceRepository: NewsSourceRepository
) {
    private val stringTruncator: StringTruncator = StringTruncatorImpl()
    private val timeFormatter: TimeTextFormatter = TimeTextFormatterImpl(locale)
    
    suspend fun getLatestSnippet(): NewsSnippet {
        val article = articleRepository.getLatestArticle()
        val articleText = stringTruncator.truncate(article.contextText, ARTICLE_TEXT_LENGTH, locale)
        val dateText = timeFormatter.toShortMonthDayText(article.timestampInMillis)
        val sourceName = sourceRepository.getSource(article.sourceId).shortName
        return NewsSnippet(article.title, articleText, dateText, sourceName)
    }

    companion object {
        private const val ARTICLE_TEXT_LENGTH = 280
    }
}

의존성 주입을 활용할 때는 목적이 명확해야 합니다. 일반적으로 다음과 같은 목적으로 의존성을 주입합니다.

위와 같은 목적이 아닌 경우, 예를 들어 참조 투명(referential transparency)한 유틸리티 함수나 단순한 모델 클래스 등은 의존성을 주입할 필요가 거의 없으며, 불필요한 주입은 다음과 같은 문제를 일으킬 수 있습니다.


한 줄 요약: 의존성을 주입할 때는 그 목적을 명확히 한다.

키워드: dependency injection, dependency explicitness, constructor parameter