728x90

https://kotlinlang.org/docs/control-flow.html

 

Conditions and loops | Kotlin

 

kotlinlang.org

 

Conditions and loops

If expression

코틀린에서는 if는 표현식이다.(값을 반환할 수 있다.)

그렇기에 코틀린에서는 삼항 연산자(condition ? then : else)가 없다.

If가 이 역할을 하기 때문이다.

fun main(){
    val a = 1
    val b = 2
    
    var max = 0
    
    max = if (a > b) a else b
    
    val maxOr100 = if (a > 100) a else if(b > 100) b else 100
}

 

When expression

When은 다양한 곳을 분기할 수 있는 조건식을 정의한다.

C언어에서의 switch라고 생각하면 된다.

fun main(){
    val x = 0
    
    when(x){
        0 -> println("x is 0")
        1 -> println("x is 1")
        2 -> println("x is 2")
        else -> println("x is neither 1 nor 2")
    }
}

When은 만족하는 condition이 있을 때까지 순서대로 매칭해 간다.

else는 아무것에도 해당하지 않을 때를 정의한다.

 

만약 When이 표현식으로 사용된다면 Else branch는 필수적이다.

그래도 컴파일러가 모든 경우의 수를 커버하고 있다고 알 수 있으면, Else는 생략 가능하다.

enum class Bit{
    ZERO, ONE
}

fun main(){
    val a = Bit.ZERO
    
    when(a){
        Bit.ZERO -> println("Bit.ONE")
        Bit.ONE -> println("Bit.TWO")
    }
}

 

값이 범위 안에 속하는지도 확인할 수 있다.

fun main(){
    val a = 5
    
    when(a){
        in 1..10 -> println(a)
        else -> println("else")
    }
}

 

여기서도 is로 타입을 검사하면 스마트캐스팅이 되어 추가적으로 검사할 필요가 없다.

fun hasPrefix(x: Any) = when(x) {
    is String -> x.startsWith("prefix")
    else -> false
}

 

For loops

for는 iterator를 순환합니다.

for (item in collection) print(item)

 

숫자를 반복하고 싶다면, range expression을 사용한다.

for (i in 1..3) {
    println(i)
}
for (i in 6 downTo 0 step 2) {
    println(i)
}

 

While loops

while과 do-while은 조건이 만족할 때까지 계속 ㅁ반복한다.

while: 조건을 체크하고 그것이 참이면 실행한다. 이것을 반복한다.

do-while: 일단 블록을 실행하고 조건을 체크한다.

while (x > 0) {
    x--
}

do {
    val y = myFunc()
} while (y != null) // y is visible here!

 

Returns and jumps

Returns and jumps

fun main(){
    val a:Int? = null

    val b = (a ?: return).toString()

    println(b)
}

return은 본인이 속한 가장 가까운 함수를 리턴한다

break은 본인이 속한 가장 가까운 loop를 정지한다.

continue는 루프의 다음 스텝으로 넘어간다.

 

Break and continue labels

fun main(){
    loop@ for (i in 1..10) {
        for (j in 1..10) {
            if(i != j) break@loop
            println("$i $j")
        }
    }
}

코틀린에서는 표현식에 label을 붙일 수 있다.

라벨은 @로 끝나며, abc@와 같은 형태가 된다.

break와 continue를 사용하여 해당 부분으로 jump 할 수 있다.

 

Return to labels

fun main(){
    listOf(1, 2, 3, 4, 5).forEach lit@{
        if (it == 3) return@main
        print(it)
    }
    print(" done with explicit label")
}

이렇게 return에서도 라벨을 사용하여 리턴할 수 있다.

 

Exceptions

예외 처리는 크게 2가지로 나뉜다.

Throwing exceptions: 문제가 발생 했을 때 알린다.

Catching exceptions: 문제가 발생 했을 때 어떤 식으로 처리할 것인지 다룬다.

 

이런 예외 처리는 코드를 더 예측가능하게 도와줄 수 있다.

 

Throw exceptions

throw IllegalArgumentException()

throw 키워드를 사용하여 Exception을 발생시킨다.

Exception이 발생하는 것은 런타임 중에 예상하지 못한 문제가 발생했다는 것을 알리는 것이다.

 

Throw Exceptions with precondition functions

코틀린은 조건을 체크하고 에러를 발생시켜주는 precondition function이 존재한다.

Precondition function Use case Exception thrown
require() 유저의 input이 valid 한지 확인 IllegalArgumentException
check() Object 혹은 변수의 상태가 valid 한지 확인 IllegalStateException
error() 잘못된 상태나 조건을 알린다. IllegalStateException

 

  • require() function
fun getIndices(count: Int): List<Int> {
    require(count >= 0) { "Count must be non-negative. You set count to $count." }
    return List(count) { it + 1 }
}
  • check() function
fun main() {
    var someState: String? = null

    fun getStateValue(): String {

        val state = checkNotNull(someState) { "State must be set beforehand!" }
        check(state.isNotEmpty()) { "State must be non-empty!" }
        return state
    }
    // If you uncomment the line below then the program fails with IllegalStateException
    // getStateValue()

    someState = ""

    // If you uncomment the line below then the program fails with IllegalStateException
    // getStateValue() 
    someState = "non-empty-state"

    // This prints "non-empty-state"
    println(getStateValue())
}

check에서 condition이 false라면 exception을 throw한다.

  • error() function
class User(val name: String, val role: String)

fun processUserRole(user: User) {
    when (user.role) {
        "admin" -> println("${user.name} is an admin.")
        "editor" -> println("${user.name} is an editor.")
        "viewer" -> println("${user.name} is a viewer.")
        else -> error("Undefined role: ${user.role}")
    }
}

fun main() {
    // This works as expected
    val user1 = User("Alice", "admin")
    processUserRole(user1)
    // Alice is an admin.

    // This throws an IllegalStateException
    val user2 = User("Bob", "guest")
    processUserRole(user2)
}

그냥 exception을 발생시켜버린다.

 

Handle exceptions using try-catch blocks

exception이 발생하면, 그래도 프로그램을 이어가기 위해 exception을 handling 해야 한다.

그럴 때 try-catch를 사용한다.

try {
    // exception을 발생시키는 코드
} catch (e: YourException) {
    // exception을 handling 하는 코드
}

 

또한 if와 마찬가지로 try-catch는 expression으로 사용 할 수 있다.

fun main() {
    val num: Int = try {
    	10/0
    } catch (e: ArithmeticException) {
        -1
    }
    println("Result: $num")
}

 

또, 하나의 try에 대하여 여러개의 Catch를 작성 할 수 있다.

 

The finally block

finally block은 무조건 실행되는 부븐이다.

이 finally block은 try에서 exception이 발생하더라도 무조건 실행된다.

그렇기에 finally는 네트워크나 파일과 같은 리소스를 Close 하는 것에서 중요하다.

try {
    // Exception이 발생 할 수 있는 코드
}
catch (e: YourException) {
    // Exception을 handling 하는 코드
}
finally {
    // 무조건 실행되어야 하는 코드
}

이 finally 블록은 catch 블록을 작성하지 않아도 작성할 수 있다.

 

 

'백엔드 > 코틀린' 카테고리의 다른 글

Idioms  (1) 2024.10.01
Basic syntax  (0) 2024.09.28
코틀린 시작  (0) 2024.09.28
728x90

https://kotlinlang.org/docs/idioms.html

 

Idioms | Kotlin

 

kotlinlang.org

 

Create DTOs (POJOs/POCOs)

코틀린에서는 Data만 가지는 클래스를 Data class라고 따로 정의할 수 있다.

data class User(val name: String, val email: String)

 

여기에서 POJOs, POCOs의 개념이 나오는데

POJO(Plain Old Java Object):  자바 모델이나 기능, 프레임워크 등을 따르지 않은 자바 오브젝트를 지칭하는 말이다.

POCO(Plain Old CLR Object)는 자바가 아닌 닷넨 오브젝트들을 말한다.

Default values for function parameters

fun foo(a: Int = 0, b: String = "") { ... }

 

함수를 선언 할 때, 함수에 기본 값을 줄 수 있다.

foo(1, "123) 이렇게 호출 할 수도 있지만, foo(1) 이렇게 호출 할 수도 있는 것이다.

이런 경우에는 미리 선언해둔 기본값이 들어가게 된다.

 

Filter a list

fun main(){
    val list = listOf(1, 2, 3, 4, 5)

    val filteredList1 = list.filter{
        x -> x > 3
    }

    println(filteredList1)

    val filteredList2 = list.filter{it > 3}

    println(filteredList2)
}

 

리스트에서 조건 filter를 통해, 원하는 값들을 추출 할 수 있다.

이 때 람다 뿐 만 아니라, it을 사용해서도 filter 할 수 있다.

 

Check the presence of an element in a collection

fun main(){

    val emailsList = listOf("trust1204@gmail.com", "trust1204@naver.com")

    if ("trust1204@gmail.com" in emailsList) {
        println("HI trust1204@gmail.com")
    }
}

컬렉션에서 in을 사용하여 해당 값이 컬렉션에 존재하는지 확인 할 수 있다.

String interpolation

fun main(){
    
    val name = "seungkyu"
    
    println("hi $name")
}

$을 통해 문자열 안에 문자열을 끼워 넣을 수 있다.

 

Instance checks

open class A

class B: A()

fun main(){

    val a = A()

    when(a){
        is B -> println("It is B")
        is A -> println("It is A")
        else -> println("I dont know")
    }

}

 

is [Class name]을 통해 해당 클래스의 인스턴스인지 검사 할 수 있다.

 

Read-only list

val list = listOf(1, 2, 3)

이렇게 읽을 수만 있는 리스트를 생성할 수 있다.

이 데이터는 add, put 과 같은 함수로 추가 혹은 수정이 불가능하다. 

Read-only map

val map = mapOf("a" to 1, "b" to 2, "c" to 3)

위와 마찬가지로 읽을 수만 있는 map을 생성할 수 있다.

to로 key와 value를 mapping 한다.

 

Access a map entry

fun main(){
    val map = mapOf("a" to 1, "b" to 2, "c" to 3)

    println(map["a"])

}

이런 식으로 map에 접근하여 데이터를 읽을 수 있다.

 

Traverse a map or a list of pairs

fun main(){
    val map = mapOf("a" to 1, "b" to 2, "c" to 3)

    for((k,v) in map){
        println("$k = $v")
    }

}

map의 key와 value를 반복문으로 한 번에 읽어올 수 있다.

 

Iterate over a range

for (i in 1..10) { ... }  //includes 10
for (i in 1..<10) { ... } //not include 10
for (x in 2..10 step 2) { ... }
for (x in 10 downTo 1) { ... }
(1..10).forEach { ... }

..과 <을 이용해서 iterate의 범위를 정할 수 있다.

Lazy property

fun main(){
    val p: String by lazy { 
        if(0 > 1)
            "Hello World"
        else
            "Hi world"
    }
}

val 타입에서 by lazy로 실행된 이후에 값을 할당 할 수 있다. 

Create a singleton

Object Seungkyu{
	val name = "seungkyu"
}

Object 키워드를 사용하여 싱글톤 객체를 만들 수 있다.

Use inline value classes for type-safe values

@JvmInline
value class UserId(private val id: String)

value 클래스로 만들면, 클래스가 데이터를 직접 참조하기 때문에 객체 생성에 대한 cost를 아낄 수 있다고 한다.

 

Instantiate an abstract class

abstract class InstantiateAnAbstractClass {
    abstract fun doSomething()
    abstract fun sleep()
}

fun main(){
    val myObject = object : InstantiateAnAbstractClass() {
        override fun doSomething() {
            println("It is doSomething")
        }

        override fun sleep() {
            println("It is sleeping")
        }
    }

    myObject.doSomething()
}

이렇게 추상 클래스를 바로 만들 수 있다.

If-not-null shorthand

fun main(){
    val a: String? = "seungkyu!!"

    println(a?.length)
}

null 가능 변수에 ?. 을 붙여서 만약 null이 아니면 가져올 값을 지정할 수 있다.

 

If-not-null-else shorthand

fun main(){
    val a: String? = null

    println(a?.length ?: "hello!!")
}

null이 아닐 때 뿐만 아니라, null 일 때 가져올 값을 지정할 수 있다.

만약 ?: 앞의 값이 null이라면 뒤에 있는 값으로 가져오게 된다.

 

Execute a statement if null

fun main(){
    val values= mapOf<String, Int>()

    println(values["a"] ?: throw NullPointerException())
}

 

?: 를 통해 null 일 때 Exception을 throw 할 수 있다.

 

Get first item of a possibly empty collection

fun main(){
    val emails = listOf<String>()

    val mainEmail = emails.firstOrNull()

    println(mainEmail)
}

이런 식으로 first item을 가져오거나 null을 가져 올 수 있다.

 

Execute if not null

fun main(){
    val value1 = "String"
    val value2:String? = null

    value1?.let {
        println(it)
    }

    value2?.let {
        println(it)
    }
}

?.let을 이용하여 해당 데이터가 null이 아닐 때 실행할 값을 실행할 수 있다.

 

Return on when statement

fun main(){
    println(myFunc(3))
}

fun myFunc(value: Int): String{
    return when(value){
        1 -> "one"
        2 -> "two"
        3 -> "three"
        else -> "no"
    }
}

 

이렇게 함수 Return 부분에서 when을 사용하여 값을 return 할 수 있다.

이 때, 어떠한 경우에도 값이 return 될 수 있도록 꼭 when을 지정해주어야 한다.

 

try-catch expression

fun main(){
    val result = try{
        throw NullPointerException()
    }catch (e: NullPointerException){
        1
    }

    println(result)
}

값을 할당 할 때에도, try-catch Expression을 사용하여 값을 대입할 수 있다.

 

Builder-style usage of methods that return Unit

fun main(){
    arrayOfMinusOnes(10).forEach { print("$it ") }
}

fun arrayOfMinusOnes(size: Int): IntArray {
    return IntArray(size).apply { fill(-1) }
}

return 할 때에도, 자바의 Builder 스타일을 사용해 값을 반환 할 수 있다.

 

Single-expression functions

fun theResult1() = 1111

fun theResult2(value: Int) = when(value){
    1 -> "one"
    2 -> "two"
    else -> "other"
}

fun main(){
    println(theResult1())
    println(theResult2(1))
}

함수를 이렇게 하나의 표현식으로도 작성할 수 있다.

 

Call multiple methods on an object instance (with)

class Bird{
    fun fly(){
        println("Bird fly")
    }

    fun go(x: Int, y: Int){
        println("Bird go $x $y")
    }
}

fun main(){
    val myBird = Bird()

    with(myBird){
        fly()

        for(i in 1..3){
            go(i,i)
        }
    }
}

with를 사용해서 하나의 인스턴스에서 다양한 함수들을 호출 할 수 있다.

 

Generic function that requires the generic type information

inline fun <reified T: Any> hello(value: String): T =  value as T

fun main() {
    
}

타입 정보를 같이 받는 Generic Function을 구현 할 수도 있다.

 

Mark code as incomplete (TODO)

fun main(){
    println("Hello Kotlin")
    TODO("아직이요")
}

TODO를 이용하여 아직 작성하지 않은 코드라고 알릴 수 있다.

 

자동으로 코드를 삽입해주어서, 실행이 가능하다.

물론 TODO()의 코드에 도착하면 그때는 에러가 발생한다.

'백엔드 > 코틀린' 카테고리의 다른 글

Concepts:Control flow  (1) 2024.10.05
Basic syntax  (0) 2024.09.28
코틀린 시작  (0) 2024.09.28
728x90

https://kotlinlang.org/docs/basic-syntax.html

 

Basic syntax | Kotlin

 

kotlinlang.org

 

Package definition and imports

패키지는 소스파일 가장 위에 명시한다.

package seungkyu

class PackageDefinitionAndImports {

}

 

코틀린에서는 디렉토리와 패키지를 똑같이 맞출 필요는 없다고 한다.

 

Program entry point

코틀린 애플리케이션이 시작하는 부분은 main 함수 부분이다.

package seungkyu

fun main(args: Array<String>) {
   println("Hello, world!")

    println(args[0])
}

 

만약 애플리케이션 실행에서 매개변수를 받고 싶으면, 메인 함수에 저렇게 문자열 배열로 받을 수 있다.

 

print 함수는 매개변수를 standard output에 출력한다.

package seungkyu

fun main(){
    print("seungkyu")
    
    println("seungkyu")
}

println은 print에서 한 line을 차지하여 출력하는 함수이다.

다음 출력은 해당 출력의 다음줄부터 시작된다.

Read from the standard input

readln은 standard input으로부터 읽어오는 함수이다.

이 함수는 한 줄 전체를 읽어온다.

package seungkyu

fun main(){
    print("Enter your name: ")

    val name = readln()

    print("Your name: ")
    println(name)

    println("is contain new line? : ${name.contains("\n")}")
}

 

엔터키는 제외하고 읽어오는 것을 볼 수 있다.

Functions

아래는 2개의 Int를 매개변수로 받고 Int를 반환하는 함수이다.

fun sum1(a: Int, b: Int): Int {
    return a + b
}

 

아래와 같이 명시적이라면 return과 괄호를 생략 할 수 있다.

fun sum2(a: Int, b: Int) = a + b

 

만약 return이 아무것도 반환하지 않는다면 Unit으로 return type을 준다.

fun printSum(a: Int, b: Int): Unit {
    println("sum of $a and $b is ${a + b}")
}

 

이 때 Unit은 생략이 가능하다.

fun printSum(a: Int, b: Int) {
    println("sum of $a and $b is ${a + b}")
}

 

Variables

코틀린은 val, var 키워드로 변수 선언이 가능하며, 그 뒤에 변수의 이름을 적어준다.

 

val은 할당이 한 번만 일어나는 변수를 선언 할 때 사용하며, 후에 값이 변하지 않는다.

선언 이후에는 read-only이다.

val x: Int = 5

 

그에 비해 var은 선언 이후에도 값을 변경 할 수 있다.

fun main(){
    var x: Int = 5
    x += 1
}

 

위에서는 변수의 type을 :로 같이 명시해 준 것을 볼 수 있는데, 사실 코틀린은 변수의 타입을 추정이 가능하기 때문에 변수 타입은 생략을 해주어도 된다.

 

var x = 5

 

아래와 같이 변수의 타입까지만 선언해두고 값은 나중에 할당하는 것도 가능하다.

fun main(){
    val x = 5
    
    val c: Int
    c = 3
}

 

자바와는 다르게, 변수를 클래스 밖인 top level에 선언도 가능하다.

val value = 0

fun main(){

    println(value)
}

 

Creating classes and instances

늘 그렇듯, 클래스를 정의하기 위해서는 class 키워드를 사용한다.

class Shape

 

클래스의 속성들은 선언이나 body에 작성할 수 있다.

class Rectangle(val height: Double, val length: Double){
    val perimeter = (height + length) * 2
}

 

이 때, 클래스 선언부에 작성된 매개변수를 바탕으로 기본 생성자가 만들어진다.

class Rectangle(val height: Double, val length: Double){
    val perimeter = (height + length) * 2
}

fun main(){
    val rectangle = Rectangle(10.0, 20.0)
    println("The Perimeter is ${rectangle.perimeter}")
}

 

클래스 간의 상속은 : 을 사용하면 된다.

클래스는 기본적으로 상속이 불가능한 final 상태이지만, 앞에 open 키워드를 붙여주면 상속 가능한 클래스가 된다.

 

open class Shape

class Rectangle(val height: Double, val length: Double): Shape() {
    val perimeter = (height + length) * 2
}

Comments

주석은 //와 /* */를 사용해서 작성 할 수 있다.

//주석입니다.

/* 이것은
주석 블럭입니다 */

 

String templates

기본적으로 String은 ""이런 형태로 있을 것이다.

자바에서는 문자열들을 붙일 때 +를 사용했지만 코틀린에서는 $을 사용하여 문자열 안에 넣을 수 있다.

val mystring = "${1+2}, but now is $a"

 

Conditional expressions

조건식도 If를 사용한다.

fun max_result(a: Int, b: Int): Int{
	if (a > b){
    	return a
    }
    else{
    	return b
    }
}

 

코틀린에서는 3항 연산자가 없으며, 대신 if 또한 표현식으로 사용된다.

fun max_result(a: Int, b: Int) = if (a > b) a else b

 

for loop

다른 언어와 마찬가지로 for를 사용해서 반복문을 작성한다.

 

fun main(){
    val items = listOf("apple", "banana", "kiwi")
    for (item in items) {
        println(item)
    }
}

 

배열에서 in으로 각각의 item을 가져올 수 있다.

while loop

이것도 마찬가지로 while의 괄호 안에 있는 조건이 true일 동안 반복하게 된다.

fun main(){
    val items = listOf("apple", "banana", "kiwi")
    var index = 0
    while (index < items.size) {
        println("item at $index is ${items[index]}")
        index++
    }
}

 

when expression

C언어에서의 switch이다.

C언어와는 다르게 다양한 타입으로 분기할 수 있다.

 

또한 이것도 표현식이기 때문에 when을 사용하여 변수에 값을 할당 할 수 있다.

이 때는 else로 모두 해당하지 않는 경우를 꼭 지정해주어야 한다.

fun main(args: Array<String>) {
    val obj = "hi"
    
    val result = when (obj) {
        "hi" -> "hello"
        "hello" -> "hi"
        else -> ""
    }
}

 

Ranges

in 연산자를 사용하여 숫자가 범위 내에 있는지 확인한다.

fun main(){
    val x = 10
    val y = 9

    if(x in 1..y+1){
        println("fits in range")
    }
}

 

반복문에서도 이렇게 사용할 수 있다.

for (x in 1..5){
	print(x)
}

 

downTo와 step을 사용하여 증가인지 감소인지와 간격을 지정해 줄  수 있다.

fun main(){
    for (x in 1..10 step 2) {
        print(x)
    }
    println()
    for (x in 9 downTo 0 step 3) {
        print(x)
    }
}

 

 

Collections

collection을 반복하는 방법

for (item in items){
	println(item)
}

 

map을 사용하여 해당 collection을 반복 할 수도 있다.

    val fruits = listOf("banana", "avocado", "apple", "kiwifruit")
    fruits
    	//a로 시작하는 단어 찾기
        .filter { it.startsWith("a") }
        //정렬
        .sortedBy { it }
        //대문자로 변환
        .map { it.uppercase() }
        //각각을 출력
        .forEach { println(it) }

 

Nullable values and null checks

코틀린에서는 null을 허용할 때 명시를 해주어야 한다.

타입의 마지막에 ?를 붙여주면 nullable 타입이다.

fun main(){
    val a: Int? = null
    val b: Int = 1
}

 

Type checks and automatic casts

is는 인스턴스가 해당 타입이 맞는지 확인하는 연산자이다.

그리고 is로 검사가 끝나면 해당 타입으로 자동 캐스팅해준다.

fun getStringLength(obj: Any): Int? {
    if (obj is String) {
        // `obj` is automatically cast to `String` in this branch
        return obj.length
    }

    // `obj` is still of type `Any` outside of the type-checked branch
    return null
}

fun main() {
    fun printLength(obj: Any) {
        println("Getting the length of '$obj'. Result: ${getStringLength(obj) ?: "Error: The object is not a string"} ")
    }
    printLength("Incomprehensibilities")
    printLength(1000)
    printLength(listOf(Any()))
}

 

'백엔드 > 코틀린' 카테고리의 다른 글

Concepts:Control flow  (1) 2024.10.05
Idioms  (1) 2024.10.01
코틀린 시작  (0) 2024.09.28
728x90

거의 1년간 코틀린으로 스프링부트를 사용하고 있었지만, 사실 나의 코틀린은 자바에서 단지 널 안정성만 추가된 언어였다.

 

물론 널 안정성 만으로도 자바에서 코틀린으로 넘어 올 가치는 있지만, 그래도 더 나은 개발자가 되기 위해서는 코루틴까지 자유롭게 사용할 수 있어야 한다는 생각이 들었다.

 

코틀린은 아직 한국어 자료가 많이 없기 때문에 이 참에 공식문서를 참고하여 코틀린을 깊게 공부해보려 한다.

 

https://kotlinlang.org/docs/home.html

 

Kotlin Docs | Kotlin

 

kotlinlang.org

 

'백엔드 > 코틀린' 카테고리의 다른 글

Concepts:Control flow  (1) 2024.10.05
Idioms  (1) 2024.10.01
Basic syntax  (0) 2024.09.28
728x90

마지막으로 Java NIO non-blocking으로 만든 서버에서도 문제가 있었다.

accept 할 때마다 조건문으로 확인을 하고 Thread.sleep을 걸어주면, 그만큼 리소스의 낭비가 생기기 때문이다.

그리고 sleep 중간에 요청이 들어오면, sleep이 끝날 때까지 대기를 하기 때문에 그만큼 딜레이가 생긴다.

 

이런 이벤트들을 계속 확인하지 않고, 여러 이벤트를 추적할 수 있도록 변경해보자.

이번 내용은 네트워크 프로그래밍에서 배웠던 내용을 C에서 Java로 변경한 느낌일 것이다.

 

 

Selector 이론

SelectableChannel

 

selectableChannel에는 register 함수를 제공한다.

register는 Selector에 channel을 등록할 수 있다.

public abstract class SelectableChannel
    extends AbstractInterruptibleChannel
    implements Channel
{

    public abstract SelectableChannel configureBlocking(boolean block)
        throws IOException;

    public final SelectionKey register(Selector sel, int ops)
        throws ClosedChannelException
    {
        return register(sel, ops, null);
    }
}

 

 

Selector

 

여러 Channel의 이벤트를 등록하고 준비된 이벤트를 모아서 조회할 수 있도록 한다.

public abstract class Selector implements Closable{
    public abstract int select() throws IOException;
    public abstract Set<SelectionKey> selectedKEys();
}

 

 

Selector를 만들고, channel에 selector와 관심 있는 이벤트를 등록한다.

channel의 register를 이용하면 내부 함수에서 다시 selector의 register를 호출하기 때문에, channel에 등록하더라도 selector에서 이벤트를 받을 수 있다.

관심있는 이벤트가 ops인데, 무슨 이벤트인지에 대한 정보이다.

이벤트들은 다음과 같다.

  • OP_READ: Channel에 읽기 준비가 완료되었다.
  • OP_WRITE: Channel에 쓸 준비가 완료되었다.
  • OP_ACCEPT: serverSocketChannel에서 accept 할 준비가 완료되었다.
  • OP_CONNECT: socketChannel에서 connect 할 준비가 완료되었다.

 

이렇게 해서 Accept를 등록하려는 코드는 다음과 같다.

serverChannel.register(selector, SelectionKey.OP_ACCEPT);

 

 

등록이 끝나면 이제 Accept를 대기할 텐데, select를 사용하면 준비가 될 때까지 쓰레드 blocking이 된다.

 

while(true){
    selector.select();
    
    var selectedKEys = selector.selectedKeys().iterator();
    
    while(selectedKeys.hasNext()){
    	var key = selectedKeys.next();
        /////
        selectedKeys.remove();
    }
}

 

select에서 준비가 완료된 작업들이 발견된다면, 다음 line으로 이동한다.

준비가 완료된 작업을 가져올 때는 selectedKeys를 사용한다.

 

여기서 SelectionKey를 iterator로 가져오게 되는데, SelectionKey는 다음과 같다.

public abstract class SelectionKey {

    public abstract SelectableChannel channel();

    public abstract Selector selector();

    public abstract int interestOps();

    public final boolean isReadable() {
        return (readyOps() & OP_READ) != 0;
    }

    public final boolean isWritable() {
        return (readyOps() & OP_WRITE) != 0;
    }

    public final boolean isConnectable() {
        return (readyOps() & OP_CONNECT) != 0;
    }

    public final boolean isAcceptable() {
        return (readyOps() & OP_ACCEPT) != 0;
    }

}

 

 

SelectionKey는 channel과 selector, 발생한 이벤트의 정보를 가지고 있는 객체이다.

여기에 모든 정보가 있기 때문에, 이거만 있으면 추가적인 정보를 넘길 필요가 없을 것이다.

 

이렇게 accept를 알아보았고, read인 경우에는 해당 이벤트를 받아 동일하게 처리하면 될 것이다.

 

 

Selector 실습

이제 Selector를 사용하여 더 최적화를 해보도록 하자.

저번에 작성했던 NIO 서버를 변경할 것이다.

@Slf4j
public class SelectorServer {

    private static ExecutorService executorService = Executors.newFixedThreadPool(50);

    @SneakyThrows
    public static void main(String[] args) {
        log.info("start");
        try(ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            Selector selector = Selector.open()){

            serverSocketChannel.bind(new InetSocketAddress("localhost", 8080));
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            while(true){

                selector.select();

                Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();

                while(selectedKeys.hasNext()){
                    SelectionKey key = selectedKeys.next();
                    selectedKeys.remove();

                    if(key.isAcceptable()){
                        SocketChannel clientSocket = ((ServerSocketChannel)key.channel()).accept();
                        clientSocket.configureBlocking(false);
                        clientSocket.register(selector, SelectionKey.OP_READ);
                    }else if(key.isReadable()){
                        SocketChannel clientSocket = (SocketChannel) key.channel();

                        String responseBody = handleRequest(clientSocket);
                        sendResponse(clientSocket, responseBody);
                    }
                }

            }
        }
    }

    @SneakyThrows
    private static String handleRequest(SocketChannel clientSocket) {
        ByteBuffer requestByteBuffer = ByteBuffer.allocateDirect(1024);
        clientSocket.read(requestByteBuffer);

        requestByteBuffer.flip();
        String requestBody = StandardCharsets.UTF_8.decode(requestByteBuffer).toString();
        log.info("request: {}", requestBody);

        return requestBody;
    }

    @SneakyThrows
    public static void sendResponse(SocketChannel clientSocket, String requestBody){
        CompletableFuture.runAsync(() -> {

            try {
                Thread.sleep(10);
                String content = "received: " + requestBody;
                ByteBuffer responseByteBuffer = ByteBuffer.wrap(content.getBytes());
                clientSocket.write(responseByteBuffer);
                clientSocket.close();
            } catch (Exception e) {
            }
        }, executorService);

    }
}

 

여기서 Accept 이벤트와 Read 이벤트를 처리하고 있는 것을 볼 수 있다.

각각의 이벤트 키로 해당 이벤트를 넣어 처리를 하며, 다른 쓰레드에서 비동기적으로 마지막 메서드를 실행하는 것을 볼 수 있다.

 

 

'백엔드 > 리액티브 프로그래밍' 카테고리의 다른 글

Java AIO  (0) 2024.03.19
Java IO Server 서버를 NIO로 변경  (0) 2024.03.19
Java NIO  (0) 2024.03.18
JAVA IO  (0) 2024.03.18
Mutiny  (0) 2024.03.14
728x90

AsynchronousChannel을 지원한다고 한다.

callback 함수를 지정할 수 있고, future로 반환을 지원한다고 한다.

callback과 future를 사용하면 비동기적인 로직이 가능할 것 같다.

 

  • File - callback

우선 callback 함수를 이용해서 파일을 읽어보자.

@Slf4j
public class AsyncFileChannelReadCallbackExample {

    @SneakyThrows
    public static void main(String[] args) {
        log.info("start");
        var file = new File("path");
        var channel = AsynchronousFileChannel.open(file.toPath());
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);

        channel.read(byteBuffer, 0, null, new CompletionHandler<Integer, Object>() {
            @SneakyThrows
            @Override
            public void completed(Integer result, Object attachment) {
                byteBuffer.flip();
                var resultString = StandardCharsets.UTF_8.decode(byteBuffer);
                log.info("result: {}", resultString);
                channel.close();
            }

            @Override
            public void failed(Throwable exc, Object attachment) {

            }
        });

        while(channel.isOpen()){
            log.info("Reading...");
        }
        log.info("end");
    }
}

 

completed 함수로 callback을 지정해주어, 파일이 모두 읽히면 result: {}로 내용을 출력할 수 있도록 하였다.

파일이 열린동안 메인 쓰레드가 닫히지 않도록 channel.isOpen으로 대기를 걸어주었다.

 

이렇게 callback으로 파일의 내용이 출력된 것을 볼 수 있다.

 

  • File - future
@Slf4j
public class AsyncFileChannelReadFutureExample {
    
    @SneakyThrows
    public static void main(String[] args) {
        log.info("start");
        
        var file = new File("path");
        
        try(var channel = AsynchronousFileChannel.open(file.toPath())){
            var buffer = ByteBuffer.allocateDirect(1024);
            Future<Integer> channelRead = channel.read(buffer, 0);
            while(!channelRead.isDone()){
                log.info("Reading...");
            }
            
            buffer.flip();
            var result = StandardCharsets.UTF_8.decode(buffer);
            log.info("result: {}", result);
        }
        
        log.info("end");
    }
}

 

이제 Future로 받고, 해당 Future가 끝나기를 기다린 후 파일의 내용을 출력하게 되었다.

코드는 깔끔해졌지만, while에서 동기적으로 동작하게 된다.

 

  • socket - callback

이번엔 소켓이다.

accept하는 부분을 비동기적으로 변경하였다.

@Slf4j
public class AsyncServerSocketCallbackExample {

    @SneakyThrows
    public static void main(String[] args) {
        log.info("start");

        var serverSocketChannel = AsynchronousServerSocketChannel.open();
        var address = new InetSocketAddress("localhost", 8080);
        serverSocketChannel.bind(address);

        serverSocketChannel.accept(null, new CompletionHandler<>() {

            @Override
            public void completed(AsynchronousSocketChannel clientSocket, Object attachment) {
                log.info("accepted");
                var requestBuffer = ByteBuffer.allocateDirect(1024);

                clientSocket.read(requestBuffer, null, new CompletionHandler<>() {
                    @SneakyThrows
                    @Override
                    public void completed(Integer result, Object attachment) {
                        requestBuffer.flip();
                        var request = StandardCharsets.UTF_8.decode(requestBuffer);
                        log.info("request: {}", request);

                        var response = "I am server";
                        var responseBuffer = ByteBuffer.wrap(response.getBytes());
                        clientSocket.write(responseBuffer);
                        clientSocket.close();
                        log.info("end");
                    }
                    @Override
                    public void failed(Throwable exc, Object attachment) {

                    }
                });
            }
            @Override
            public void failed(Throwable exc, Object attachment) {

            }
        });
        Thread.sleep(1000000);
        log.info("end");

    }
}

 

Callback이 2개이다.

바깥은 accept하는 부분이고, 안 쪽은 read하는 부분의 callback이다.

 

 

  • socket - Future
@Slf4j
public class AsyncServerSocketFutureExample {

    @SneakyThrows
    public static void main(String[] args) {
        log.info("start");
        var serverSocketChannel = AsynchronousServerSocketChannel.open();
        var address = new InetSocketAddress("localhost", 8080);
        serverSocketChannel.bind(address);

        Future<AsynchronousSocketChannel> clientSocketFuture = serverSocketChannel.accept();
        while(!clientSocketFuture.isDone()){
            Thread.sleep(100);
            log.info("Wainting");
        }
        var clientSocket = clientSocketFuture.get();

        var requestBuffer = ByteBuffer.allocateDirect(1024);
        Future<Integer> channelRead = clientSocket.read(requestBuffer);
        while(!channelRead.isDone()){
            log.info("Reading...");
        }

        requestBuffer.flip();
        var request = StandardCharsets.UTF_8.decode(requestBuffer);
        log.info("request: {}", request);

        var response = "This is server.";
        var responseBuffer = ByteBuffer.wrap(response.getBytes());
        clientSocket.write(responseBuffer);
        clientSocket.close();
        log.info("end client");

    }
}

 

Future로 변경을 해보았다.

File 때와 마찬가지로 while에서 동기적으로 작동하게 되며, 보완이 필요하다고 생각된다.

'백엔드 > 리액티브 프로그래밍' 카테고리의 다른 글

Java Selector  (1) 2024.03.20
Java IO Server 서버를 NIO로 변경  (0) 2024.03.19
Java NIO  (0) 2024.03.18
JAVA IO  (0) 2024.03.18
Mutiny  (0) 2024.03.14
728x90

지금까지 배운 것을 바탕으로 서버를 만들고, 차례로 성능을 높여보자.

 

 

우선 간단한 기본 서버이다.

 

@Slf4j
public class JavaIOServer {

    @SneakyThrows
    public static void main(String[] args) {
        log.info("start");

        try(ServerSocket serverSocket = new ServerSocket()){
            serverSocket.bind(new InetSocketAddress("localhost", 8080));

            while(true){

                Socket clientSocket = serverSocket.accept();

                byte[] requestBytes = new byte[1024];
                InputStream inputStream = clientSocket.getInputStream();
                inputStream.read(requestBytes);

                log.info("request: {}", new String(requestBytes).trim());

                OutputStream outputStream = clientSocket.getOutputStream();
                String response = "I am Server";
                outputStream.write(response.getBytes());
                outputStream.flush();
            }

        }
    }
}

 

간단한 서버이다.

while(true)를 돌며 계속 응답을 받고, 받은 후에 클라이언트에게 I am Server라는 값을 준다.

 

클라이언트이다.

요청을 보내고, 응답을 받은 후 종료된다.

@Slf4j
public class JavaIOClient {

    public static void main(String[] args) {
        try(Socket socket = new Socket()){
            socket.connect(new InetSocketAddress("localhost", 8080));

            var outputStream = socket.getOutputStream();
            String requestBody = "I am Seungkyu client";
            outputStream.write(requestBody.getBytes());
            outputStream.flush();

            InputStream inputStream = socket.getInputStream();
            byte[] responseBytes = new byte[1024];
            inputStream.read(responseBytes);

            log.info("result: {}", new String(responseBytes).trim());


        }catch (IOException e){
            throw new RuntimeException(e);
        }
    }
}

이런 식으로 서버에 계속 요청이 오는 것을 볼 수 있다.

 

이제 서버를 NIO로 변경해보자.

@Slf4j
public class JavaNIOBlockingServer {

    @SneakyThrows
    public static void main(String[] args) {
        log.info("start");

        try(ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {

            serverSocketChannel.bind(new InetSocketAddress("localhost", 8080));

            while(true){

                SocketChannel socketChannel = serverSocketChannel.accept();

                ByteBuffer requestByteBuffer = ByteBuffer.allocateDirect(1024);
                socketChannel.read(requestByteBuffer);
                requestByteBuffer.flip();
                String requestBody = StandardCharsets.UTF_8.decode(requestByteBuffer).toString();
                log.info("request: {}", requestBody);

                ByteBuffer responseByteBuffer = ByteBuffer.wrap("I am Server".getBytes());
                socketChannel.write(responseByteBuffer);
                socketChannel.close();
            }

        }
    }
}

 

Channel을 이용하여 데이터를 읽고 쓰며, 버퍼를 사용하는 것을 볼 수 있다.

이렇게 속도를 높이겠지만, 아직도 accept에서 Blocking이 발생하고 있다.

 

그렇기에 이거를 Non-Blocking으로 바꿔보자

@Slf4j
public class JavaNIONonBlockingServer {

    @SneakyThrows
    public static void main(String[] args) {
        log.info("start");

        try(ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {

            serverSocketChannel.bind(new InetSocketAddress("localhost", 8080));
            serverSocketChannel.configureBlocking(false);

            while(true){

                SocketChannel socketChannel = serverSocketChannel.accept();

                if(socketChannel == null){
                    Thread.sleep(100);
                    continue;
                }

                ByteBuffer requestByteBuffer = ByteBuffer.allocateDirect(1024);
                while(socketChannel.read(requestByteBuffer) == 0){
                    log.info("reading");
                }
                socketChannel.read(requestByteBuffer);
                requestByteBuffer.flip();
                String requestBody = StandardCharsets.UTF_8.decode(requestByteBuffer).toString();
                log.info("request: {}", requestBody);

                ByteBuffer responseByteBuffer = ByteBuffer.wrap("I am Server".getBytes());
                socketChannel.write(responseByteBuffer);
                socketChannel.close();
            }
        }
    }
}

 

configureBlocking에 false를 주어 Non-Blocking으로 동작하게 했다.

하지만 socketChannel이 null 일 때 Thread.sleep을 하기에 좋은 방법은 아닌 것 같다.

또한 requestBuffer에도 데이터가 있는지를 계속 체크 해주어야 하기 때문에, 이 부분도 수정이 필요해보인다.

 

이거를 1000개의 요청으로 얼마나 걸리는지 테스트를 해보도록 하자.

테스트 용도의 코드는 다음과 같다.

@Slf4j
public class JavaIOMultiClient {
    private static ExecutorService executorService = Executors.newFixedThreadPool(50);

    @SneakyThrows
    public static void main(String[] args) {
        log.info("start main");

        List<CompletableFuture<Void>> futures = new ArrayList<>();
        long start = System.currentTimeMillis();

        for (int i = 0; i < 1000; i++) {
            var future = CompletableFuture.runAsync(() -> {
                try (Socket socket = new Socket()) {
                    socket.connect(new InetSocketAddress("localhost", 8080));

                    OutputStream out = socket.getOutputStream();
                    String requestBody = "This is client";
                    out.write(requestBody.getBytes());
                    out.flush();

                    InputStream in = socket.getInputStream();
                    byte[] responseBytes = new byte[1024];
                    in.read(responseBytes);
                    log.info("result: {}", new String(responseBytes).trim());
                } catch (Exception e) {}
            }, executorService);

            futures.add(future);
        }

        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
        executorService.shutdown();
        log.info("end main");
        long end = System.currentTimeMillis();
        log.info("duration: {}", (end - start) / 1000.0);
    }
}

 

50개의 쓰레드로 1000개의 요청을 한다.

 

이렇게 실행해서 시간을 측정해보니, 약 12초가 나왔다.

 

여기서 코드를 수정해서 속도를 높일 수는 없을까?

 

서버에서 처리하는 코드를 CompletableFuture를 이용해 별개의 쓰레드에서 처리하도록 했다.

@Slf4j
public class JavaNIONonBlockingServer {
    @SneakyThrows
    public static void main(String[] args) {
        log.info("start main");
        try (ServerSocketChannel serverSocket = ServerSocketChannel.open()) {
            serverSocket.bind(new InetSocketAddress("localhost", 8080));
            serverSocket.configureBlocking(false);

            while (true) {
                SocketChannel clientSocket = serverSocket.accept();
                if (clientSocket == null) {
                    Thread.sleep(100);
                    continue;
                }

                CompletableFuture.runAsync(() -> {
                    handleRequest(clientSocket);
                });
            }
        }
    }

    @SneakyThrows
    private static void handleRequest(SocketChannel clientSocket) {
        ByteBuffer requestByteBuffer = ByteBuffer.allocateDirect(1024);
        while (clientSocket.read(requestByteBuffer) == 0) {
            log.info("Reading...");
        }
        requestByteBuffer.flip();
        String requestBody = StandardCharsets.UTF_8.decode(requestByteBuffer).toString();
        log.info("request: {}", requestBody);

        Thread.sleep(10);

        ByteBuffer responeByteBuffer = ByteBuffer.wrap("This is server".getBytes());
        clientSocket.write(responeByteBuffer);
        clientSocket.close();
    }
}

 

이렇게 서버를 실행해보니 시간이 많이 준 것을 볼 수 있었다.

 

'백엔드 > 리액티브 프로그래밍' 카테고리의 다른 글

Java Selector  (1) 2024.03.20
Java AIO  (0) 2024.03.19
Java NIO  (0) 2024.03.18
JAVA IO  (0) 2024.03.18
Mutiny  (0) 2024.03.14
728x90

Java에서 NIO에 대해 알아보자.

 

java New Input/Output의 줄인말이며, 파일과 네트워크에 데이터를 읽고 쓸 수 있는 API를 제공한다.

buffer기반이며, non-blocking을 지원한다.

 

java IO는 InputStream, OutputStream을 사용해서 방향이 정해져있었다면, NIO는 Channel을 이용해 읽고 쓰기가 한번에 가능하다.

또한 NIO는 아예 시작부터 buffer의 단위로 데이터를 주고 받는다.

 

Channel, Buffer

데이터를 읽을 때는 Channel read()를 사용하여 버퍼에 데이터를 저장한다.

데이터를 쓸 때는 Channel write()를 사용하여 버퍼에 데이터를 쓴다.

 

이 버퍼에서도 ByteBuffer부터 CharBuffer까지 다양한 타입의 버퍼가 존재한다.

 

Buffer의 위치

버퍼의 현재 커서를 나타내는 다양한 속성이 있다.

capacity: Buffer의 크기이다. Buffer 생성시에 결정된다.

position: Buffer의 현재 위치이다. 버퍼에서 데이터를 읽거나 쓸 때 시작하는 위치이며, 1Byte가 추가될 때마다 1씩 증가한다.

limit: Buffer에서 데이터를 읽거나 쓸 수 있는 마지막 위치이다. limit 이후로 데이터를 읽거나 쓰기 불가능하다.

mark: 현재 position의 위츠를 지정한다.

 

Buffer의 위치 메서드

flip: Buffer의 limit 위치를 현재 position으로 이동시키고, position을 0으로 리셋한다.

rewind: Buffer의 position 위치를 0으로 리셋. limit은 유지한다.

clear: Buffer의 limit 위치를 capacity 위치로 이동시키고, position을 0으로 리셋한다.

 

Buffer

Java IO는 JVM의 버퍼에만 접근이 가능하고, 시스템 자체에는 접근이 불가능했다.

하지만 Java NIO는 시스템 버퍼에도 접근이 가능하다.

NIO의 버퍼들이다.

DirectByteBuffer HeapByteBuffer
시스템 자체 메모리에 저장 JVM Heap 메모리에 저장
자체 메모리에서 JVM으로 복사를 하지 않기 때문에 속도가 빠르다. JVM으로 복사가 일어나므로 데이터를 읽고 쓰는 속도가 느리다.
allocate, deallocate가 느리다. gc에서 관리하기에 allocate, deallocate가 빠르다.

 

DirectByteBuffer는 allocateDirect() 함수로 생성이 가능하고, HeapByteBuffer는 allocate(), wrap() 함수로 생성 가능하다.

isDirect인지 DirectBuffer인지 아닌지 확인 가능하다.

 

FileChannel - Read

@Slf4j
public class FileChannelReadExample {

    public static void main(String[] args) throws IOException {
        log.info("start");

        var file = new File("path");

        try(var fileChannel = FileChannel.open(file.toPath())){
            var byteBuffer = ByteBuffer.allocateDirect(1024);
            fileChannel.read(byteBuffer);
            byteBuffer.flip();

            var result = StandardCharsets.UTF_8.decode(byteBuffer);
            log.info("result: {}", result);
        }
        log.info("end");
    }
}

 

allocateDirect로 커널을 사용하여 파일을 읽어온다.

NIO를 사용하기 때문에 FileChannel에서 읽고 쓰기가 가능하다.

 

아래는 ByteBuffer.wrap()으로 JVM버퍼를 사용하는 예제이다.

@Slf4j
public class FileChannelWriteExample {

    public static void main(String[] args) throws IOException {
        var file = new File("path");

        var mode = StandardOpenOption.WRITE;
        try(var fileChannel = FileChannel.open(file.toPath(), mode)){
            var buffer = ByteBuffer.wrap("Hi Seungkyu~~".getBytes());
            var result = fileChannel.write(buffer);
            log.info("result: {}", result);
        }
    }
}

 

SocketChannel

이제 배운것들로 소켓 서버와 클라이언트를 만들어보자.

 

우선 서버이다.

@Slf4j
public class ServerSocketChannelExample {

    public static void main(String[] args) throws IOException {

        log.info("start");

        try(var serverChannel = ServerSocketChannel.open()){
            var address = new InetSocketAddress("localhost", 8080);
            serverChannel.bind(address);

            try(var clientSocket = serverChannel.accept()){
                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
                clientSocket.read(byteBuffer);
                byteBuffer.flip();

                var request = new String(byteBuffer.array()).trim();
                log.info("request: {}", request);

                var response = "I am Server";
                var responseBuffer = ByteBuffer.wrap(response.getBytes());
                clientSocket.write(responseBuffer);
                responseBuffer.flip();
            }

        }

        log.info("end");
    }
}

 

Direct 버퍼를 만들어서 읽어온다.

Socket에서도 read와 flip을 사용해서 읽어노는 것을 볼 수 있다.

 

다음은 클라이언트이다.

@Slf4j
public class ClientSocketChannelExample {

    public static void main(String[] args) throws IOException {
        log.info("start");

        try(var socketChannel = SocketChannel.open()){
            var address = new InetSocketAddress("localhost", 8080);
            var connected = socketChannel.connect(address);
            log.info("connected: {}", connected);

            String request = "I am Client";
            ByteBuffer requestBuffer = ByteBuffer.wrap(request.getBytes());
            socketChannel.write(requestBuffer);
            requestBuffer.clear();
            
            ByteBuffer responseBuffer = ByteBuffer.allocateDirect(1024);
            while (socketChannel.read(responseBuffer) > 0){
                responseBuffer.flip();
                log.info("response: {}", StandardCharsets.UTF_8.decode(responseBuffer));
                responseBuffer.clear();
            }
        }
        log.info("end");
    }
}

 

 

'백엔드 > 리액티브 프로그래밍' 카테고리의 다른 글

Java AIO  (0) 2024.03.19
Java IO Server 서버를 NIO로 변경  (0) 2024.03.19
JAVA IO  (0) 2024.03.18
Mutiny  (0) 2024.03.14
RxJava Reactor  (2) 2024.03.14

+ Recent posts