[Open Source] 첫 오픈소스 기여 도전기 (MockK #1356)

 

안녕하세요. 이번 글에서는 처음으로 오픈소스에 기여한 경험을 정리해 보려고 합니다.

그동안 오픈소스 기여는 실력이 뛰어난 개발자들만 하는 일이라고 생각해서 관심은 있었지만 선뜻 시작하지 못했는데요.

이번에 좋은 기회가 닿아 ‘오픈소스 기여모임 10기’에 참여하게 되었고, 그 과정에서 평소 관심이 있었던 테스트 프레임워크 MockK에 첫 PR을 올려

Merge까지 완료할 수 있었습니다. 이 글에서는 이슈를 고른 이유부터 구현과 검증, 그리고 진행하면서 겪었던 시행착오까지 공유해 보겠습니다.

 

 

 

 

이슈 선정 과정

오픈소스 기여를 처음 시도하면서 느낀 점은, 문제를 해결하는 기술적인 부분보다 기여할 만한 이슈를 찾는 과정이 더 어렵다는 것이었습니다.

어떤 이슈가 있는지, 그중에서 제가 도전하기에 적합한 것을 어떻게 고를지 고민이 많았는데요. 오픈소스 기여모임에서 제공해 준 가이드가 이슈를 찾는 방법부터 후보를 좁히는 기준까지 단계별로 정리되어 있어, 처음 도전하는 입장에서도 방향을 잡기 수월했습니다.

 

우선 가이드에 따라 제가 관심 있는 언어를 사용하면서도 운영이 활발한 저장소들을 먼저 찾아보았습니다. 이후 GitHub 이슈 수집기 를 활용해 제가 해결할 수 있는 난이도의 이슈가 있는지 살펴보았습니다.

 

저는 최종적으로 `mockk/mockk` 저장소를 선택했는데요. 평소 TDD를 지향하며 관련 공부를 해오던 터라 MockK에도 관심이 있었고, 이번 기여를 통해 내부 동작을 직접 확인해 보고 싶었습니다. Kotlin이 익숙하진 않았지만 같은 JVM 기반 언어라 코드 분석에는 큰 어려움이 없을 것이라 판단했고, MockK의 경우 메인테이너들의 활동이 활발하고, 이미 기여를 기다리고 있는 이슈들이 있어 첫 기여를 시작하기에 적절하다고 판단했습니다.

 

제가 최종적으로 선택한 이슈 #1356의 상세 내용은 다음과 같습니다.

  • 내용: `@InjectMockKs` 사용 시 `List<Type>` 형태의 주입이 지원되지 않는 문제였습니다.
  • 현상: 클래스 생성자 파라미터로 리스트를 받을 때, MockK가 해당 리스트를 적절히 구성하지 못해 예외가 발생하거나 주입에 실패하는 상황이었습니다.

 

 

소스 분석 및 해결 방법

이슈의 핵심은 `@InjectMockKs`로 생성자 주입을 할 때, 파라미터가 `List<Type>` 형태인 경우 MockK가 리스트를 구성하지 못해 주입이 실패한다는 점이었습니다. 단일 객체 주입은 정상 동작하지만, 여러 구현체를 한 번에 주입받는 형태는 지원되지 않는 상태였습니다.

 

 

흐름 파악

주입이 실패하는 원인을 찾기 위해 `MockInjector` 클래스를 중심으로 분석했습니다. MockK는 Kotlin의 리플렉션 기능을 활용해 런타임에 클래스의 생성자 정보를 확인하고, 파라미터 타입에 맞는 Mock 객체를 찾아 주입합니다. 저는 이 과정에서 각 파라미터에 값을 매칭하는 `matchParameter`와 `tryMatchingParameters` 함수의 흐름을 중점적으로 확인했습니다.

 

 

기존 로직 분석 (AS-IS)

기존의 `matchParameter`는 생성자 파라미터에 주입할 값을 찾을 때 이름(`lookupValueByName`) 또는 타입(`lookupValueByType`)을 기준으로만 탐색하고 있었습니다.

 

private fun matchParameter(param: KParameter): Any? =
    //{
    lookupValueByName(param.name, param.type.classifier)
    //}
        //{
        ?: lookupValueByType(param.type.classifier)
        //}
        //{
        ?: if (param.isOptional) null else throw MockKException("Parameter unmatched: $param")
        //}
// 가장 먼저 파라미터 이름을 기준으로 일치하는 Mock 객체가 있는지 검색합니다. // 이름 매칭에 실패하면 타입을 기준으로 검색하며, 이때 오직 단일 객체만 매칭을 시도합니다. // 모든 매칭에 실패하고 주입이 필수인 파라미터(isOptional=false)라면 예외를 발생시킵니다.

 

이 구조에서는 `List<BaseComponent>` 같은 파라미터가 들어오더라도, 리플렉션을 통해 리스트 내부에 담길 원소 타입을 분석하고 그에 맞는 Mock들을 수집하는 단계가 없었습니다. 결과적으로 MockK는 List 타입 자체에 매칭되는 단일 객체만을 찾으려다 실패하거나 `MockKException`을 발생시키게 됩니다.

 

 

해결 방법 (TO-BE)

리스트 파라미터는 성격상 단일 값으로 매칭하기 어렵기 때문에, 기존의 매칭 흐름 사이에 리스트 타입을 처리하는 분기를 새롭게 추가했습니다. 우선 아래와 같이 `matchParameter`의 기존 매칭 순서 사이에 리스트 주입 로직을 추가했습니다.

 

private fun matchParameter(param: KParameter): Any? =
    lookupValueByName(param.name, param.type.classifier)
        //{
        ?: lookupListValues(param)
        //}
        ?: lookupValueByType(param.type.classifier)
        ?: if (param.isOptional) null else throw MockKException("Parameter unmatched: $param")
// 리스트 타입 체크 및 다중 주입 로직을 추가했습니다.

 

위에서 호출한 `lookupListValues` 함수는 파라미터가 리스트 타입일 때 리플렉션으로 제네릭 인자를 추출한 뒤, 해당 타입과 호환되는 Mock 객체들을 수집합니다. 또한 이 로직을 `matchParameter`뿐만 아니라 생성자 주입의 또 다른 경로인 `tryMatchingParameters`에도 함께 반영하여 주입 과정에서 누락되는 부분이 없도록 했습니다.

 

추가로 MockK의 기본 설정인 타입 기반 룩업(`lookupType.byType`)이 활성화되었을 때만 이 로직이 동작하도록 해, 기존 동작 방식과의 호환성도 유지했습니다. 실제 구현된 로직은 다음과 같습니다.

 

private fun lookupListValues(param: KParameter): List<Any>? {
    if (!isListType(param.type.classifier)) return null
    if (!lookupType.byType) return null

    //{
    val elementType = listElementType(param) ?: return null
    //}

    //{
    val mocks = mockHolder::class
        .memberProperties
        .filter { isMatchingType(it, elementType) }
        .mapNotNull { it.getAnyIfLateNull(mockHolder) }
    //}

    return mocks.takeIf { it.isNotEmpty() }
}
// 리플렉션으로 리스트의 제네릭 원소 타입을 추출합니다. // mockHolder에서 타입이 일치하는 모든 Mock 객체를 수집합니다.

 

이 수정을 통해 타입 기반 룩업이 활성화된 환경에서 생성자가 `List<Type>`을 요구할 때, 해당 타입과 매칭되는 Mock 객체들을 수집해 리스트로 주입할 수 있게 되었습니다.

 

검증

기능 구현만큼 중요한 것이 기존 로직에 영향을 주지 않으면서 새로운 기능이 정상적으로 작동하는지 확인하는 것이었습니다. 이를 확인하기 위해 기존 `InjectMocksTest` 외에 `InjectMocksListTest`를 추가했습니다. 기존 테스트가 그대로 통과하는 것을 확인했고, 새 테스트를 통해 여러 개의 Mock 객체가 리스트에 의도한 대로 담겨 주입되는 것도 검증했습니다.

 

 

시행착오

코드 구현을 마치고 PR을 올린 뒤, 예상과 달리 CI 빌드에서 실패했습니다. 원인은 스타일 검사 규칙을 통과하지 못한 것이었습니다.

로컬에서 `./gradlew spotlessApply` 명령어를 실행해 스타일 규칙을 일괄 적용한 뒤 다시 푸시했고, 이후 모든 빌드 과정을 통과할 수 있었습니다.

 

개인 프로젝트나 회사 업무에서는 IDE 자동 포맷팅에 의존하는 경우가 많았는데, 이번 경험을 통해 오픈소스 프로젝트에서는 많은 사람이 같은 코드베이스를 다루는 만큼, 코딩 컨벤션을 일관되게 유지하는 것이 중요하다는 것을 알게 되었습니다.

 

 

결과 및 회고

메인테이너가 빠르게 확인해 준 덕분에 PR을 올린 지 얼마 지나지 않아 머지가 완료되었습니다.

많은 개발자가 사용하는 도구에 제가 만든 변경이 포함됐다는 점이 신기했고, 의미 있는 경험이었습니다.

 

이번 경험을 계기로 Spring Kafka 프로젝트에도 기여를 시도해 PR을 올려둔 상태인데요.

첫 기여를 통해 얻은 경험을 바탕으로, 앞으로도 제가 기여할 수 있는 이슈를 찾아 꾸준히 참여해 보려고 합니다. 긴 글 읽어주셔서 감사합니다! 🙇‍♀️

 

 

 

 

issue 링크: https://github.com/mockk/mockk/issues/1356

 

Can not inject a list of implementations in a @InjectMockKs test instance · Issue #1356 · mockk/mockk

Spring Boot allows to inject a list of all implementations for an interface in another component class interface BaseComponent @Component class ComponentImpl0 : BaseComponent @Component class Compo...

github.com

 

PR 링크: https://github.com/mockk/mockk/pull/1492

 

Add List injection support for @InjectMockKs (#1356) by h2jinee · Pull Request #1492 · mockk/mockk

Fixes #1356 Problem Spring Boot allows injecting a list of implementations via constructor, but currently @InjectMockKs cannot handle List<T> parameters. It throws an exception or fails to ma...

github.com