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

Spring Kafka Test

Spring Kafka 를 사용할 때, 외부 카프카 서버에 의존하지 않고 테스트하는 방법을 정리한다.

Setup

spring-kafka 를 의존성으로 추가하자. 그러면, spring-kafka-test 도 같이 추가된다.
spring-kafka-test 에는 테스트를 돕는 다양한 util 이 존재한다.

이제 간단한 Producer 를 추가하자.
KafkaTemplate 를 이용해서, 특정 토픽에 메세지를 전송한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.slf4j.LoggerFactory
import org.springframework.kafka.core.KafkaTemplate
import org.springframework.stereotype.Component

@Component
class Producer(
private val kafkaTemplate: KafkaTemplate<String, String>
) {

private val logger = LoggerFactory.getLogger(this.javaClass.name)

fun produce(topic: String, message: String) {
logger.info("Produced, (message: $message), (topic: $topic)")

kafkaTemplate.send(topic, message)
}
}

Consumer 를 추가하자. “jko-topic” 을 listen 하는 consumer 이다.
CountDownLatch 는 테스트 할 때, producer 가 send 한 record 를 consumer thread 가 consume 을 완료할 때 까지 대기하기 위해 사용된다.

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
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.slf4j.LoggerFactory
import org.springframework.kafka.annotation.KafkaListener
import org.springframework.stereotype.Component
import java.util.concurrent.CountDownLatch

@Component
class Consumer(
private val countDownLatch: CountDownLatch = CountDownLatch(1)
) {

private val logger = LoggerFactory.getLogger(this.javaClass.name)
private var message: String? = null

@KafkaListener(topics = ["jko-topic"])
fun consume(consumerRecord: ConsumerRecord<String, String>) {
logger.info("Consumed, (record: $consumerRecord)")

message = consumerRecord.value()
countDownLatch.countDown()
}

fun await() = countDownLatch.await()

fun equalsConsumedMessageWith(message: String) = this.message == message
}

Test

테스트를 작성할 때의 핵심은, 외부 카프카 서버에 의존하지 않고 테스트하는 것이다.
이를 위해, spring-kafka-test 에서 지원하는 @EmbeddedKafka 를 사용한다.
@EmbeddedKafka 는, Spring for Apache Kafka 기반 테스트를 실행하는 테스트 클래스에 지정할 수 있는 어노테이션이다.
테스트를 실행할 때, in-memory kafka instance 를 사용하게 된다.

Read more

Spring Data JPA Paging

Spring Data JPA 를 이용해서 paging 처리를 할 때,
부가적인 count query 를 발생시키지 않는 방법을 정리한다.

Setup

아래와 같이 의존성으로, Spring Data JPA 와 H2 를 추가하자.
테스트를 위해 Embedded DB 로 H2 를 사용한다.

Entity 로 Book 을 정의하자.

1
2
3
4
5
6
7
8
@Entity
class Book(

@Id @GeneratedValue
val id: Long? = null,

val name: String
)

그리고, BookRepository 를 정의하자.
Paging 을 위해, PagingAndSortingRepository interface 를 상속한다.

1
interface BookRepository : PagingAndSortingRepository<Book, Long>

Test

JPA 관련 테스트이기 때문에, @DataJpaTest 를 활용해보자.

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