Kotlin Coverage Test – Static Analysis with Jenkins

Kotlin으로 모듈을 작성하다 보면, 모듈의 각 코드에 대한 단위 테스트와 코드 분석을 진행할 필요가 있다.

특히 그 모듈은 구(공 모양의 도형) 상에 존재하는 x개의 데이터 셋에 대해 y 를 제공했을 때, 데이터 셋 상에서 y에 최근접해있는 데이터를 찾는 모듈이었기 때문이었다.

그래서 모듈을 구성하는 모든 코드에 대한 단위 테스트를 진행하여 최대한 모듈 상의 버그를 없애고 (물론, 로직을 구성하는 최하위 알고리즘 상에 문제는 생길 수는 있다. 다만, 서울에서 멀리 떨어진 지역의 데이터이기 때문에 검증하기도 많이 어렵다.) 퍼포먼스, 보안 상 문제가 될 수 있는 모든 가능성을 없앨 필요가 있었다.

이 글에서는 그 과정에서 도입한 Spek Framework, Jacoco Report, Detekt 에 대해 알아보고, 이 세 개를 Jenkins에 연동하여 Jenkins dashboard 에서 해당 리포트에 대해 보여주는 과정을 살펴보려 한다.

Spek Framework

Spek Framework란 코틀린용 유닛 테스트 프레임워크로 Kotlin의 기여자들이 관리하는 프레임워크이다. (JetBrains 공식 프로젝트는 아니나, JetBrains 가 개발한 흔적은 있다.) 현재는 2.0.0-RC1 버전으로, 특징적인 점이라면 DSL로 유닛 테스트를 진행할 수 있다는 점이다.

class SomeTest: Spek({
    describe("Some test") {
        it("Check value is something") {
            // TEST BODY
        }
    }
})

이러한 구조를 가지고 있어 JUnit와 다른 테스트 프레임워크와는 다르게 쉬운 구조를 가지고 있다.

(이렇게 보면, Ktor도 그렇고 JetBrains 가 개발한 프레임워크들은 DSL를 매우 강조하는 것 같다. 과연 그것이 좋은지는 아직도 잘 모르겠긴 하다.)

임포트는 다음과 같이 진행한다.

buildscript {
    ext.kotlin_version = '1.3.11'
    ext.spek_version = '2.0.0-rc.1'
    ext.junit_version = '4.12'
​
    repositories {
        jcenter()
    }
​
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
​
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
    testImplementation "org.spekframework.spek2:spek-dsl-jvm:$spek_version"
    testRuntimeOnly "org.spekframework.spek2:spek-runner-junit5:$spek_version"
    testRuntimeOnly "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
    testCompile("org.assertj:assertj-core:3.11.1")
    testCompile "junit:junit:$junit_version"
}
​
test {
    useJUnitPlatform {
        includeEngines 'spek2'
    }
}

이렇게 작성한 테스트 코드는 gradlew test 로 확인이 가능하다.

Jacoco Report

Jacoco Report는 Java Code Coverage를 구현하는 데 사용하는 툴킷으로, Line, Branch에 대한 Coverage를 제공한다.

임포트는 다음과 같이 한다.

apply plugin: 'jacoco'
​
test {
    useJUnitPlatform {
        includeEngines 'spek2'
    }
    jacoco {
        destinationFile = file("${buildDir}/jacoco/test.exec")
    }
}
​
test.finalizedBy(jacocoTestReport)
​
jacoco {
    // You may modify the Jacoco version here
    toolVersion = "0.8.2"
}
​
jacocoTestReport {
    // Adjust the output of the test report
    reports {
        xml.enabled true
        csv.enabled false
    }
}

gradlew test 로 테스트를 구동했을 경우, destinationFile로 제공한 경로에 exec 파일이 저장된다. 이 exec 파일은 Jacoco 가 생성한 리포트 파일이 포함되며, gradlew jacocoTestReport 명령어로 리포트 파일을 생성할 수 있다. (여기서는 xml, html 리포트를 생성한다.)

Detekt

Detekt는 코틀린에 대한 Static Analysis를 제공하는 툴킷으로, 코틀린으로 코드를 작성함에 있어 피해야하는 패턴들에 대해 감지, 특정 패턴들의 가중치를 파악해서 일정 이상이면 build 자체가 failed 되게 할 수 있어 코드 본연의 문제를 좀 더 파악할 수 있게 하는 툴킷이다.

특히 Codacy (https://www.codacy.com/) 에서 코틀린 프로젝트를 임포트 했을 때 사용하는 분석 툴킷으로, 많은 사람들에 의해 검증된 툴킷이기도 하다.

임포트 과정은 프로젝트 사이트(https://arturbosch.github.io/detekt/) 에 설명되어 있다.

detekt의 경우 gradlew detekt 로 통해 테스트를 진행할 수 있다. 이 때 report는 build/reports/detekt 에 detekt.html 에 저장되며 이 리포트 파일을 참고로 해서 파악할 수 있다.

Jenkins로 실행

위에서 살펴본 Spek Framework, Jacoco, Detekt를 빌드마다 실행하기 위해서 JenkinsFile에 해당 과정을 구현했다.

stage('Test Analysis') {
      parallel {
        stage('Static Analysis') {
          steps {
            sh './gradlew detekt'
            publishHTML(target: [reportDir:'build/reports/detekt/', reportFiles: 'detekt.html', reportName: 'Detekt report'])
          }
        }
        stage('Unit Test') {
          steps {
            sh './gradlew cleanTest test'
            sh './gradlew jacocoTestReport'
            publishHTML(target: [reportDir:'build/reports/jacoco/test/html', reportFiles: 'index.html', reportName: 'Code Coverage'])
          }
        }
      }
    }

gradlew build 가 끝난 다음, 이 단계를 실행하게 하면 된다. publishHTML은 특정 Html 파일을 Jenkins 메뉴 상에 표시해서 쉽게 볼 수 있게 한다.

Jenkins에 테스트 결과에 대한 Output 출력

Test Code를 작성할 때 println 메서드로 Test 콘솔에 Output를 출력할 수 있으나, 위 JenkinsFile를 가지고 실행하면 Output가 나오지 않는다.

이 때는 https://stackoverflow.com/a/36130467 링크에 있는 답변을 사용하면 된다.

최종 결과

전체 파이프라인 통과
각 테스트에 대해 PASSED / FAILED / SKIPPED 등이 표시된다. 만일 Output가 있을 경우 그 아래에 표시된다.
마지막에 총 결과가 표시된다.
Detekt 분석 결과는 콘솔에도 표시된다. 물론, 리포트에도 볼 수 있다.
Jenkins 의 메뉴에 Code Coverage, Detekt Report가 생긴 것을 확인할 수 있다.
Detekt가 생성한 report 파일이다.
Jacoco가 생성한 Coverage Report로, Line Coverage는 99%, Branch Coverage는 94%로 측정되었다.

마무리

제목은 다소 거창하지만, 실제로 한 행동은 그렇게 어렵지는 않다. 다만 이 글에서 사용한 프로젝트는 순수 Kotlin 프로젝트로 안드로이드 프로젝트는 아니다.

이 면에서 보면, 안드로이드 프로젝트에 대해 유닛 테스트를 진행하는 것은 다소 어렵기 때문에 조금 진입장벽이 높을 수는 있다고는 판단된다.

하지만 작은 것 부터 나아가면, 점점 더 나아질 것이라 생각한다.

NaraeAudioRecorder, AudioRecorder for Android

회사에서 프로젝트를 진행하던 도중, 음성 녹음에 대한 기능을 개발할 일이 있었다.

예전(아마 2017년쯤)에 쓰던 lameMp3 wrapper가 있었기에 그대로 활용했었는데, 최신 환경에서는 녹음된 소리가 제대로 나오지 않고 이상하게 나오는 현상이 있었다.

그래서, 이번 주말동안 기존에 AudioRecord를 만졌던 기억을 되살려 음성 녹음 라이브러리를 만들게 되었고, 그것이 바로 WindSekirun/NaraeAudioRecorder 이다.

구조

크게 바라보면 해당 라이브러리는 총 4개의 파트로 구성된다.

  • AudioChunk: AudioRecord에서 bufferSize 만큼 읽어온 만큼의 raw 데이터와 raw 데이터에 대한 정보를 담고 있다.
  • AudioRecorder: Wav, PCM, FFmpeg 등 여러 포맷에 대한 객체로 start, stop, resume, pause 이벤트에 맞춰 작업을 실행한다.
  • AudioSource: 기본과 NoiseSuppressor (배경 노이즈를 제거하는 안드로이드 API 전처리기) 에 대한 기본 속성이며, AudioRecord에 대한 정보를 갖고 있다.
  • RecordWriter: AudioSource 와 AudioRecorder 사이에 있는 중간 매개체로 AudioSource로 설정한 AudioRecord에서 bufferSize만큼 raw data를 가져와 OutputStream에 쓰고, 그 이벤트를 AudioRecorder 에 전달하는 역할을 한다.

각각 유기적으로 연결되어있어, 처리 순서는 AudioRecorder > RecordWriter > AudioChunk 이고, AudioSource는 RecordWriter를 사용하기 위한 AudioRecord 설정 객체라 이해하면 편하다.

특이점

특이점이라 한다면 AudioRecord에서 가져오는 데이터는 raw 데이터이고, 이를 wav나 mp3 등으로 변환하기 위해서는 특별한 작업이 필요하다.

예전에도 AudioRecord to Wav 란 주제로 다룬 적이 있었고, 이번에도 특별한 작업은 없다.

mp3, aac, wma 로 변환하기 위해서 FFmpeg를 사용했는데, FFmpeg에 대한 command를 구성하도록 했다

val tempFile = File(file.parent, "tmp-${file.name}")
commandBuilder.addAll(listOf("-y", "-i", file.path))

if (convertConfig.samplingRate != FFmpegSamplingRate.ORIGINAL) {
     commandBuilder.addAll(listOf("-ar", convertConfig.samplingRate.samplingRate.toString()))
}

if (convertConfig.bitRate !== FFmpegBitRate.def) {
    commandBuilder.addAll(listOf("-sample_fmt", convertConfig.bitRate.bitRate))
}

if (convertConfig.mono) {
    commandBuilder.addAll(listOf("-ac", "1"))
}

commandBuilder.add(tempFile.path)

FFmpeg를 다뤄보는 것은 두번째이기 때문에, 다소 그렇게 삽질하지는 않은 것 같다. (아마도… (._.

물론 FFmpeg를 사용한다면 라이센스 문제가 나오기 때문에, 멀티 모듈로 구성해서 core만을 사용한다면 FFmpeg에 의존성이 없게 구성했다. 아무래도 FFmpeg를 프로덕션에서 바로 사용하기에는 부적절한 면도 있기도 하다.

마지막으로 조금 특이한 점이라면, 목적 파일의 확장자에 따라 wav, mp3 등 각각의 recorder로 넘기는데 이 과정에서 리플렉션을 통해 접근했다는 점이다.

class FFmpegRecordFinder : RecordFinder {

    /**
     * see [RecordFinder.find]
     */
    override fun find(extension: String, file: File, writer: RecordWriter): AudioRecorder {
        return when (extension) {
            "wav" -> WavAudioRecorder(file, writer)
            "pcm" -> PcmAudioRecorder(file, writer)
            "aac" -> FFmpegAudioRecorder(file, writer)
            "mp3" -> FFmpegAudioRecorder(file, writer)
            "m4a" -> FFmpegAudioRecorder(file, writer)
            "wma" -> FFmpegAudioRecorder(file, writer)
            "flac" -> FFmpegAudioRecorder(file, writer)
            else -> PcmAudioRecorder(file, writer)
        }
    }

}

RecordFinder 란 클래스에서 find 메서드를 통해 Recorder 로 넘기고, 이 RecordFinder를 Class 객체를 사용하여 바로 인스턴스를 만들고 활용하는 방법으로 접근했다.

 try {
            val finder = recordFinder.getConstructor().newInstance() as? RecordFinder
                    ?: throw IllegalArgumentException(LogConstants.EXCEPTION_FINDER_NOT_HAVE_EMPTY_CONSTRUCTOR)

            val file = recorderConfig.destFile ?: return // it can't be null
            audioRecorder = finder.find(file.extension, file, recordWriter)
} catch (exception: Exception) {
            throw IllegalArgumentException(LogConstants.EXCEPTION_FINDER_NOT_HAVE_EMPTY_CONSTRUCTOR, exception)
}

물론 해당 RecordFinder 클래스를 정확한 형식에 맞추지 않고 개발을 진행했을 경우 사용하지 못한다는 문제점이 있지만, 가능한 멀티모듈로 구성하면서도 유기적으로 연계되기 위해서 해당 방법을 사용했다. (적어도 개발자 자신이 확정할 수 있으면 괜찮지 않을까.. 라는 조금 어린 생각을 가지기도 했다.)

마무리

총 3일동안 개발하면서 나름대로 코드를 최대한 깔끔하게 짜고, 문서화도 제대로 하려고는 노력은 했지만 아직까지 표현력이 부족한 점도 많긴 많았다.

그래도 2019년 한 해의 시작을 라이브러리로 시작했으니, 이번 년도 말에도 라이브러리 개발로 끝날 것 같기도 하다.

마지막으로 개발했던 모든 소스와 샘플들은
WindSekirun/NaraeAudioRecorder 에 공개되어 있다. JCenter에도 배포되어있으니, 바로 다운 받아 사용이 가능하다.

2018 한 해를 돌아보며…

2018년.

기나고도 짧은 1년이 지나가고, 이제 2019년이 다가옵니다.

그런 기념으로, 2018년의 중요하고 변화점이나 느낀 점을 살펴보고 2019년에 달성하고 싶은 목표를 적어보려 합니다. (물론 개발적으로요.)

1월 – PyxisBaseApp (MVVM + Dagger)

2018년 1월 20일, PyxisBaseApp라는 MVVM + Dagger + Databinding + Retrofit 스택을 가진 베이스 앱을 작성하기 시작, 2018년 12월 31일 기준으로 배포 버전 1.3.3, 커밋 수 316개의 적당한 규모의 베이스 앱을 제작하였습니다.

이 베이스앱의 전/후로 지금까지의 본인 안에서의 개발 패러다임이 바뀌었는데, 전에는 일정한 패턴 없이 Activity 하나에 집중하는 형태였다면 지금은 일정한 패턴을 따라서 MVVM 과 Continous Integration을 목적으로 개발하고 있습니다.

이 때 MVP를 거치지 않고 MVVM으로 바로 거친 이유로는 당시 같은 회사에 계셨던 개발자분이 MVP를 사용하셨는데 이 구조를 보니까 Activity – Contract – Presenter가 강제되는 형태로 조금 비효율적으로 보였기 때문입니다 . 그리고 마침, 안드로이드에서 Android Architecture Component 라고 하는 구조가 나왔었기에 MVVM을 좀 더 쉽게 작성할 수 있었습니다.

이렇게 PyxisBaseApp는 계속해서 변화를 거쳐 9월 29일에는 JFrog Artifactory를 통해 본격적인 라이브러리 형태의 베이스 앱을 제작했고, 나름대로 만족할 구조가 탄생하였습니다.

3월 – Annotation Processor

1월과 2월에는 MVVM 구조를 중점으로 적용했다면, 3월에는 중복되는 코드를 줄이기 위한 Annotation Processor에 대해 집중했습니다.

지금까지 어노테이션을 Reflection 을 통한 접근방법만 사용했었는데, 타겟으로 하는 어노테이션이 있는 클래스나 메서드 대상으로 코드를 생성하는 Annotation Processor의 도입으로 Dagger의 Activity, Fragment, ViewModel의 주입이 어노테이션 하나를 부착하는 것 만으로도 끝나고, CustomView의 Attribute 파싱 등을 빠르게 할 수 있었습니다.

물론, 이 떄 공부한 지식으로 Annotation Processor 기반 기술을 도입하거나 분석할 때 큰 도움이 되었기도 했습니다. (Databinding나 Dagger 등)

7월 – RxJava

계속 미루고 왔었던 RxJava 도입을 이 때부터 시작하였습니다. 처음에는 Listener 형태의 Callback를 Observable로 변경하는 것에서부터 Socket 통신의 RxJava wrapper까지 작성하면서 이전에는 쉽게 달성하지 못했던 스레드 관리나 데이터 소스로부터의 효율적인 합성을 달성하였습니다.

그리고 이 때, SocialLogin 기능과 RxJava를 합친 RxSocialLogin(https://github.com/WindSekirun/RxSocialLogin)을 개발하고 배포하였습니다.

8월

산업기능요원으로 근무하다보면 언젠가 찾아오는 시련인 훈련소(._. )를 다녀왔습니다. 따라서 이 때는 기록이 없습니다.

9월 – Docker를 통한 SaaS 컨테이너 배포

이 때까지 PyxisBaseApp는 프로젝트마다 멀티모듈로 포함되어 있었기에 버전 관리가 안되고, 여러 프로젝트마다 다른 베이스 앱 코드를 가지고 있었기에 코드 관리도 더더욱 힘들었습니다.

그래서 Docker를 공부하고 새 인스턴스에 JFrog Artifactory를 올리고 PyxisBaseApp를 모듈 단위 배포가 아닌 라이브러리 형태 배포로 변경하였습니다.

추가적으로, 처음으로 BLE 앱을 원개발하면서 RxAndroidBle(https://github.com/Polidea/RxAndroidBle) 도 사용해보았습니다. 다소 어려웠지만 notify, write 이벤트를 쉽게 처리할 수 있었던 것이 가장 마음에 들었습니다.

12월 – Jenkins 도입

이전에는 CircleCI를 사용했었으나 Private project에는 적용하지 못하고 오픈 소스 프로젝트만 사용하였습니다. 그래서, 꽤나 이전부터 계획은 있었음에도 쉽게 도입할 수 없었던 Jenkins를 Docker의 힘을 빌려 사용하게 되었습니다.

그리고 12월 25일, 대망의 확장 공사를 시작하면서 지금까지 분리되어있던 블로그 인스턴스 + (Artifactory/Jenkins) 인스턴스를 합쳐 지금의 인스턴스로 이사하면서 메일 서버나 FTP 서버 등도 전부 Docker를 통해 관리하게 되었습니다.

마무리

2018년에만 공부하고, 새로 도입해서 실제 사용한 기술이 MVVM, DataBinding, RxJava, Dagger (DI), Retrofit, ObjectBox, Docker, Jenkins, AndroidX, Static Analysis 로 작은 것 까지 포함하면 20개는 족히 넘을 것 같습니다.

2019년에는 빠르게 변화하기 보다는 기존에 사용하던 기술을 좀 더 가다듬어 좀 더 전문적인 지식을 가질 수 있도록 열심히 갈고 닦아야 될 것 같습니다.

그리고, 대망의 그 날(2019-04-19)도 얼마 남지 않았고요. 후후…