ETC

  • ν…ŒμŠ€νŠΈ

  • μž…λ ₯/좜λ ₯

  • κ·Έ λ°–μ˜ μ½”ν‹€λ¦° κΈ°λŠ₯

  • μŠ€ν”„λ§ ν”„λ ˆμž„μ›Œν¬

  • 코루틴과 ꡬ쑰적 λ™μ‹œμ„±

ν…ŒμŠ€νŠΈ

ν…ŒμŠ€νŠΈ 클래슀 수λͺ…μ£ΌκΈ° μ„€μ •

Junit 5의 ν…ŒμŠ€νŠΈ 수λͺ…μ£ΌκΈ°λ₯Ό 기본값인 ν…ŒμŠ€νŠΈ ν•¨μˆ˜λ‹Ή ν•œ 번 λŒ€μ‹ 

클래슀 μΈμŠ€ν„΄μŠ€λ‹Ή ν•œ λ²ˆμ”© μΈμŠ€ν„΄μŠ€ν™” ν•˜κ³  μ‹Άλ‹€λ©΄,

@TestInstance μ• λ…Έν…Œμ΄μ…˜μ΄λ‚˜ junit-platform.properties 파일의 lifecycle.default 속성을 μ„€μ •ν•˜μž.

Junit 5λŠ” ν΄λž˜μŠ€μ—μ„œ @TestInstance μ• λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•΄ 수λͺ…μ£ΌκΈ°λ₯Ό λͺ…μ‹œν•  수 μžˆλ‹€.

  • ν…ŒμŠ€νŠΈ μΈμŠ€ν„΄μŠ€μ˜ 수λͺ…μ£ΌκΈ°λ₯Ό PER_CLASS둜 μ„€μ •ν•˜λ©΄ ν…ŒμŠ€νŠΈ ν•¨μˆ˜μ˜ μ–‘κ³Ό 상관 없이 ν…ŒμŠ€νŠΈ μΈμŠ€ν„΄μŠ€κ°€ λ”± ν•˜λ‚˜λ§Œ 생성

@TestInstance(TestInstance.Lifecycle.PER_CLASS) // λͺ¨λ“  ν…ŒμŠ€νŠΈμ˜ ν…ŒμŠ€νŠΈ 클래슀 μΈμŠ€ν„΄μŠ€κ°€ 였직 ν•˜λ‚˜
class JUnit5ListTests {
    // μΈμŠ€ν„΄μŠ€ν™”μ™€ μ›μ†Œ μ΄ˆκΈ°ν™”κ°€ 였직 ν•œλ²ˆ
    private val strings = listOf("this", "is", "a", "list", "of", "strings")

    // ν…ŒμŠ€νŠΈ ν•  λ•Œλ§ˆλ‹€ μ‹€ν–‰ μ „ λ‹€μ‹œ μ΄ˆκΈ°ν™”
    private lateinit var modifiable : MutableList<Int>

    @BeforeEach
    fun setUp() {
        // ν…ŒμŠ€νŠΈ ν•  λ•Œλ§ˆλ‹€ μ‹€ν–‰ μ „ λ‹€μ‹œ μ΄ˆκΈ°ν™”
        modifiable = mutableListOf(3, 1, 4, 1, 5)
        println("Before: $modifiable")
    }

    @AfterEach
    fun finish() {
        println("After: $modifiable")
    }

    @Test
    fun addElementsToList() {
        modifiable.add(9)
        modifiable.add(2)
        modifiable.add(6)
        modifiable.add(5)
        assertEquals(9, modifiable.size)
    }

    @Test
    fun size() {
        println("Testing size")
        assertEquals(6, strings.size)
        assertEquals(5, modifiable.size)
    }

    @Test
    fun accessBeyondEndThrowsException() {
        println("Testing out of bounds exception")
        assertThrows<ArrayIndexOutOfBoundsException> { strings[99] }
        assertEquals(6, strings.size)
    }
}

@TestInstanceλ₯Ό 각 ν…ŒμŠ€νŠΈ ν΄λž˜μŠ€μ— λ°˜λ³΅ν•˜λŠ” λŒ€μ‹  λͺ¨λ“  ν…ŒμŠ€νŠΈμ˜ 수λͺ…μ£ΌκΈ°λ₯Ό properties νŒŒμΌμ— μ„€μ • κ°€λŠ₯

  • 클래슀 패슀(src/test/resource)에 junit-platform.properties νŒŒμΌμ„ μƒμ„±ν•˜μ—¬ ν…ŒμŠ€νŠΈ 수λͺ…μ£ΌκΈ° μ„€μ • κ°€λŠ₯

junit.jupiter.testinstance.lifecycle.default = per_class

ν…ŒμŠ€νŠΈμ— 데이터 클래슀 μ‚¬μš©ν•˜κΈ°

μ›ν•˜λŠ” λͺ¨λ“  속성을 μΊ‘μŠν™”ν•˜λŠ” 데이터 클래슀 μƒμ„±ν•˜κΈ°

assertAll ν•¨μˆ˜μ˜ μž₯점은 Executable μΈμŠ€ν„΄μŠ€μ˜ 단언이 1개 이상 μ‹€νŒ¨ν•˜λ”λΌλ„ Executable μΈμŠ€ν„΄μŠ€λ₯Ό λͺ¨λ‘ μ‹€ν–‰

μ½”ν‹€λ¦° data ν΄λž˜μŠ€μ—λŠ” 이미 equals λ©”μ†Œλ“œκ°€ μ˜¬λ°”λ₯΄κ²Œ κ΅¬ν˜„λ˜μ–΄ μžˆμœΌλ―€λ‘œ ν…ŒμŠ€νŠΈ ν”„λ‘œμ„ΈμŠ€ κ°„μ†Œν™”

  • 단 ν•˜λ‚˜μ˜ assertion이 λͺ¨λ“  속성을 ν…ŒμŠ€νŠΈ

assertjμ—μ„œ μ œκ³΅ν•˜λŠ” contains λ©”μ†Œλ“œλ₯Ό μ΄μš©ν•΄ μ»¬λ ‰μ…˜μ˜ λͺ¨λ“  μ›μ†Œλ₯Ό 확인할 수 μžˆλ‹€.

data class Book(
    val isbn: String,
    val title: String,
    val author: String,
    val published: LocalDate
)

...

@Test
fun `check all elements in list`() {
    val badBook = books[2].copy(title = "Modern Java Cookbook")
    val found = arrayOf(books[2], books[0], books[1]) // Add badBook to see the assertion fail
    val expected = books
    assertThat(found).contains(*expected)
}

κΈ°λ³Έ μΈμžμ™€ ν•¨κ»˜ 도움 ν•¨μˆ˜ μ‚¬μš©ν•˜κΈ°

각 μΈμžμ— 기본값을 μ œκ³΅ν•˜λ„λ‘ 클래슀λ₯Ό μˆ˜μ •ν•˜μ§€ 말고

기본값을 μƒμ„±ν•˜λŠ” νŒ©ν† λ¦¬ ν•¨μˆ˜λ₯Ό μΆ”κ°€ν•˜μž.

μ΅œμƒμœ„ 레벨의 μœ ν‹Έλ¦¬ν‹° ν΄λž˜μŠ€μ— νŒ©ν† λ¦¬ ν•¨μˆ˜λ₯Ό μœ„μΉ˜μ‹œν‚€λ©΄ ν…ŒμŠ€νŠΈμ—μ„œ νŒ©ν† λ¦¬ ν•¨μˆ˜λ₯Ό μž¬ν™œμš©ν•  수 μžˆλ‹€.

fun createBook(
    isbn: String = "149197317X",
    title: String = "Modern Java Recipes",
    author: String = "Ken Kousen",
    published: LocalDate = LocalDate.parse("2017-08-26")
) = Book(isbn, title, author, published)

val mjr = createBook()

...

data class MultiAuthorBook(
    val isbn: String,
    val title: String,
    val authors: List<String>,
    val published: LocalDate
)

fun createMultiAuthorBook(
    isbn: String = "9781617293290",
    title: String = "Kotlin in Action",
    authors: List<String> = listOf("Dimitry Jeremov", "Svetlana Isakova"),
    published: LocalDate = LocalDate.parse("2017-08-26")
) = MultiAuthorBook(isbn, title, authors, published)

μ—¬λŸ¬ 데이터에 JUnit 5 ν…ŒμŠ€νŠΈ λ°˜λ³΅ν•˜κΈ°

Junit 5μ—λŠ” μ‰Όν‘œλ‘œ κ΅¬λΆ„λœ κ°’(CSV)κ³Ό νŒ©ν† λ¦¬ λ©”μ†Œλ“œκ°€ ν¬ν•¨λœ μ˜΅μ…˜κ³Ό ν•¨κ»˜ 데이터 μ†ŒμŠ€λ₯Ό λͺ…μ‹œν•  수 μžˆλŠ” νŒŒλΌλ―Έν„°ν™”λœ ν…ŒμŠ€νŠΈκ°€ μžˆλ‹€.

πŸ‘‰πŸ» CSV 데이터λ₯Ό μ‚¬μš©ν•΄ νŒŒλ¦¬λ―Έν„°ν™”λœ ν…ŒμŠ€νŠΈ μˆ˜ν–‰

@ParameterizedTest
@CsvSource("1, 1", "2, 1", "3, 2",
    "4, 3", "5, 5", "6, 8", "7, 13",
    "8, 21", "9, 34", "10, 55")
fun `first 10 Fibonacci numbers (csv)`(n: Int, fib: Int) =
    assertThat(fibonacci(n)).isEqualTo(fib)

Junit 5μ—μ„œλŠ” νŒ©ν† λ¦¬ λ©”μ†Œλ“œλ₯Ό μ‚¬μš©ν•΄ ν…ŒμŠ€νŠΈ 데이터λ₯Ό 생성할 수 μžˆλ‹€.

πŸ‘‰πŸ» νŒŒλΌλ―Έν„° μ†ŒμŠ€λ‘œμ„œ μΈμŠ€ν„΄μŠ€ ν•¨μˆ˜μ— μ ‘κ·Ό

private fun fibnumbers() = listOf(
    Arguments.of(1, 1), Arguments.of(2, 1),
    Arguments.of(3, 2), Arguments.of(4, 3),
    Arguments.of(5, 5), Arguments.of(6, 8),
    Arguments.of(7, 13), Arguments.of(8, 21),
    Arguments.of(9, 34), Arguments.of(10, 55))
    
@ParameterizedTest(name = "fibonacci({0}) == {1}")
    @MethodSource("fibnumbers")
    fun `first 10 Fibonacci numbers (instance method)`(n: Int, fib: Int) =
        assertThat(fibonacci(n)).isEqualTo(fib)

ν…ŒμŠ€νŠΈ 수λͺ…μ£ΌκΈ°κ°€ κΈ°λ³Έ μ˜΅μ…˜μΈ Lifecycle.PER_METHOD 라면 ν…ŒμŠ€νŠΈ 데이터 μ†ŒμŠ€ ν•¨μˆ˜λ₯Ό λ™λ°˜ 객체 μ•ˆμ— μœ„μΉ˜μ‹œμΌœμ•Ό ν•œλ‹€.

companion object {
    // needed if parameterized test done with Lifecycle.PER_METHOD
    @JvmStatic
    fun fibs() = listOf(
        Arguments.of(1, 1), Arguments.of(2, 1),
        Arguments.of(3, 2), Arguments.of(4, 3),
        Arguments.of(5, 5), Arguments.of(6, 8),
        Arguments.of(7, 13), Arguments.of(8, 21),
        Arguments.of(9, 34), Arguments.of(10, 55))
}

@ParameterizedTest(name = "fibonacci({0}) == {1}")
@MethodSource("fibs")
fun `first 10 Fibonacci numbers (companion method)`(n: Int, fib: Int) =
    assertThat(fibonacci(n)).isEqualTo(fib)

νŒŒλΌλ―Έν„°ν™”λœ ν…ŒμŠ€νŠΈμ— data 클래슀 μ‚¬μš©ν•˜κΈ°

μž…λ ₯ κ°’κ³Ό μ˜ˆμƒ 값을 κ°μ‹ΈλŠ” data 클래슀λ₯Ό λ§Œλ“€κ³ 

λ§Œλ“  data 클래슀 기반의 ν…ŒμŠ€νŠΈ 데이터λ₯Ό μƒμ„±ν•˜λŠ” ν•¨μˆ˜λ₯Ό ν…ŒμŠ€νŠΈ λ©”μ†Œλ“œ μ†ŒμŠ€λ‘œμ„œ μ‚¬μš©ν•˜μž.

@JvmOverloads
tailrec fun fibonacci(n: Int, a: Int = 0, b: Int = 1): Int =
when (n) {
    0 -> a
    1 -> b
    else -> fibonacci(n - 1, b, a + b)
}

πŸ‘‰πŸ» μž…λ ₯κ³Ό μ˜ˆμƒλ˜λŠ” 좜λ ₯을 λ‹΄λŠ” 데이터 클래슀 μ •μ˜

  • 데이터 ν΄λž˜μŠ€λŠ” 이미 toString이 μž¬μ •μ˜λ˜μ–΄ μžˆμœΌλ―€λ‘œ

  • μž…λ ₯κ³Ό 좜λ ₯ μŒμ„ λ‚˜νƒ€λ‚΄λŠ” data 클래슀λ₯Ό μΈμŠ€ν„΄μŠ€ν™”ν•˜λŠ” νŒŒλΌλ―Έν„°ν™”λœ ν…ŒμŠ€νŠΈλ₯Ό μ‚¬μš©ν•˜λŠ” ν…ŒμŠ€νŠΈ λ©”μ†Œλ“œλ₯Ό μž‘μ„±ν•  수 μžˆλ‹€.

data class FibonacciTestData(val number: Int, val expected: Int)

...

@ParameterizedTest
@MethodSource("fibonacciTestData")
fun `check fibonacci using data class`(data: FibonacciTestData) {
    assertThat(fibonacci(data.number)).isEqualTo(data.expected)
}

private fun fibonacciTestData() = Stream.of(
    FibonacciTestData(number = 1, expected = 1),
    FibonacciTestData(number = 2, expected = 1),
    FibonacciTestData(number = 3, expected = 2),
    FibonacciTestData(number = 4, expected = 3),
    FibonacciTestData(number = 5, expected = 5),
    FibonacciTestData(number = 6, expected = 8),
    FibonacciTestData(number = 7, expected = 13)
)

μž…λ ₯/좜λ ₯

use둜 λ¦¬μ†ŒμŠ€ κ΄€λ¦¬ν•˜κΈ°

ν™•μ‹€ν•˜κ²Œ λ¦¬μ†ŒμŠ€λ₯Ό λ‹«κ³  μ‹Άμ§€λ§Œ 코틀린은 μžλ°”μ˜ try-with-resource ꡬ문을 μ§€μ›ν•˜μ§€ μ•ŠλŠ”λ‹€.

kotlin.io νŒ¨ν‚€μ§€μ˜ use λ˜λŠ” java.io.Reader의 useLines ν™•μž₯ ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜μž.

File.useLines

  • useLines의 첫 번째 선택적 μΈμžλŠ” 문자 집합이며 기본값은 UTF-8

  • 두 번째 μΈμžλŠ” 파일의 쀄을 λ‚˜νƒ€λ‚΄λŠ” Sequenceλ₯Ό μ œλ„€λ¦­ 인자 T둜 λ§€ν•‘ν•˜λŠ” λžŒλ‹€

public inline fun <T> File.useLines(
    charset: Charset = Charsets.UTF_8, 
    block: (Sequence<String>) -> T
): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    // BufferedReaderλ₯Ό μƒμ„±ν•˜κ³  BufferedReader의 use ν•¨μˆ˜μ— 처리λ₯Ό μœ„μž„
    return bufferedReader(charset).use { block(it.lineSequence()) }
}

...

fun get10LongestWordsInDictionary() =
    File("/usr/share/dict/words").useLines { line ->
        line.filter { it.length > 20 }
            .sortedByDescending(String::length)
            .take(10)
            .toList()
    }

@Test @EnabledOnOs(OS.MAC)
internal fun `10 longest words in dictionary`() {
    get10LongestWordsInDictionary().forEach { word -> println("$word (${word.length})") }
}

use ν•¨μˆ˜μ˜ μ‹œκ·Έλ‹ˆμ²˜

public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R

νŒŒμΌμ— κΈ°λ‘ν•˜κΈ°

File 클래슀의 ν™•μž₯ ν•¨μˆ˜μ—λŠ” 일반적인 μžλ°” μž…μΆœλ ₯ λ©”μ†Œλ“œλΏλ§Œ μ•„λ‹ˆλΌ 좜λ ₯ 슀트림과 Writerλ₯Ό λ¦¬ν„΄ν•˜λŠ” ν™•μž₯ ν•¨μˆ˜κ°€ μžˆλ‹€.

forEachLine ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•΄μ„œ νŒŒμΌμ„ μˆœνšŒν•  수 μžˆλ‹€.

  • 파일이 맀우 크지 μ•Šλ‹€λ©΄ File의 readLinesλ₯Ό ν˜ΈμΆœν•΄ λͺ¨λ“  쀄이 λ‹΄κΈ΄ μ»¬λ ‰μ…˜μ„ νšλ“ν•  μˆ˜λ„ μžˆλ‹€.

  • useLines ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•΄ 파일의 μ€„λ§ˆλ‹€ ν˜ΈμΆœλ˜λŠ” ν•¨μˆ˜λ₯Ό μ œκ³΅ν•  수 μžˆλ‹€.

  • 파일의 크기가 μž‘λ‹€λ©΄ readText λ˜λŠ” readBytesλ₯Ό μ‚¬μš©ν•΄ 전체 λ‚΄μš©μ„ 각각 λ¬Έμžμ—΄μ΄λ‚˜ λ°”μ΄νŠΈ λ°°μ—΄λ‘œ μ½μ–΄μ˜¬ 수 μžˆλ‹€.

νŒŒμΌμ— μ‘΄μž¬ν•˜λŠ” λ‚΄μš©μ„ λͺ¨λ‘ κ΅μ²΄ν•˜κ³  μ‹Άλ‹€λ©΄ writeText ν•¨μˆ˜λ₯Ό μ‚¬μš©

File("myfile.txt).writeText("My data")

File ν΄λž˜μŠ€μ—λŠ” νŒŒμΌμ— 데이터λ₯Ό μΆ”κ°€ν•˜λŠ” appendTextλΌλŠ” ν™•μž₯ ν•¨μˆ˜κ°€ μžˆλ‹€.

  • writeText와 appendText ν•¨μˆ˜λŠ” writeBytes와 appendBytes에 기둝 μž‘μ—…μ„ μœ„μž„νžŒλ‹€.

  • writeBytes와 appendBytesλŠ” 기둝이 λλ‚˜λ©΄ use ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•΄ νŒŒμΌμ„ ν™•μ‹€νžˆ λ‹«λŠ”λ‹€.

κ°œλ°œμžλŠ” OutputStreamWriter와 BufferedWriterλ₯Ό λ¦¬ν„΄ν•˜λŠ” writer(printWriter)와 bufferedWriter ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•  μˆ˜λ„ μžˆλ‹€.

File(fileName).printWriter().use { writer ->
    writer.println(data) }

κ·Έ λ°–μ˜ μ½”ν‹€λ¦° κΈ°λŠ₯

μ½”ν‹€λ¦° 버전 μ•Œμ•„λ‚΄κΈ°

μ½”νŠΈλ₯Ό μž‘μ„±ν•΄ ν˜„μž¬ μ‚¬μš© 쀑인 μ½”ν‹€λ¦° 버전을 μ•Œλ €λ©΄,

KotlinVersion 클래슀 λ™λ°˜ 객체의 CURRENT 속성을 μ‚¬μš©ν•˜μž.

πŸ‘‰πŸ» μ½”ν‹€λ¦° 버전 λΉ„κ΅ν•˜κΈ°

@Test
internal fun `comparison of KotlinVersion instances work`() {
    val v12 = KotlinVersion(major = 1, minor = 2)
    val v1341 = KotlinVersion(1, 3, 41)
    assertAll(
        { assertTrue(v12 < KotlinVersion.CURRENT) },
        { assertTrue(v1341 <= KotlinVersion.CURRENT) },
        { assertEquals(KotlinVersion(1, 3, 41),
            KotlinVersion(major = 1, minor = 3, patch = 41)) }
    )
}

@Test
internal fun `current version is at least 1_3`() {
    assertTrue(KotlinVersion.CURRENT.isAtLeast(major = 1, minor = 3))
    assertTrue(KotlinVersion.CURRENT.isAtLeast(major = 1, minor = 3, patch = 40))
}

반볡적으둜 λžŒλ‹€ μ‹€ν–‰ν•˜κΈ°

주어진 λžŒλ‹€ 식을 μ—¬λŸ¬ 번 μ‹€ν–‰ν•˜κ³  μ‹Άλ‹€λ©΄ μ½”ν‹€λ¦° λ‚΄μž₯ repeat ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜μž.

/**
 * times: λ°˜λ³΅ν•  횟수
 * action: μ‹€ν–‰ν•  (Int) -> Unit ν˜•μ‹μ˜ ν•¨μˆ˜
 */
@kotlin.internal.InlineOnly
public inline fun repeat(times: Int, action: (Int) -> Unit) {
    contract { callsInPlace(action) }

    for (index in 0 until times) {
        action(index)
    }
}

...

/*
 * Counting: 0
 * Counting: 1
 * Counting: 2
 * Counting: 3
 * Counting: 4
 */
repeat(5) {
    println("Counting: $it")
}

μ™„λ²½ν•œ when κ°•μ œν•˜κΈ°

πŸ‘‰πŸ» μ»΄νŒŒμΌλŸ¬μ—κ²Œ else μ ˆμ„ μš”κ΅¬ν•˜λ„λ‘ κ°•μš”

val <T> T.exhaustive: T
    get() = this

@Test
fun `test`() {
    fun printMod3Exhaustive(n: Int) {
        when (n % 3) {
            0 -> println("$n % 3 == 0")
            1 -> println("$n % 3 == 1")
            2 -> println("$n % 3 == 2")
            else -> println("Houston, we have a problem...")
        }.exhaustive
    }

    (1..10).forEach { printMod3Exhaustive(it) }
}

μ‹€ν–‰ κ°€λŠ₯ν•œ 클래슀 λ§Œλ“€κΈ°

ν΄λž˜μŠ€μ—μ„œ 단일 ν•¨μˆ˜λ₯Ό κ°„λ‹¨ν•˜κ²Œ ν˜ΈμΆœν•˜κ³  μ‹Άλ‹€λ©΄,

ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•  ν΄λž˜μŠ€μ—μ„œ invoke μ—°μ‚°μž ν•¨μˆ˜λ₯Ό μž¬μ •μ˜ν•˜μž.

data class Assignment(
    val name: String,
    val craft: String
)

data class AstroResult(
    val message: String,
    val number: Int,
    val people: List<Assignment>
)

...

class AstroRequest {
    companion object {
        private const val ASTRO_URL = "http://api.open-notify.org/astros.json"
    }

    // invoke μ—°μ‚°μž ν•¨μˆ˜λ₯Ό 톡해 μ‹€ν–‰ κ°€λŠ₯ν•œ 클래슀둜 μ‚¬μš©
    operator fun invoke(): AstroResult =
        Gson().fromJson(URL(ASTRO_URL).readText(), AstroResult::class.java)
}

...

@Test
fun `get people in space`() {
    // val request = AstroRequest() // AstroRequest 클래슀 μΈμŠ€ν„΄μŠ€ν™”
    // val result = request() // ν•¨μˆ˜μ²˜λŸΌ 클래슀λ₯Ό 호좜(invoke 호좜)
    var result = AstroRequest()()
    assertAll(
        { assertEquals("success", result.message) },
        { assertTrue { result.number >= 0 } },
        { assertEquals(result.number, result.people.size) }
    )
    }

invoke μ—°μ‚°μž ν•¨μˆ˜λ₯Ό μ œκ³΅ν•˜κ³  클래슀 λ ˆνΌλŸ°μŠ€μ— κ΄„ν˜Έλ₯Ό μΆ”κ°€ν•˜λ©΄ 클래슀 μΈμŠ€ν„΄μŠ€λ₯Ό λ°”λ‘œ μ‹€ν–‰ν•  수 μžˆλ‹€.

  • μ›ν•œλ‹€λ©΄ ν•„μš”ν•œ 인자λ₯Ό μΆ”κ°€ν•œ invoke ν•¨μˆ˜ 쀑볡도 μΆ”κ°€ν•  수 μžˆλ‹€.


κ²½κ³Ό μ‹œκ°„ μΈ‘μ •ν•˜κΈ°

μ½”λ“œ 블둝이 μ‹€ν–‰λ˜λŠ” 데 κ±Έλ¦° μ‹œκ°„μ„ μ•Œκ³  μ‹Άλ‹€λ©΄, measureTimeMillis λ˜λŠ” measureNanoTime ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜μž.

measureTimeMillis ν•¨μˆ˜μ˜ κ΅¬ν˜„

public inline fun measureTimeMillis(block: () -> Unit): Long {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    val start = System.currentTimeMillis()
    block()
    return System.currentTimeMillis() - start
}

πŸ‘‰πŸ» μ½”λ“œ λΈ”λ‘μ˜ κ²½κ³Ό μ‹œκ°„ μΈ‘μ •ν•˜κΈ°

fun doubleIt(x: Int): Int {
    Thread.sleep(100L)
    println("doubling $x with on thread ${Thread.currentThread().name}")
    return x * 2
}

/**
 * This machine has 10 processors
 * doubling 1 with on thread main
 * ...
 * doubling 8 with on thread main
 * Sequential stream took 845ms
 * doubling 6 with on thread main
 * doubling 8 with on thread ForkJoinPool.commonPool-worker-2
 * ...
 * doubling 1 with on thread ForkJoinPool.commonPool-worker-6
 * Parallel stream took 106ms
 */
fun main() {
    println("This machine has ${Runtime.getRuntime().availableProcessors()} processors")

    var time = measureTimeMillis {
        IntStream.rangeClosed(1, 8)
            .map { doubleIt(it) }
            .sum()
    }
    println("Sequential stream took ${time}ms")

    time = measureTimeMillis {
        IntStream.rangeClosed(1, 8)
            .parallel()
            .map { doubleIt(it) }
            .sum()
    }
    println("Parallel stream took ${time}ms")

}

더 μ •ν™•ν•œ μ„±λŠ₯ 츑정을 μ›ν•œλ‹€λ©΄ μ˜€ν”ˆ JDK의 μžλ°” 마이크둜벀치마크 도ꡬJMH, Java Microbenchmark Harnessν”„λ‘œμ νŠΈλ₯Ό μ‚¬μš©ν•˜μž.


μŠ€λ ˆλ“œ μ‹œμž‘ν•˜κΈ°

μŠ€λ ˆλ“œ ν™•μž₯ ν•¨μˆ˜μ˜ μ‹œκ·Έλ‹ˆμ²˜

public fun thread(
    start: Boolean = true,
    isDaemon: Boolean = false,
    contextClassLoader: ClassLoader? = null,
    name: String? = null,
    priority: Int = -1,
    block: () -> Unit
): Thread 

πŸ‘‰πŸ» λ‹€μˆ˜μ˜ μŠ€λ ˆλ“œλ₯Ό μž„μ˜μ˜ κ°„κ²©μœΌλ‘œ μ‹œμž‘ν•˜κΈ°

/**
 * Thread-0 for 0 after 2ms
 * Thread-1 for 1 after 345ms
 * Thread-2 for 2 after 370ms
 * Thread-3 for 3 after 153ms
 * Thread-4 for 4 after 138ms
 * Thread-5 for 5 after 879ms
 */
(0..5).forEach { n ->
    val sleepTime = Random.nextLong(range = 0..1000L)
    thread {
        Thread.sleep(sleepTime)
        println("${Thread.currentThread().name} for $n after ${sleepTime}ms")
    }.join()
}

Thread ν™•μž₯ ν•¨μˆ˜λŠ” μžμ‹ μ˜ λ³Έλ¬Έμ—μ„œ μƒμ„±ν•œ μŠ€λ ˆλ“œλ₯Ό λ¦¬ν„΄ν•˜λ―€λ‘œ, μŠ€λ ˆλ“œμ˜ join λ©”μ†Œλ“œλ₯Ό μ‚¬μš©ν•΄μ„œ λͺ¨λ“  μŠ€λ ˆλ“œλ₯Ό 순차적으둜 ν˜ΈμΆœν•˜κ²Œ λ§Œλ“€ 수 μžˆλ‹€.

(0..5).forEach { n ->
    thread {
    // ...
    }.join() // λ¦¬ν„΄λœ μŠ€λ ˆλ“œμ˜ invoke λ©”μ†Œλ“œλ₯Ό μ‚¬μš©ν•  수 μžˆλ‹€.
}

TODO둜 μ™„μ„± κ°•μ œν•˜κΈ°

πŸ‘‰πŸ» TODO ν•¨μˆ˜μ˜ κ΅¬ν˜„

  • νš¨μœ¨μ„±μ„ 이유둜 μ†ŒμŠ€λŠ” μΈλΌμΈλ˜μ–΄ 있고, ν•¨μˆ˜κ°€ 호좜될 λ•Œ NotImplementedError λ°œμƒ

public inline fun TODO(reason: String): Nothing = 
    throw NotImplementedError("An operation is not implemented: $reason")

...

fun main() {
    TODO(reason = "none, really")
}

Random의 λ¬΄μž‘μœ„ λ™μž‘ μ΄ν•΄ν•˜κΈ°

πŸ‘‰πŸ» nextInt ν•¨μˆ˜

@Test
internal fun `nextInt with no args gives any Int`() {
    val value = Random.nextInt()
    assertTrue(value in Int.MIN_VALUE..Int.MAX_VALUE)
}

@Test
internal fun `nextInt with a range gives value between 0 and limit`() {
    val value = Random.nextInt(10)
    assertTrue(value in 0..10)
}

@Test
internal fun `nextInt with min and max gives value between them`() {
    val value = Random.nextInt(5, 10)
    assertTrue(value in 5..10)
}

@Test
internal fun `nextInt with range returns value in range`() {
    val value = Random.nextInt(7..12)
    assertTrue(value in 7..12)
}

πŸ‘‰πŸ» μ‹œλ“œ κ°’κ³Ό ν•¨κ»˜ λ‚œμˆ˜ 생성기 μ‚¬μš©ν•˜κΈ°

@Test
internal fun `Random function produces a seeded generator`() {
    val r1 = Random(12345)
    val nums1 = (1..10).map { r1.nextInt() }

    val r2 = Random(12345)
    val nums2 = (1..10).map { r2.nextInt() }

    // println(nums1)
    assertEquals(nums1, nums2)
}

μŠ€ν”„λ§ ν”„λ ˆμž„μ›Œν¬

μ½”ν‹€λ¦°μœΌλ‘œ μŠ€ν”„λ§ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μž‘μ„±ν•  λ•Œ μ‚¬μš©ν•  수 μžˆλŠ” λͺ‡ 가지 κΈ°μˆ λ“€

ν™•μž₯을 μœ„ν•΄ μŠ€ν”„λ§ 관리 빈 클래슀 μ˜€ν”ˆν•˜κΈ°

ν™•μž₯을 μœ„ν•΄ μžλ™μœΌλ‘œ ν•„μš”ν•œ μŠ€ν”„λ§ 관리 클래슀λ₯Ό μ—΄μ–΄μ£ΌλŠ” μ½”ν‹€λ¦° μŠ€ν”„λ§ ν”ŒλŸ¬κ·ΈμΈμ„ λΉŒλ“œ νŒŒμΌμ— μΆ”κ°€ν•˜μž

코틀린은 기본적으둜 μ •μ μœΌλ‘œ κ²°ν•©ν•œλ‹€.

  • ν΄λž˜μŠ€κ°€ open ν‚€μ›Œλ“œλ₯Ό μ‚¬μš©ν•΄ ν™•μž₯을 μœ„ν•œ μ—΄λ¦ΌμœΌλ‘œ ν‘œμ‹œλ˜μ§€ μ•ŠμœΌλ©΄ λ©”μ†Œλ“œ μž¬μ •μ˜ λ˜λŠ” 클래슀 ν™•μž₯이 λΆˆκ°€λŠ₯ν•˜λ‹€.

  • 코틀린은 이 문제λ₯Ό all-open ν”ŒλŸ¬κ·ΈμΈμœΌλ‘œ ν•΄κ²°ν•œλ‹€.

  • all-open ν”ŒλŸ¬κ·ΈμΈμ€ ν΄λž˜μŠ€μ™€ ν΄λž˜μŠ€μ— ν¬ν•¨λœ ν•¨μˆ˜μ— λͺ…μ‹œμ μœΌλ‘œ open ν‚€μ›Œλ“œλ₯Ό μΆ”κ°€ν•˜μ§€ μ•Šκ³  λͺ…μ‹œμ μΈ open μ• λ…Έν…Œμ΄μ…˜μœΌλ‘œ 클래슀λ₯Ό μ„€μ •ν•œλ‹€.

kotlin-spring ν”ŒλŸ¬κ·ΈμΈμ€ μ•„λž˜ μ• λ…Έν…Œμ΄μ…˜μœΌλ‘œ 클래슀λ₯Ό 열도둝 μ„€μ •λ˜μ–΄ μžˆλ‹€.

  • @Component

  • @Async

  • @Transactional

  • @Cacheable

  • @SpringBootTest

implementation("org.jetbrains.kotlin.plugin.spring:org.jetbrains.kotlin.plugin.spring.gradle.plugin:2.0.21")

kotlin-spring이 μ œκ³΅ν•˜λŠ” 것보닀 더 많이 ν•„μš”ν•˜λ‹€λ©΄ all-open ν”ŒλŸ¬κ·ΈμΈλ„ μΆ”κ°€ν•  수 μžˆμ§€λ§Œ 거의 ν•„μš” μ—†λ‹€.


μ½”ν‹€λ¦° data 클래슀둜 νΌμ‹œμŠ€ν„΄μŠ€ κ΅¬ν˜„ν•˜κΈ°

data 클래슀둜 JPAλ₯Ό μ‚¬μš©ν•˜κ³  μ‹Άλ‹€λ©΄ kotlin-jpa ν”ŒλŸ¬κ·ΈμΈμ„ μΆ”κ°€ν•˜μž.

JPA κ΄€μ μ—μ„œ data ν΄λž˜μŠ€λŠ” 두 가지 λ¬Έμ œκ°€ μžˆλ‹€.

1️⃣ JPAλŠ” λͺ¨λ“  속성에 기본값을 μ œκ³΅ν•˜μ§€ μ•ŠλŠ” 이상 κΈ°λ³Έ μƒμ„±μžκ°€ ν•„μˆ˜μ§€λ§Œ data ν΄λž˜μŠ€λŠ” κΈ°λ³Έ μƒμ„±μžκ°€ μ—†λ‹€.

  • no-arg ν”ŒλŸ¬κ·ΈμΈμ€ μΈμžκ°€ μ—†λŠ” μƒμ„±μžλ₯Ό μΆ”κ°€ν•  클래슀λ₯Ό 선택할 수 있고

  • κΈ°λ³Έ μƒμ„±μž μΆ”κ°€λ₯Ό ν˜ΈμΆœν•˜λŠ” μ• λ„ˆν…Œμ΄μ…˜μ„ μ •μ˜ν•  μˆ˜λ„ μžˆλ‹€.

kotlin-jpa ν”ŒλŸ¬κ·ΈμΈμ€ no-arg ν”ŒλŸ¬κ·ΈμΈμ„ 기반으둜 λ§Œλ“€μ–΄ 쑌고, μ•„λž˜ μ• λ…Έν…Œμ΄μ…˜μœΌλ‘œ μžλ™ ν‘œμ‹œλœ ν΄λž˜μŠ€μ— κΈ°λ³Έ μƒμ„±μžλ₯Ό μΆ”κ°€ν•œλ‹€.

  • @Entity

  • @Embeddable

  • @MappedSuperclass

kotlin("plugin.jpa") version "1.2.71"

2️⃣ val 속성과 ν•¨κ»˜ data 클래슀λ₯Ό μƒμ„±ν•˜λ©΄ λΆˆλ³€ 객체가 μƒμ„±λ˜λŠ”λ°, JPAλŠ” λΆˆλ³€ 객체와 λ”λΆˆμ–΄ 잘 λ™μž‘ν•˜λ„λ‘ μ„€κ³„λ˜μ§€ μ•Šμ•˜λ‹€.

  • μ—”ν‹°ν‹°λ‘œ μ‚¬μš©ν•˜κ³  싢은 μ½”ν‹€λ¦° ν΄λž˜μŠ€μ— (data 클래슀 λŒ€μ‹ ) ν•„λ“œ 값을 λ³€κ²½ν•  수 있게 속성에 var νƒ€μž…μ„ μ‚¬μš©ν•˜λŠ” λ‹¨μˆœ 클래슀 μ‚¬μš©μ„ μΆ”μ²œ

πŸ‘‰πŸ» λ°μ΄ν„°λ² μ΄μŠ€ ν…Œμ΄λΈ”λ‘œ λ§€ν•‘λ˜λŠ” μ½”ν‹€λ¦° 클래슀

@Entity
class Article(
    var title: String,
    var headline: String,
    var content: String,
    @ManyToOne var author: User,
    var slug: String = title.toSlug(),
    var addedAt: LocalDateTime = LocalDateTime.now(),
    @Id @GeneratedValue var id: Long? = null)

@Entity
class User(
    var login: String,
    var firstname: String,
    var lastname: String,
    var description: String? = null,
    @Id @GeneratedValue var id: Long? = null)

ref. Building web applications with Spring Boot and Kotlin


μ˜μ‘΄μ„± μ£Όμž…ν•˜κΈ°

μ½”ν‹€λ¦° μŠ€ν”„λ§μ€ μƒμ„±μž μ£Όμž…μ„ μ œκ³΅ν•˜μ§€λ§Œ ν•„λ“œ μ£Όμž…μ—λŠ” lateinit var ꡬ쑰λ₯Ό μ‚¬μš©ν•΄μ•Ό ν•œλ‹€.

선택적인 λΉˆμ€ 널 ν—ˆμš© νƒ€μž…μœΌλ‘œ μ„ μ–Έν•œλ‹€.

ν΄λž˜μŠ€μ—μ„œ μƒμ„±μžκ°€ ν•˜λ‚˜λΏμ΄λΌλ©΄ μŠ€ν”„λ§μ΄ μžλ™μœΌλ‘œ 클래슀의 μœ μΌν•œ μƒμ„±μžμ— λͺ¨λ“  인자λ₯Ό μžλ™μœΌλ‘œ μ˜€ν† μ™€μ΄μ–΄λ§ν•˜κΈ° λ•Œλ¬Έμ— @Autowired μ• λ…Έν…Œμ΄μ…˜μ„ μ‚¬μš©ν•  ν•„μš”κ°€ μ—†λ‹€.

πŸ‘‰πŸ» μŠ€ν”„λ§μœΌλ‘œ μ˜μ‘΄μ„± μ˜€ν† μ™€μ΄μ–΄λ§ν•˜κΈ°

/** 단일 μƒμ„±μžλ₯Ό κ°–λŠ” 클래슀 */
@RestController
class GreetingController(val service: GreetingService) { /* ... */ }

/** λͺ…μ‹œμ μœΌλ‘œ μ˜€ν† μ™€μ΄μ–΄λ§ */
@RestController
class GreetingController(@Autowired val service: GreetingService) { /* ... */ }

/** μ˜€ν† μ™€μ΄μ–΄λ§ μƒμ„±μž 호좜. 주둜 λ‹€μˆ˜μ˜ μ˜μ‘΄μ„±μ„ κ°–λŠ” 클래슀 */
@RestController
class GreetingController @Autowired constructor(val service: GreetingService) { 
    // ...
}

/** ν•„λ“œ μ£Όμž…(λΉ„μΆ”μ²œν•˜μ§€λ§Œ μœ μš©ν•  수 μžˆλ‹€) */
@RestController
class GreetingController {
    @Autowired 
    lateinit var service: GreetingService

    // ...
}

클래슀의 속성이 ν•„μˆ˜κ°€ μ•„λ‹ˆλΌλ©΄ ν•΄λ‹Ή 속성을 널 ν—ˆμš© νƒ€μž…μœΌλ‘œ μ„ μ–Έν•  수 μžˆλ‹€.

πŸ‘‰πŸ» 선택 κ°€λŠ₯ν•œ νŒŒλΌλ―Έν„°λ₯Ό κ°–λŠ” 컨트둀러 ν•¨μˆ˜

@GetMapping("/hello")
fun greetUser(@RequestParam name: String?) = 
    Greeting(service.sayHello(name ?: "World"))

...

@DataJpaTest
class RepositoriesTests @Autowired constructor (
    val entityManager: TestEntityManager,
    val userRepository: UserRepository,
    val articleRepository: ArticleRepository) {
    // ...
}

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {
    // ...
}

Last updated