Mockk

Mockk 는 “mocking library for Kotlin” 이다. 간단한 사용법을 정리해보자.

Install

우선, 시작하기 앞서 dependency 를 추가해보자.

for gradle:

1
testImplementation("io.mockk:mockk:{version}")

for maven:

1
2
3
4
5
6
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>{version}</version>
<scope>test</scope>
</dependency>

Simple Example

mockk 를 활용한 간단한 테스트 코드를 작성해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
fun `sum`() {
// given
val additionCalculator = mockk<AdditionCalculator>()
every { additionCalculator.calculate(1, 3) } returns 4

// when
val result = additionCalculator.calculate(1, 3)

// then
verify { additionCalculator.calculate(1, 3) }
assertEquals(4, result)
}
Read more

Effective Kotlin - Reusability 2

코틀린의 재사용성을 활용하는 방법을 정리한다.

  • 일반적인 알고리즘을 구현할 때, 제네릭을 사용해라
  • 타입 파라미터의 shadowing 을 피해라
  • 공통 모듈을 추출해서 여러 플랫폼에서 재사용해라

일반적인 알고리즘을 구현할 때, 제네릭을 사용해라

type parameter 를 가지는 함수를 generic function 이라고 한다.
예를 들어, type parameter T 를 가지는 filter function 이 있다.

1
2
3
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}

타입 파라미터는 구체적인 타입의 서브 타입만 사용하도록 타입을 제한할 수 있다.
타입에 제한이 걸려서, 내부에서 해당 타입이 제공하는 메서드를 사용할 수 있다.

아래를 보자.
Number 를 타입 파라미터 T 의 상한으로 지정한다.
그리고, 내부에서 Number 클래스에 정의되 메서드를 호출한다.

1
2
3
4
5
fun <T: Number> oneHalf(value: T): Double {
return value.toDouble() / 2.0
}

oneHalf(3) // 실제 타입 인자인 Int 가 Number 를 확장하기 때문에 가능

타입 파라미터의 shadowing 을 피해라

지역 파라미터가 외부 scope 에 있는 프로퍼티를 가리는 것을 shadowing 이라고 한다.
예를 들어,

Read more

Effective Kotlin - Reusability 1

코틀린의 재사용성을 활용하는 방법을 정리한다.

  • knowledge 를 반복해서 사용하지 마라
  • 일반적인 알고리즘을 반복해서 구현하지 마라
  • 일반적인 프로퍼티 패턴은 프로퍼티 위임으로 만들어라

knowledge 를 반복해서 사용하지 마라

knowledge

knowledge 를 반복해서 사용하지 말라는 의미는, 아래와 같이도 표현할 수 있다.

  • DRY 규칙: Don’t Repeat Yourself
  • WET 안티 패턴: We Enjoy Typing, Waste Everyone’s Time, Write Everything Twice

knowledge 는 코드 또는 데이터로 표현할 수 있는, 의도적인 정보이다.
프로그램에서 중요한 두 가지 knowledge 는,

  1. 비즈니스 로직: 프로그램이 어떤 식으로 동작하는지 (시간에 따라 계속해서 변한다)
  2. 공통 알고리즘: 원하는 동작을 하기 위한 알고리즘 (한 번 정의되면 쉽게 변하지 않는다)

둘의 가장 큰 차이는 “변화” 이다.

모든 건은 변화한다

Read more

Effective Kotlin - Readability 2

코틀린을 가독성이 좋게 작성하는 방법을 정리한다.

  • 리시버를 명시적으로 참조해라
  • 프로퍼티는 동작이 아니라, 상태를 나타내야한다
  • named arguments 를 사용해라
  • 코딩 컨벤션을 지켜라

리시버를 명시적으로 참조해라

짧게 적을 수 있다는 이유만으로, 리시버를 제거하지 말자.
여러 개의 리시버가 있으면, 리시버를 명시적으로 적어줘야한다.
그러면, 어떤 리시버의 함수인지를 명확하게 알 수 있어서 가독성이 향상된다.

예를 들어 apply, with, run 함수를 사용할 때가 있다.

1
2
3
4
5
6
7
8
9
10
class Node(val name: String) {
fun makeChild(childName: String) =
create("$name.$childName")
.also { print("Created ${it?.name}")} // Created parent.child

fun create(name: String): Node? = Node(name)
}

val node = Node("parent")
node.makeChild("child")

프로퍼티는 동작이 아니라, 상태를 나타내야한다

프로퍼티는 개념적으로,

  • val 의 경우에 getter
  • var 의 경우에 getter 와 setter 를 나타낸다.

그래서 프로퍼티를 정의하여 오버라이드 가능하다.

Read more

Effective Kotlin - Readability 1

코틀린을 가독성이 좋게 작성하는 방법을 정리한다.

  • 가독성을 목표로 설계해라
  • 연산자 오버로드할 때는 의미에 맞게 사용해라
  • Unit? 을 리턴하지 마라
  • 변수 타입이 명확하지 않으면 확실하게 지정해라

가독성을 목표로 설계해라

로버트 마틴의 클린코드에서 다음과 같은 내용이 있다.
개발자가 코드 작성하는데 1분 걸리지만, 읽는데는 10분 걸린다

프로그래밍은 쓰기보다 읽기가 중요하다는 의미이다.
그래서, 가독성을 생각하면서 코드를 작성해야한다.

인지 부하 감소

인지 부하를 줄이는 방향으로 코드를 작성하자.
자주 사용되는 패턴을 활용하면, 뇌가 프로그램의 작동 방식을 이해하는 과정을 더 짧게 만들 수 있다.

다음 두 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
// A
if (person != null && person.isAdult) {
view.showPerson(person)
} else {
view.showError()
}

// B
person?.takeIf { it.isAdult}
?.let(view.showPerson)
?: view.showError()

어느 코드가 더 좋을까 ? A 가 훨씬 가독성이 좋은 코드이다.
가독성이란, 코드를 읽고 얼마나 빠르게 이해할 수 있는지를 의미힌다.
구현 A 가 더 좋은 코드인 이유는,

Read more

Effective Kotlin - Safety 2

코틀린을 안전하게 사용하기 위한 방법을 정리한다.

  • 사용자 정의 오류보다 표준 오류를 사용해라
  • 결과 부족이 발생하면, null 과 Failure 를 사용해라
  • 적절하게 null 을 처리해라
  • use 를 사용해서 리소스를 닫아라
  • 단위 테스트를 만들어라

사용자 정의 오류보다 표준 오류를 사용해라

표준 라이브러리의 오류는 많은 개발자가 알고 있기 때문에, 이를 재사용하는 것이 좋다.
다른 사람들이 API 를 더 쉽게 배우고 이해할 수 있다. 예를 들어,

  • IllegalArgumentException,
  • NoSuchElementException
  • UnsupportedOperationException

결과 부족이 발생하면, null 과 Failure 를 사용해라

함수가 원하는 결과를 만들지 못할 때가 있다. 예를 들어,

  1. 인터넷 연결 문제로, 서버로부터 데이터를 읽어 들이지 못할 때
  2. 조건에 맞는 첫 번째 요소가 없을 때
  3. 텍스트를 파싱해서 객체를 만들려고 했는데, 텍스트 형식이 맞지 않을 때

이를 처리하는 방법에는,

  1. null 을 리턴한다.
  2. 실패를 나타내는 sealed 클래스를 리턴한다. (일반적으로, Failure 라는 이름)
  3. 예외를 throw 한다.
Read more

Effective Kotlin - Safety 1

코틀린을 안전하게 사용하기 위한 방법을 정리한다.

  • 가변성을 제한하라
  • 변수의 스코프를 최소화해라
  • 최대한 플랫폼 타입을 사용하지 마라
  • inferred 타입으로 리턴하지 마라
  • 예외를 활용해 코드에 제한을 걸어라

가변성을 제한하라

읽고 쓸 수 있는 프로퍼티인 var 를 사용하거나, mutable 객체를 사용하면 상태를 가질 수 있다.
다음 예를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class InsufficientFunds : Exception()

class BankAccount {
var balance = 0.0
private set

fun deposit(amount: Double) {
balance += amount
}

@Throws(InsufficientFunds::class)
fun withdraw(amount: Double) {
if (balance < amount) {
throw InsufficientFunds()
}

balance -= amount
}
}

@Test
fun `BankAccount 에는 계좌에 돈이 얼마 있는지 나타내는 상태가 있다`() {
val account = BankAccount()
assertEquals(0.0, account.balance)

account.deposit(100.0)
assertEquals(100.0, account.balance)

account.withdraw(50.0)
assertEquals(50.0, account.balance)
}

시간의 변화에 따라, 변하는 요소를 표현하는 것은 유용하다. 하지만,

  1. 프로그램을 이해하기 어렵고 디버깅이 어렵다.
  2. 시점에 따라 값이 달라져서, 코드의 실행을 예측하기 어렵다.
  3. 멀티 스레드 프로그램이면, 적절한 동기화가 필요하다.
  4. 모든 상태를 테스트해야해서, 더 많은 조합을 테스트해야한다.
  5. 상태가 변경되면, 다른 부분에 알려야하는 경우가 있다. (ex) 리스트에 요소 추가되면, 전체 다시 정렬

코틀린에서, 가변성을 제한할 수 있는 방법에는 다음이 있다.

  1. 읽기 전용 프로퍼티 val
  2. 가변 컬렉션과 읽기 전용 컬렉션 구분
  3. 데이터 클래스의 copy

읽기 전용 프로퍼티 val

Read more

Kotlin Compiler Plugins

kotlin compiler plugins 중에 plugin-spring 과 plugin-jpa 에 대해 정리한다.

프로젝트 생성

Spring Initializer 를 이용해서 프로젝트를 생성해보자.
kotlin + gradle 기반에 의존성으로 Spring Data JPA 만 추가한다.
생성된 build.gradle.kt 파일을 보자.

plugin 에 위와 같이 “plugin.spring”, “plugin.jpa” 가 추가된 것을 볼 수 있다.
왜 이 두 개의 plugin 이 자동으로 추가된 걸까 ?

plugin.spring

“org.jetbrains.kotlin.plugin.spring” 는 org.jetbrains.kotlin.plugin.allopen” 의 wrapper plugin 이다. “org.jetbrains.kotlin.plugin.allopen” 와 동일하게 동작한다.

“org.jetbrains.kotlin.plugin.spring” compiler plugin 은, 아래 annotation 이 붙은 클래스와 그 클래스의 맴버에 open 을 자동으로 추가한다.

  • @Component
  • @Async
  • @Transactional
  • @Cacheable
  • @SpringBootTest

직접 확인해보자. 다음과 같이 프로젝트에 두 개의 클래스를 추가해보자.

Read more

Generics

다음 내용을 정리한다.

  • 제네릭 타입 파라미터
  • 제네릭 함수
  • 제네릭 클래스
  • 제네릭 타입 파라미터 제약
  • 제네릭스 동작 원리

제네릭 타입 파마리터

제네릭스를 사용하면, 타입 파마리터를 받는 타입을 정의할 수 있다.
제네릭 타입의 인스턴스를 만들려면, 타입 파라미터를 구체적인 타입 파라미터로 치환해야한다.

예를 들어, Map 클래스는 키 타입과 값 타입을 타입 파리미터로 받으므로 Map<K, V> 이다.
이 제네릭 클래스에 대해 제네릭 타입의 인스턴스를 만들려면,
Map<String, Person> 처럼 구체적인 타입 파라미터로 치환해서 인스턴스화할 수 있다.

타입 파라미터 추론

코틀린 컴파일러는 보통의 타입과 마찬가지로, 타입 파라미터도 추론가능하다.

코틀린 Collections 의 listOf function 에는 아래 처럼, 타입 파라미터 T 가 정의되어 있다.

아래에서는, listOf 에 전달된 두 값이 문자열이므로 여기서 생기는 리스트가 List<String> 임을 추론한다.

Read more

고차함수

코틀린의 고차 함수에 대해 정리한다.

고차 함수

고차 함수란, “다른 함수를 인자로 받거나 반환하는 함수” 이다.

그런데 코틀린에서는, 람다나 함수 참조를 사용해서 함수를 값으로 표현할 수 있다.
그래서, “람다나 함수 참조를 인자로 반거나 반환하는 함수” 도 고차 함수이다.

예를 들어, 표준 라이브러리에 있는 함수인 filter 는 고차 함수이다.
filter 는 술어 함수를 인자로 받는 함수이기 때문이다.

1
2
val numbers = listOf("one", "two", "three", "four")
val longerThan3 = numbers.filter { it.length > 3 } // 술어함수를 인자로 받는 filter

함수 타입

위에서 정리한 것 처럼, 람다를 인자로 받는 함수는 고차함수이다.
그러면 람다 인자의 타입은 어떻게 선언할까 ?

더 단순한 단계인, 람다를 로컬 변수에 대입하는 경우를 보자.

1
2
val sum = { x: Int, y: Int -> x + y }
val action = { println(sum) }
Read more