1. 디폴트 값으로 대처 

interface Transfer {
    fun moveTo(id : String)
}

interface Asset{
    val id: String
    val name: String
    val data: String
}

class Token(
    override val id: String,
    override val name: String,
    override val data: String) : Asset,Transfer{
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }

}
class NFT(
    override val id: String,
    override val name: String,
    override val data: String) : Asset,Transfer {
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }

}

class Account(val id: String, val name: String?): Transfer {
    val assets = mutableListOf<Asset>()
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }
}

먼저 이런 코드가 있다고 가정 해 본다.

@Test
fun setUp() {
    val account = Account("1", null)
    val name = account.name?:"default_name"
    Assertions.assertEquals("default_name", name )
}


해당 Account의 name을 가져왔을 때 만약 null이라면 "default_name"을 넣어 줄 수 있다.
많은 경우 이렇게 처리 할 수 있을 것이다. 특히 설정값을 처리할 때 많이 사용되는데 특별히 설정된 값이 없을 경우 디폴트 값을 넣어주는 경우가 그럴 것이며 굉장히 처리하기 쉬운 경우이다.

2. nullable  

그리고 위에 보듯이 특정 값이 없을 경우 우리는 null을 리턴 할 수 있다.  비교적 일상적으로 벌어지는 문제이다.
즉 어떤 컬렉션에 특정 값이 없을 경우엔 null을 리턴하는게 정상이며 이후에 어떻게 처리 할 지는 사용자의 몫이다.

class Bank{
    val accounts = mutableMapOf<String, Account>()
    fun createAccount(account: Account){
        accounts.put(account.id, account)
    }
    
    fun getAccount(id: String): Account? {
        return accounts.get(id) 
    }
}

위에서 getAccount를 호출할 경우 해당 정보가 없을 수 도 있는데, 이것은 예외라고 보긴 어려울 것이다. (기술적으로 예외로 처리할 수도 있지만, 나는 그렇게 처리하지 않는다. 이유가 너무 명확하기 때문이다.) 

 @Test
 fun getAccount() {
   val bank = Bank()
   bank.createAccount(Account("1", "hama"))
   val result = bank.getAccount("1")?.name

  Assertions.assertEquals("hama", result)
}

만약 "2"를 넣어서  null을 리턴한다면 아무것도 처리하지 않으며, "1"을 넣는다면 name을 출력 할 것이다.  
? 는 null이 아닐 경우만 처리하라는 의미로, 위의 코드에서 ? 없이 name을 코딩 할 순 없다.
근데 위의 코드는 resultnull이 담길 수 있으므로 위험 할 수 있다. ?: 엘비스 연산자로 디폴트값을 넣거나 예외를 전파하거나 무엇인가 해야한다. NPE를 막아주는게 아니라 주의를 줄 뿐이다.

 val bank = Bank()
    bank.createAccount(Account("1", "hama"))
    bank.getAccount("1")?.let {
        val result = it.name
        println(result)
    }

적어도 let을 사용해 이렇게 하면 null을 전파하진 않는다.

3. Execption으로 전파 

null로 처리하게 된 다면, 호출당한 함수내부에서 어떤 이유로 null이 리턴 됬는지 궁금 할 수가 있다.일반적으로 예상되는 경우라면 호출한 측에서 이유를 작성 할 수도 있지만,  즉 예외가 왜 일어 났는지 잘 모를 수 있는 경우라면 호출당한 함수에서 이유를 알려주는 것이 일반적이다. 

class Bank{
    val accounts = mutableMapOf<String, Account>()
    fun createAccount(account: Account){
        accounts.put(account.id, account)
    }
    fun getAccount(id: String): Account? {
        return accounts.get(id)
    }

    fun dipositToken(accountId: String, token: Token){
        accounts.get(accountId)?.dipositToken(token)
    }

만약 위의 dipositToken을 호출했는데 accountId에 해당하는 계좌가 없으면 아무일도 안 일어날 것이다. 뭐 이렇게 되도 상관없을 수도 있겠다. 일관성이 깨지는 것 같은 큰 문제는 일어나지 않았으니깐, 대신 이 함수를 호출한 측에서는 diposit이 잘 됬는지 아닌지 알수가 없다. 잘 됬다고 생각하고 다음 단계를 밟을 수도 있는데 그렇게 되면 문제가 된다. 따라서 이 경우에는 리턴 값으로 문제 상황을 알리거나 예외를 전파해야하는데, 예외로 전파하는것을 추천한다. 나는 command 행동에서 문제가 생기면 예외를 던지고, query행동에서문제가 생기면 null (일반적인 상황일때) 혹은 예외(진짜 예외상황일때)를 전파하는 컨벤션을 갖는다.

@Throws(NoAccountException::class)
fun createToken(accountId: String, token: Token){
    accounts.get(accountId)?.createToken(token) ?: throw NoAccountException("there is no $accountId in our bank")
}

이렇게 예외를 던저주고 함수에 명시해 둔다. (호출하는 클라이언트에 대한 배려) 

@Test
fun dipositToken() {
    Assertions.assertThrows(NoAccountException::class.java, {
        bank.createToken("2", exception.Token("token_1", "opusm", 1000))
    })
}

Junit5를 통해서 테스트를 성공한다. 

 

4. Result 

1.5 버전 부터 Result<T> 을 사용 할 수 있다. 함수안에서 일어나는 일의 결과를 성공,실패로 단순히 알 수있는 Boolean과 다른점은 성공 했을 경우의 타입과 실패했을 경우의 예외 타입 및 메세지를 포함 시켜서 보다 풍부한 결과를 알고 싶을 때 사용한다. Result<T>구현은 유틸리티성 메소드가 굉장히 많이 포함되어 있다. 5번의 runCatching에서 더 설명한다.

import java.lang.IllegalArgumentException


data class Passport (val name: String, val age: Int)
data class EntranceData(val purposeCode: Int, val jobCode : Int, val passport: Passport)

data class PassportException(override val message: String?) : IllegalArgumentException(message)
data class JobException(override val message: String?) : IllegalArgumentException(message)


enum class Nation(val exportValid : List<ExportCheck>, val importValid : List<ImportCheck>) {
    KOREA(listOf(ExportCheck.Passport, ExportCheck.Job),listOf(ImportCheck.Passport) ),
    JAPAN(listOf(ExportCheck.Passport),listOf(ImportCheck.Passport)),
    USA(listOf(ExportCheck.Passport),listOf(ImportCheck.Passport));

    enum class ExportCheck {
        Passport {
            override fun check(data : EntranceData) : Result<String> {
                return if(data.passport.age > 10){
                    Result.success("Export passport ok")
                }
                else {
                    Result.failure(PassportException("reason why ~~"))
                }
            }
        },
        Job {
            override fun check(data : EntranceData) : Result<String>{
                return if(data.jobCode == 1){
                    Result.success("Export Job ok")
                }
                else {
                    Result.failure(JobException("reason why ~~"))
                }
            }
        };

        abstract fun check(data : EntranceData): Result<String>
    }

    enum class ImportCheck {
        Passport {
            override fun check(data : EntranceData) : Result<String>{
                return if(data.passport.age > 10){
                    Result.success("Export passport ok")
                }
                else {
                    Result.failure(PassportException("reason why ~~"))
                }
            }
        },
        Job {
            override fun check(data : EntranceData) : Result<String>{
                return if(data.jobCode > 1){
                    Result.success("Export Job ok")
                }
                else {
                    Result.failure(JobException("reason why ~~"))
                }
            }
        };

        abstract fun check(data : EntranceData): Result<String>
    }

    companion object {
        fun getNumberOfNations() = values().size
    }
}

fun tradeTest(entranceData: EntranceData, nation: Nation): List<Result<String>> {
  return nation.exportValid.map{
       it.check)
   }
}

fun main(args: Array<String>) {

    val korea = Nation.KOREA;
    val data = EntranceData(1,1, Passport("john",3))

    val results = tradeTest(data,korea)

    results.forEach { result ->
        result.onSuccess {
            println(it)
        }
        .onFailure {
            println(it.message)
        }
    }

}

5. runCatching 

runCatching은 Result<T>를 사용하는데 도움이 될 수 있다.

account)

fun withrawToken(id: String, balance: Int): Int {
    val token = assets.find { it -> it.id == id  } as Token
    require(token.balance > balance )
    token.balance = token.balance - balance
    return balance
}

bank)

fun withrawToken(accountId: String, tokenId: String, balance: Int): Int{
    val result = kotlin.runCatching {
        accounts.get(accountId)?.withrawToken(tokenId, balance)
    }

    return result.getOrDefault(0)!!
}

test)

@Test
fun withrawToken() {
    bank.createToken("1", exception.Token("token_1", "opusm", 1000))
    val result = bank.withrawToken("1", "token_1", 5000)
    Assertions.assertEquals(0, result)
}


아래와 같이 runCatching 문은 좀 더 다양한 방식으로 처리 될 수 있다. 

fun withrawToken(accountId: String, tokenId: String, balance: Int):String{
    return  kotlin.runCatching { accounts.get(accountId)?.withrawToken(tokenId, balance)}
    .onFailure { println(" withraw token failure") } // 특정 예외를 구분 할 수도 있다.
    .onSuccess { println(" withraw token success") }
    .mapCatching { it -> it.toString() }
    .getOrDefault("0") // getOrNull, getOrThrows
}

/////// 아래와 같이 특정 예외별로 구분해서 처리 할 수도 있다.
getOrElse {
    when(it) {
        is LowBalanceException -> "LowBalanceException"
        is IllegalStateException -> "IllegalStateException"
        is NullPointerException -> "null"
        else -> throw it
    }
}

 

전체 코드)

package exception

import exception.bankexception.LowBalanceException
import exception.bankexception.NoAccountException
import kotlin.jvm.Throws


interface Transfer {
    fun moveTo(id : String)
}

interface Asset{
    val id: String
    val name: String
}

enum class AssetKind{
    Token,
    NFT
}

data class Token(
    override val id: String,
    override val name: String,
    var balance: Int) : Asset,Transfer{
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }

}
data class NFT(
    override val id: String,
    override val name: String) : Asset,Transfer {
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }

}

data class Account(val id: String, val name: String?): Transfer {
    val assets = mutableListOf<Asset>()
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }

    fun createToken(token: Token) {
        assets.add(token)
    }

    fun withrawToken(id: String, balance: Int): Int {
        val token = assets.find { it -> it.id == id  } as Token
        require(token.balance > balance )
        token.balance = token.balance - balance
        return balance
    }
}

class Bank{
    val accounts = mutableMapOf<String, Account>()
    fun createAccount(account: Account){
        accounts.put(account.id, account)
    }
    fun getAccount(id: String): Account? {
        return accounts.get(id)
    }

    @Throws(NoAccountException::class)
    fun createToken(accountId: String, token: Token){
        accounts.get(accountId)?.createToken(token) ?: throw NoAccountException("there is no $accountId in our bank")
    }

    fun withrawToken(accountId: String, tokenId: String, balance: Int):Int{
        return  kotlin.runCatching { accounts.get(accountId)?.withrawToken(tokenId, balance)}
        .onFailure { println(" withraw token failure") }
        .onSuccess { println(" withraw token success") }
        .getOrDefault(0)!!
    }
}

 




자바와 체크 예외 

- 어떤 예외에 대해 명시적으로 메서드 시그니처에 포함시켜서 좀 더 처리를 강제한다.
-
체크드예외는 클래스 못찾는거, 파일 못찾는거 IO예외, SQL예외 같은거...즉 언제든지 자신의 탓이 아닌데도 문제가 발생할 수 있고, 메모리가 없는것처럼 아예손놓고 있지 않아도 될 만한것
- 오류Error는 메모리나 HDD가 없는 것 처럼 멀 할 수 없는 경우를 말한다. 그냥 끝내는게 최선
-
런타임예외는 Error긴 한데 좀 다른 유행이다. 근데 런타임예외는 체크드예외인 Exception 아래에 있다.의도는 개발자의 실수에 의한 예외를 말한다. 널문제, 잘못된인자나 프로그램 상태 같은거. 즉 개발자의 탓이고 멍청하게 프로그램을 사용했으니 그냥 연산 중단 되버려지만 Error처럼 무조건 끝내지 말고복구를 시도 해 볼 순 있다 정도
-
문자열에서 데이터추출시 URL은 체크예외인 MalformedURLExcetion 던지지만
  Integer.parseInt
는 언체크예외인 NumberFormatException 던짐. ???
-
자바 8 람다나 함수형 인터페이스는 체크예외를 전파할 수 없게 됨. 개발자들 체크예외 사용 포기
-
그냥 모두 RuntimeException으로 다루자. (코틀린)

자바의 Optional

1. 자바 8이전에는 예외를 던지거나 null을 반환하는 것
2. 
두 방법 모두 허점이 있는데 예외는 진짜 예외적인 상황(?)에서 사용해야 하며, 예외를 생성할 때 스택트레이싱 전체를 캡쳐하므로 비용도 많이 든다. null을 반환하면 이런 문제는 없지만 그것을 처리 안하면 원래 오류가 아니라 그냥 어디선가 갑자기 NullPointerException이 튀어 나올 수 있다.
3. Optional
은 예외를 반드시 사용할 필요 없는 곳에 null대신 사용하기 좋다.
4. illegalArgumentException
을 던지는 곳에 Optional을 던지는게 더 나은 경우가 많다. Optional은 검사를 강제하므로 검사예외와도 비슷한데 더 간단하고 함수형 처럼 체이닝 하기도 좋다.
6. 
다만 컬렉션,스트림,배열,옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안된다.
7. 
물론 그냥 null을 던지는게 성능상은 더 유리하다.


소프트웨어 엔지니어링에서 풀의 종류는 다양한데요.

쓰레드풀,메모리풀,캐쉬풀,커넥션풀,객체풀 등등이 있습니다. "풀"어서 말하면 미리 만들어두고 돌려막기로 사용하자 라고 볼 수 있는데요. 미리 만들어 두는 방식 / 쓰레드가 태스크를 처리하는 방식/ 동기,비동기에 따라서 다양한 풀의 구현체들이 있을 수 있습니다.  이 글에서는 Kotlin으로 객체풀을 만드는 간단한 예제를 보여 줍니다. 

1. 리스트를 이용한 고정크기 동기 객체풀 

import java.lang.IllegalStateException
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

interface ObjectPool<T> {
  fun take(): T
  fun release(obj: T)
  fun poll(waitTime: Long, waitUnit: TimeUnit): T
  fun newInstance(): T

  fun <R> use(function: (T) -> R): R {
      take().apply {
        return try {
          function(this)
        } finally {
          release(this)
        }
      }
    }

  fun <R> useAsync(function: (T) -> CompletableFuture<R>): CompletableFuture<R> {
    return CompletableFuture.supplyAsync{
      take()
    }.thenCompose {
      try{
        function(it)
      }
      finally {
        release(it)
      }
    }
   }
}

interface ObjectFactory<T> {
  fun newInstance(): T
}

class FixedObjectPool<T>(factory: ObjectFactory<T>, size: Int, val waitSize: Int = 0 ): ObjectPool<T>{
  
  val factory : ObjectFactory<T>
  val lock = ReentrantLock()
  val condition = lock.newCondition()
  val pool = mutableListOf<T>()
  var pause = false
  
  
  init {
    this.factory = factory
    for (i in 1 .. size) {
      pool.add(newInstance())
    }
  }
  
  fun size(): Int {
    return pool.size
  }
  
  override fun take(): T {
    lock.withLock {
      while (pool.isEmpty()){
        condition.await()
      }
      val obj = pool.first()
      pool.removeFirst()
      return obj
    }
  }
  
  override fun poll(waitTime: Long, waitUnit: TimeUnit): T {
    lock.withLock {
      while (pool.isEmpty()){
        if (!condition.await(waitTime, waitUnit)) throw IllegalStateException("fail to get object")
      }
      val obj = pool.first()
      pool.removeFirst()
      return obj
    }
  }
  
  override fun release(obj: T) {
    lock.withLock {
      if (pool.isEmpty()) {
        pause = true;
      }
      
      pool.add(obj)
      
      if (pause && pool.size > waitSize) {
        condition.signalAll()
        pause = false;
      }
    }
  }
  
  override fun newInstance(): T {
    return factory.newInstance()
  }
}

class EthereumWallet{
  /*
   something
  */
}

class EthereumWalletObjectFactory : ObjectFactory<EthereumWallet>{
  override fun newInstance(): EthereumWallet {
    return EthereumWallet()
  }
}

fun main() {
  println("object pool start")
  
  val pool = FixedObjectPool(EthereumWalletObjectFactory(), 4)
  println(pool.size())
  
  val obj1 = pool.take()
  println(obj1)
  val obj2 = pool.take()
  println(obj2)
  val obj3 = pool.take()
  println(obj3)
  
  val result = pool.use { it -> it.print() }
  println("use")
  println(result)
  
  val result2 = pool.useAsync { it -> CompletableFuture.supplyAsync{it.print()} }
  println("useAsync")
  println(result2.get())
  
  val obj5 = runCatching {
    pool.poll(1, TimeUnit.MILLISECONDS)
  }.onFailure {
    println("failed")
  }
  
  println("object pool end")
  
}

- 고정된 숫자를 가진 객체풀이다. 
- mutable list를 통해 객체가 관리되며, 쓰레드안전을 위해서 lock 과 condition을 직접 구현하였다. 
- use를 이용하여 take와 release를 신경 안 쓸수도 있다. 
- 눈여겨 봐야 할 부분은 2개가 있는데 첫째로 take의 while 문인데, 여러개의 쓰레드가 동시에 signal을 받을때, 뒤늦게 깨어난 쓰레드가 풀의 갯수를 확인해야 문제가 생기지 않는다. 
- 두번째로는 waitSize가 있는데, 이는 빈번한 wait/signal을 사용하지 않기 위해, 어느정도 일정 갯수의 객체가 풀안에 들어 왔을때만 동작하게 함으로써 성능을 개선 시킬 수 있다. 

2. DisruptorBlockingQueue 를 이용한 단순한 객체풀 

import com.conversantmedia.util.concurrent.DisruptorBlockingQueue
import java.util.concurrent.TimeUnit

interface ObjectPool<T> {
  fun take(): T
  fun release(obj: T)
  fun poll(waitTime: Long, waitUnit: TimeUnit): T
  fun newInstance(): T
}

interface ObjectFactory<T> {
  fun newInstance(): T
}

class FixedObjectPool<T>(factory: ObjectFactory<T>, size: Int): ObjectPool<T>{
  
  private val pool: DisruptorBlockingQueue<T>
  private val factory: ObjectFactory<T>
  
  init {
    this.factory = factory
    this.pool = DisruptorBlockingQueue<T>(size).apply {
      for (i in (1 .. size)) {
        add(newInstance())
      }
    }
  }
  
  fun size(): Int {
    return pool.size
  }
  
  override fun take(): T {
    return pool.take()
  }
  
  override fun release(obj: T) {
    pool.add(obj)
  }
  
  override fun newInstance(): T {
    return factory.newInstance()
  }
  
  override fun poll(waitTime: Long, waitUnit: TimeUnit): T {
    return pool.poll(waitTime,waitUnit)
  }
}


class EthereumWallet{
  /*
    something
  */
}

class EthereumWalletObjectFactory : ObjectFactory<EthereumWallet>{
  override fun newInstance(): EthereumWallet {
    return EthereumWallet()
  }
}


fun main() {
  val pool = FixedObjectPool(EthereumWalletObjectFactory(), 4)
  println(pool.size())
}

- LinkedBlockingQueue를 사용 할 수도 있지만... 
DisruptorBlockingQueue는 초고속 Concurrent 라이브러리를 이용한 것으로써 아래 링크를 참고한다.
https://github.com/conversant/disruptor


Coroutine

// THREAD 방식 

fun main() {
  
  val startTime = System.currentTimeMillis()
  val counter = AtomicInteger(0)
  val numberOfCoroutines = 100_00
  val jobs = List(numberOfCoroutines) {
    thread(start = true) {
      Thread.sleep(100L)
      counter.incrementAndGet()
    }
  }
  jobs.forEach { it.join() }

  val timeElaspsed = System.currentTimeMillis() - startTime
  println(timeElaspsed)
}
// Coroutine 방식

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.concurrent.atomic.AtomicInteger
fun main() {
  val startTime = System.currentTimeMillis()
  runBlocking<Unit> {
    val counter = AtomicInteger(0)
    val numberOfCoroutines = 100_00

    val jobs = List(numberOfCoroutines) {
      launch {
        delay(100L)
        counter.incrementAndGet()
      }
    }
    jobs.forEach { it.join() }
  }

  println(System.currentTimeMillis() - startTime)
}

Thread를 통해서 작업을 하면 2130밀리초가 걸리는 작업을 Coroutine을 통해서하면 209 밀리초 밖에 걸리지 않는다. Coroutine은 경량쓰레드를 제공하기 때문에 가능한데, 이 말은 코루틴 수만개가 단 몇개의 쓰레드를 다시 나누어 사용 한다는 의미이다. 즉 코루틴은 놀고 있는 쓰레드에 부착되어 작동 하므로 시작되는 쓰레드와 종료 시점의 쓰레드가 달라질 수 있다. Go언어에서 제공되는 경량쓰레드(goroutine)와는 달리 내부적으로 자바 쓰레드풀을 사용하기 때문에, 진정한 경량쓰레드를 쓰냐에 관련된 말이 있지만, 어차피 Go언어도 자신만의 추상층을 가지고 있기 때문에 별 의미는 없어 보인다.

스택오버플로우에서 설명된 go goroutine vs kotlin Coroutine차이

주요 차이점에 대한 요약을 정리하자면 아래와 같다.

1. 코틀린 코루틴은 Go 고루틴보다 간단한 인스턴스당 더 적은 메모리를 필요로 한다. 코틀린에 있는 단순한 코루틴은 힙 메모리의 수십 바이트만 차지하고, 고 고루틴은 스택 공간의 4KiB로 시작한다. 말 그대로 수백만 개의 코루틴을 가질 계획이라면 코틀린의 코루틴은 Go 에 비해 우위를 점할 수 있습니다. 또한 코틀린 코루틴은 생성기 및 게으른 시퀀스와 같은 매우 짧고 작은 작업에 더 적합하게 만든다.

2. 코틀린 코루틴은 임의의 스택 깊이까지 갈 수 있지만, 함수를 일시 중단할 때마다 힙에 개체를 할당한다. 코틀린 코루틴의 호출 스택은 현재 힙 객체의 링크된 목록으로 구현되어 있다. 반대로 Go의 고루틴은 선형 스택 공간을 사용한다. 이것은 Go에서 딥 스택의 서스펜션을 더 효율적으로 만든다. 따라서, 당신이 쓰고 있는 코드가 매우 깊게 구성되어 있다면, 고루틴이 당신에게 더 효율적이라는 것을 알게 될 것이다.

3. 효율적인 비동기 IO는 매우 다차원적인 설계 문제이다. 한 종류의 애플리케이션에 효율적인 접근 방식은 다른 종류의 애플리케이션에 최상의 성능을 제공하지 못할 수 있다. 코틀린 코루틴의 모든 IO 연산은 코틀린이나 자바 언어로 작성된 라이브러리에 의해 구현된다. 코틀린 코드에서 사용할 수 있는 IO 라이브러리는 매우 다양하다. In Go에서 비동기 IO는 일반 Go 코드에서 사용할 수 없는 기본 요소를 사용하여 Go 런타임에 구현된다. IO 작업 구현에 대한 Go 접근 방식이 애플리케이션에 적합하다면 Go 런타임과의 긴밀한 통합이 이점을 제공할 수 있다. 한편, Kotlin에서는 라이브러리를 찾거나 애플리케이션에 가장 적합한 방식으로 비동기 IO를 구현하는 라이브러리를 직접 작성할 수 있다.

4. Go runtime은 물리적 OS 스레드에서 실행 일정을 완전히 제어한다. 이 접근 방식의 장점은 모든 것을 생각할 필요가 없다는 것이다. 코틀린 코루틴을 사용하면 코루틴의 실행 환경을 세밀하게 제어할 수 있다. 이는 오류가 발생하기 쉽다(예: 단순히 너무 많은 다른 스레드 풀을 만들고 이들 사이의 컨텍스트 전환에 CPU 시간을 낭비할 수 있다). 그러나 응용 프로그램에 대한 스레드 할당 및 컨텍스트 스위치를 미세 조정할 수 있다. 예를 들어 Kotlin에서는 단일 OS 스레드(또는 스레드 풀)에서 전체 응용 프로그램 또는 코드의 하위 집합을 실행하는 것이 쉬워서 적절한 코드를 작성하는 것만으로 OS 스레드 간에 컨텍스트를 완전히 전환하는 것을 피할 수 있다.

ps)
코틀린의 코루틴은 Golang의 코루틴과는 다른 방식으로 구현되기 때문에 어떤 것이 더 '빠른' 지는 푸는 문제와 작성하는 코드의 종류에 따라 달라진다.  즉 여러분이 당면한 문제에 대해 어떤 것이 더 잘 작동할지 미리 말하는 것은 매우 어렵다. 결국 특정 워크로드에 대한 벤치마크를 실행하여 이를 파악해야 한다. 

 

(* python의 코루틴에서 주로 사용하는 generator / send / yield 같은 기능은 없어 보인다. 다만 아래서 설명할 channel로 비슷하게 만들 순 있다.)

fun main() {
  runBlocking<Unit> {

    val time = measureTimeMillis {
      // given
      val one = async {
        delay(1000L)
      }
      val two = async {
        delay(2000L)
      }
      
      // when
      runBlocking {
        one.await()
        two.await()
      }
    }
    println(time) // 2013 mills
  }
}

launch와 async의 주요 차이점은 launch는 job을 리턴하고, async는 deffered를 리턴하는 것이다. 
job은 코루틴 자체를 의미하므로, launch로 실행되는 코루틴을 취소 할 수도 있고 기다릴 수도 있다. 즉 라이프 사이클에 관심이 있으며, deffered는 future처럼 미래의 결과 값을 의미하므로 완료 되길 기다리다가 리턴되는 값에 관심이 있다.
deffered는 job을 상속받으므로 job의 역할도 할 수 있다. 즉 non-blocking cancellable future 라 볼 수 있다.  

따라서 결과값에 관심이 있으면 async를 사용하고, 아니면 launch를 사용하면 된다. 


Channel 

개인적으로 golang을 매우 좋아하는데, 그 이유는 오로지 심플한 goroutine과 go channel의 존재에 있다. CSP기법의 하나인 이것은 동시성 프로그래밍을 매우 간단하고 직관적으로 만들어 준다. 세상은 단순한 기술이 승리하더라. 그리고 중요한건 재밌다는 사실!!  kotlin에도 go channel식으로 개발하는 것을 지원해 주니 사용 안 할 이유가 없다. 

https://www.baeldung.com/kotlin/channels 에서 코드를 가져왔다. 별다른 설명이 필요 없을 정도로 코드가 깔끔하다.

import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

internal class ChannelTest {
  
  @Test
  fun `should_pass_data_from_one_coroutine_to_another`(){
    runBlocking {
      // given
      val channel = Channel<String>()
      
      // when
      launch { // coroutine1
        channel.send("Hello World!")
      }
      val result = async { // coroutine2
        channel.receive()
      }
      // then
      assertEquals(result.await(),"Hello World!")
    }
    
  }
}

하나의 코루틴에서 다른 코루틴으로 데이터를 전송하고 받는 기본적인 코드이다. 

import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() {
  runBlocking {
    // given
    val channel = Channel<Channel<String>>()
    val eventChannel = Channel<String>()
    
    // when
    launch { // coroutine1
      channel.send(eventChannel)
      print(eventChannel.receive())
    }
    launch { // coroutine2
      val eventChannel = channel.receive()
      eventChannel.send("hi there")
    }
  }
}

go channel에서는 채널을 채널에 전송 할 수 있어서, 해 보았는데 코틀린도 잘 된다.

아래는 Pub-Sub패턴의 코드이다. 

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.produce
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun CoroutineScope.producePizzaOrders(): ReceiveChannel<String> = produce {
  var x = 1
  while (true) {
    send("Pizza Order No. ${x++}")
    delay(100)
  }
}

fun CoroutineScope.pizzaOrderProcessor(id: Int, orders: ReceiveChannel<String>) = launch {
  for (order in orders) {
    println("Processor #$id is processing $order")
  }
}

fun main() = runBlocking {
  val pizzaOrders = producePizzaOrders()
  repeat(3) {
    pizzaOrderProcessor(it + 1, pizzaOrders)
  }
  
  delay(1000)
  pizzaOrders.cancel()
}

produce는 ReceiveChannel<T>을 리턴하는 코루틴이다. (async는 deffered, launch는 job)

마지막으로 채널을 파이프라이닝으로 연결 할 수도 있다.
병렬로 파이프라이닝/체이닝/필터링 패턴을 적용 할 때 좋을 거 같다. 

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.produce

fun CoroutineScope.baking(orders: ReceiveChannel<PizzaOrder>) = produce {
  for (order in orders) {
    delay(200)
    println("Baking ${order.orderNumber}")
    send(order.copy(orderStatus = BAKED))
  }
}

fun CoroutineScope.topping(orders: ReceiveChannel<PizzaOrder>) = produce {
  for (order in orders) {
    delay(50)
    println("Topping ${order.orderNumber}")
    send(order.copy(orderStatus = TOPPED))
  }
}

fun CoroutineScope.produceOrders(count: Int) = produce {
  repeat(count) {
    delay(50)
    send(PizzaOrder(orderNumber = it + 1))
  }
}

fun main() = runBlocking {
  val orders = produceOrders(3)
  
  val readyOrders = topping(baking(orders))
  
  for (order in readyOrders) {
    println("Serving ${order.orderNumber}")
  }
  
  delay(3000)
  coroutineContext.cancelChildren()
}


더 제대로된 코틀린 채널 예제와 설명은 아래 링크를 참고하자.
https://proandroiddev.com/kotlin-coroutines-channels-csp-android-db441400965f

아무튼 Golang 사용하다가 코틀린 코루틴 스터디해보면 정말 지져분해서 사용하기 싫다는 기분이 많이 든다...
언어 자체 제공 vs 라이브러리인것을 훨씬 넘어서는 복잡함
비록 kotlinx.coroutines은 리치라이브러리로써 많은 기능을 제공해 주고 있긴 하지만...
빌트인 기능을 좀 더 깔끔하게 만들 순 없었을까?  코틀린 코루틴 라이브러리 vs 빌트인 

class Tuple(vararg elements: Any) {
  val elements: Array<Any>
  
  init {
    this.elements = elements as Array<Any>
  }
  
  fun get(index: Int): Any {
    return elements[index]
  }
  
  fun size(): Int {
    return elements.size
  }
  
  override fun hashCode(): Int {
    return Arrays.deepHashCode(elements)
  }
  
  override fun equals(o: Any?): Boolean {
    if (this === o) return true
    if (o == null || javaClass != o.javaClass) return false
    val other = o as Tuple
    return Arrays.deepEquals(elements, other.elements)
  }
  
  override fun toString(): String {
    return Arrays.deepToString(elements)
  }
  
  companion object {
    val EMPTY = Tuple()
    fun of(vararg elements: Any?): Tuple {
      return Tuple(*elements as Array<out Any>)
    }
  }
}


data class  Tuple2<T1,T2>(val _1: T1, val _2: T2) : Serializable {
  override fun toString(): String {
   return "($_1, $_2)"
  }
}

data class  Tuple3<T1,T2,T3>(val _1: T1, val _2: T2, val _3: T3) : Serializable{
  override fun toString(): String {
    return "($_1, $_2, $_3)"
  }
}

fun main() {

  val tuple = Tuple("hi", 2, 1.2)
  println(tuple.size())

 val tuple2 = Tuple2<String, Int>("hi", 1)
  println(tuple2)
  val tuple3 = Tuple3<String, Int, Double>("hi", 1, 1.2)
  println(tuple3)
  
}

상황은 다음과 같다.
어느 온라인서점에는 멤버별로 금,은,동의 등급이 있으며, 
각 멤버에게 등급별로 베네핏을 주려고 하며, 베네핏의 종류는 A,B,C가 있다.
멤버는 각 베네핏을 모두 합친 만큼의 베네핏을 받을 수 있다.

원래 베네핏에 대한 로직은 더 복잡해 질 수 있지만, 예제에서는 10,20,30점으로 굉장히 단순화 하였다.

code1)

enum class GRADE {
    BRONZE,
    SILVER,
    GOLD
}

class Member(val grade: GRADE) {

    fun calcBenefit(): Int {
        var total = 0
        when(grade) {
            GRADE.BRONZE -> {
                // discount benefit
                total += 10

                // coupon benefit
                total += 20

                // point benefit
                total += 30
            }
            GRADE.SILVER -> {
                // discount benefit
                total += 20

                // coupon benefit
                total += 30

                // point benefit
                total += 40
            }
            GRADE.GOLD -> {
                // a benefit
                total += 30

                // b benefit
                total += 40

                // c benefit
                total += 50
            }
            else -> {
                throw IllegalArgumentException("unknown grade type!!")
            }
        }
        return total
    }
}

fun main() {
    val memberA = Member(GRADE.BRONZE)
    val benefitA = memberA.calcBenefit()
    println("BRONZE Benefit: " + benefitA)

    val memberB = Member(GRADE.GOLD)
    val benefitB = memberB.calcBenefit()
    println("GOLD Benefit: " + benefitB)
}


code2)

sealed class Member
class BronzeMember : Member()
class SilverMember : Member()
class GoldMember : Member()

class Benefit {
    fun discount(member: Member): Int {
        return when(member){
            is BronzeMember -> 10
            is SilverMember -> 20
            is GoldMember -> 30
        }
    }

    fun coupon(member: Member): Int {
        return when(member){
            is BronzeMember -> 20
            is SilverMember -> 30
            is GoldMember -> 40
        }
    }

    fun point(member: Member): Int {
        return when(member){
            is BronzeMember -> 30
            is SilverMember -> 40
            is GoldMember -> 50
        }
    }
}


fun main() {

    val benefit = Benefit()
    val memberA = BronzeMember() 
    val totalBenefitA = benefit.discount(memberA) + benefit.coupon(memberA) + benefit.point(memberA)
    println("BRONZE Benefit: " + totalBenefitA)
}

code3) visitor 패턴  
보통 요소들을 iterator 돌면서 로직을 적용하는데, visitor는 그 반대로 생각하면 쉽다. 
로직이 요소들을 방문하면서 액션이 이루어진다. 이 코드에서 요소는 멤버이고, 로직은 각 베네핏종류이다. 


sealed class Member {
    var totalBenefit : Int = 0
    abstract  fun calcBenefit(benefit: BenefitVisitor)
}

class BronzeMember : Member() {
    override fun calcBenefit(benefit: BenefitVisitor) {
        totalBenefit += benefit.calcBenefit(this)
    }
}

class SilverMember : Member(){
    override fun calcBenefit(benefit: BenefitVisitor) {
        totalBenefit += benefit.calcBenefit(this)
    }
}
class GoldMember : Member(){
    override fun calcBenefit(benefit: BenefitVisitor){
        totalBenefit += benefit.calcBenefit(this)
    }
}

interface BenefitVisitor {
    fun calcBenefit(member: BronzeMember): Int
    fun calcBenefit(member: SilverMember): Int
    fun calcBenefit(member: GoldMember): Int
}

class DiscountBenefit : BenefitVisitor {
    override fun calcBenefit(member: BronzeMember): Int {
        // bronzemember 특화로직일을 함
        return 10
    }

    override fun calcBenefit(member: SilverMember): Int{
        // silvermember 특화로직일을 함
        return 20
    }

    override fun calcBenefit(member: GoldMember): Int {
        // goldmember 특화로직일을 함
        return 30
    }
}

class CouponBenefit : BenefitVisitor {
    override fun calcBenefit(member: BronzeMember): Int {
        return 20
    }

    override fun calcBenefit(member: SilverMember): Int{
        return 30
    }

    override fun calcBenefit(member: GoldMember): Int {
        return 40
    }

}


class PointBenefit : BenefitVisitor {
    override fun calcBenefit(member: BronzeMember): Int {
        return 30
    }

    override fun calcBenefit(member: SilverMember): Int{
        return 40
    }

    override fun calcBenefit(member: GoldMember): Int {
        return 50
    }

}


fun main() {

    val memberBronze = BronzeMember()
    val memberSilver = SilverMember()
    val memberGold = GoldMember()

    val discount = DiscountBenefit()
    val coupon = CouponBenefit()
    val point = PointBenefit()


    memberBronze.calcBenefit(discount)
    memberBronze.calcBenefit(coupon)
    memberBronze.calcBenefit(point)

    memberSilver.calcBenefit(coupon)
    memberGold.calcBenefit(point)

    println("total : " + memberBronze.totalBenefit)


}

 

double dispaching이 일어나는것을 볼 수 있으며, if문은 없어졌지만, 먼가 장황스럽다고 느껴질수도 있다. 
Visitor Pattern의 의도는 데이터 구조와 처리를 분리하는 패턴인데, 사실 visitor패턴은 Gof의 모든 패턴중에 가장 논쟁이 많아질 수 있는 패턴이라 보여진다. Benefit은 member에 대해 속속들이 알아야 할 수가 있으며, 원래 객체(자료구조의 반대개념)가 가진 단점인 타입 추가에는 유연하나, 기능추가엔 많은 변경을 요하는 문제점에다가 여기서는 요소추가가 많아질 경우 바꿔야 할 경우까지 많아 진다.  다만 요소는 고정될 가능성이 크고, 로직부분이 많아질 가능성이 큰 시스템에서 유용 할 수 있으니 트레이드오프를 잘 계산해서 사용하면 될 듯하다. 


컨베이어 공장에서 자동차가 완성 될 때 단계별로 조금씩 완성되어 가는 모습을 볼 수 있다. 이런 비슷한 경우는 소프트웨어 개발에서도 흔하며, 그 결과로 관련 패턴들도 매우 다양하게 있지다.   
Gof의  Chain of Responsibility패턴은 모든 과정을 거치는게 아니라, 처리 할 수 있으면 처리하고, 못하면 다음 녀석에게  넘기는 "의도"를 말한다. 그리고 이 글의 제목이기도 한 Intercepting Filter의 경우는 (웹개발자 라면 친숙한  HTTP가 호출되어서 최종 컨트롤러 까지 갔다가[inbound], 다시 HTTP리턴을 해주는[outbound] 모습을 상상해 보자) 단계별로 모든 처리를 진행해서 타겟까지 호출되고, 타겟에서 최종 처리된 결과를 역순의 단계로 다시 돌아오는 모형에서 중간 중간 필요한 Filter를 사용자가 쉽게 넣을 수 있게 해주는 의도를 가지고 있다.

clinet -> filter1(word filter) -> filter2 (count filter) -> target  [ inbound]
client <- filter1(word filter) <- filter2 (count filter) <- target  [outbound] 

방식1) 순서를 제어하는 Filter Manager가 필터들을 관리하고 호출을 통제한다.

interface Filter{
    fun process(chain: FilterChain, req: Any, res: Any)
}

class WordFilter: Filter {
    override fun process(chain: FilterChain, req: Any, res: Any){
        //in bound
        val req = req as String
        val listReq = req.splitToSequence(" ").toList()

        chain.process(listReq, res)
    }
}

class CountFilter: Filter {
    override fun process(chain: FilterChain, req: Any, res: Any) {
        chain.process(req, res)
        //out bound

        val res = res as Response?
        val resMap = res?.result  as Map<String, Int>
        res?.result = resMap.filter{it -> it.value >= 2}.size

    }
}

class Target: Filter{
    override fun process(chain: FilterChain, req: Any, res: Any) {
        val req = req as List<String>
        println("Target")
        val mapReq = req.groupBy { it }.mapValues { it.value.size }
        val res = res as Response?
        res?.result = mapReq
    }
}

class FilterChain {

    val filters = mutableListOf<Filter>()
    var nextFilter = -1
    fun addFilter(filter: Filter): FilterChain {
        filters.add(filter)
        return this
    }

    fun process(req: Any, res: Any) {
        nextFilter++
        filters.get(nextFilter).process(this, req, res)
        // 더이상의 필터가 없을 경우 target을 실행시키기도 한다.
        // if (hasNextFilter) nextfilter.process else target.execute() 식으로 
    }
}

class Response {
    var result : Any? = null
}

fun main() {
    val res :Response = Response()

    FilterChain()
        .addFilter(WordFilter())
        .addFilter(CountFilter())
        .addFilter(Target())  // target은 필터에서 제외되어 따로 처리하기도 한다.
        .process("I AM A BOY I AM A GIRL", res)

    println(res.result)
}

FilterChain은 각 Filter들을 관리하며, Filter들의 연관관계 및 순서등을 조율할 수 있다. 
현재 코드에서는 Filter의 리스트상의 순서대로 호출해주고 있지만, 얼마든지 변형 가능 할 것이다. 

각 필터의 process 함수에서는 각각의 비지니스 로직을 처리할수있는데, CountFilter 처럼 다음 필터를 호출한 후에 비지니스 로직을 처리하면 자연스럽게 후처리(out bound) 목적의 코드가된다. 

위의 코드는 사실 너무 복잡한 면이 있는데, 이를 변형한 다른 방법으론 아래와 같이 짤 수도 있을 것이다.
개인적으로는 이런 방식이 좀 더 의도가 명확하게 느껴진다. 

방식2) Filter Manager가 필요 없으며 필터 스스로 다음 필터를 알아야 한다.

data class Request (var param : Any?)
class Response {
    var result : Any? = null
}

interface Filter {
    fun addLast(chain: Filter): Filter
    fun doNext(req: Request): Response
}

abstract class AbstractFilter: Filter{
    var next: Filter? = null
    override fun addLast(chain: Filter): Filter {
        require(this != chain) { "Filter duplicated: $this"}
        val setter = this.next?.let { it::addLast }?:this::next.setter::invoke
        setter(chain)
        return this
    }
}

class WordFilter: AbstractFilter() {

    override fun doNext(req: Request): Response {
        val param = req?.param as String
        req?.param = param.splitToSequence(" ").toList()

        val ret = next?.doNext(req)!!
        return ret
    }
}

class CountFilter: AbstractFilter() {
    override fun doNext(req: Request): Response {
        val ret = next?.doNext(req)

        val resMap = ret?.result as Map<String, Int>
        ret?.result = resMap.filter { it -> it.value >= 2 }.size
        return ret
    }
}

class Target: AbstractFilter(){
    override fun doNext(req: Request): Response {
        val param = req?.param as List<String>
        println("Target")
        val mapReq = param.groupBy { it }.mapValues { it.value.size }
        val res = Response()
        res?.result = mapReq
        return res
    }
}

class FilterChain{

    fun process(req: Request): Response  {
        val chain = WordFilter()
            .addLast(CountFilter())
            .addLast(Target())

        return chain.doNext(req)
    }
}


fun main() {
    val req = Request("I AM A BOY I AM A GIRL")
    val res = FilterChain().process(req)
    println(res?.result as Int)
}

 

먼저의 방식이 FilterChain이 Filter들의 순서를 단독적으로 제어하는 것에 비해서, (즉 각 Filter는 자기의 책임을 다하고, FilterChain에게, 다음것이 먼지모르겠으나 다음 처리를 해주세요~ 라고 위임) 이 방식은 링크드 리스트 방식으로 필터 스스로가 다음 필터를 내장하게 만들었다. 따라서 좀 더 직관적으로 볼 수 있겠다.  

P.S)
- 참고로 위의 예제는 좀 문제가 많다. word, count 하는 것일 뿐이라면 그냥 한줄로 짜도된다. 
- 패턴이나 코드들은 상황에 따라서 짜야하는게 맞다. 유명 패턴이라고 맞는게 절대 아니다. 

Dynamic Proxy & Reflection 1

class Tuple3 (val name:String, val age: Int, val rate: Double){
    fun size(): Int {
        return 3
    }
    fun getValue(index: Int): Any?{
        return when(index) {
            0 -> name
            1 -> age
            2 -> rate
            else -> null
        }
    }
}

class RemoteService{
    fun action(req: Any?): Tuple3 {
       //do something
        return Tuple3("tom", 1, 1.2)
    }
}

///

data class Row(val name: String, val age: Int, val rate: Double)
interface Table {
    fun getRow1(req: String): Row
    fun getRow2(req: String): List<String>
}

class TableHandler: InvocationHandler {
    
    override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {
        val methodName = method?.name
        val service = RemoteService()
        if(methodName == "getRow1"){
           val returns = service.action(args)
           return cast(returns, method.genericReturnType)
        }
        return null
    }
    
    fun cast (value : Any?, typeClass: Type): Any?{
        if(value?.let{value::class.java} == typeClass){
            return value
        }
        
        return when{
            value is Tuple3 ->  {
                Reflection.createKotlinClass(typeClass as Class<*>).primaryConstructor
                    ?.takeIf{it.parameters.size == value.size() }
                    ?.let { it.call(*it.parameters.mapIndexed{ i, parameter -> cast(value.getValue(i), parameter.type.javaType) }.toTypedArray())}
                    ?:value
            }
            
            else -> when{
                typeClass == Void.TYPE -> null
                typeClass == Int::class.java -> when (value){
                    is Long -> value.toInt()
                    else -> value
                }
                typeClass == Double::class.java -> when(value) {
                    is Int -> value.toDouble()
                    else -> value 
                }
                else -> value
            }
        }
    }
}

fun main() {
    val table = Proxy.newProxyInstance(Table::class.java.classLoader, arrayOf(Table::class.java), TableHandler()) as Table
    println(table.getRow1("1"))
}


Dynamic Proxy & Reflection 2

import java.lang.reflect.*
import kotlin.jvm.internal.Reflection
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.javaType

data class Tuple(val name: String, val age: Int, val rate: Double){
    fun size(): Int{
        return 3
    }
    fun get(index: Int): Any? {
        return when(index) {
            0 -> name
            1 -> age
            2 -> rate
            else -> null
        }
    }
}

class RemoteService() {
    fun getRow1(req: Request): Tuple{
        //do somthing
        println("remote call : ${req.method} ")
        println("remote received : ${req.param} ")
        return Tuple("hama",22,0.5)
    }
    
    fun getRow2(req: Request): Array<String>{
        //do somthing
        println("remote call : ${req.method} ")
        println("remote received : ${req.param} ")
        return arrayOf("hama","22","0.5")
    }
}

///////////


data class Request(val method: String, val param: Any?)
data class Row1(val name: String, val age: Int, val rate: Double)


interface Table {
    fun getRow1(key: String): Row1
    fun getRow2(key: String): List<String>
}

class TableHandler : InvocationHandler {
    
    override fun invoke(proxy: Any?, method: Method, args: Array<out Any>?): Any? {
        val methodName = method.name
        return if (method.declaringClass.isInterface) {
            val req = Request(methodName, args)
            val returns = RemoteService().getRow2(req)
            return cast(returns, method.genericReturnType)
        } else {
            method.invoke(this, *(args ?: emptyArray()))
        }
    }
    
    fun cast(value: Any?, typeClass: Type): Any? {
        if (value?.let { it::class.java } == typeClass) {
            return value
        }
        
        fun Type.isCollection(): Boolean {
            return when (this) {
                is ParameterizedType -> (this.rawType as? Class<*>)?.let { Collection::class.java.isAssignableFrom(it) }
                    ?: false
                is Class<*> -> Collection::class.java.isAssignableFrom(this)
                else -> false
            }
        }
        
        return when {
            value is Tuple -> {
                Reflection.createKotlinClass(typeClass as Class<*>).primaryConstructor
                    ?.takeIf { it.parameters.size == value.size() }
                    ?.let {
                        it.call(*it.parameters.mapIndexed { i, parameter ->
                            cast(
                                value.get(i),
                                parameter.type.javaType
                            )
                        }.toTypedArray())
                    }
                    ?: value
            }
            
            value?.let { it::class.java.isArray } ?: false -> {
                when {
                    typeClass.isCollection() -> (value as Array<*>)
                        .map { element ->
                            cast(
                                element,
                                (typeClass as? ParameterizedType)?.actualTypeArguments?.let { argument -> argument[0] }!!
                            )
                        }
                        .toList()
                        .also { println(it) }
                    else ->
                        Reflection.createKotlinClass(typeClass as Class<*>).primaryConstructor
                            .also { println(it) }
                            ?.let { it.call(value) }
                            ?: value
                    
                }
            }
            else -> when {
                typeClass == Void.TYPE -> null
                typeClass == Int::class.java -> when (value) {
                    is Long -> value.toInt()
                    else -> value
                }
                typeClass == Double::class.java -> when (value) {
                    is Int -> value.toDouble()
                    else -> value
                }
                else -> value
            }
        }
    }
}


fun main() {
    val loader = Table::class.java.classLoader
    val table = Proxy.newProxyInstance(loader, arrayOf(Table::class.java), TableHandler()) as Table
//    val result = table.getRow1("12")
//    println(result.name)
//    println(result.age)
//    println(result.rate)

    println(table.getRow2("12"))
}

 

Dynamic Proxy & Reflection & Annotation 

import kotlin.jvm.internal.Reflection
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.javaType
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import java.lang.reflect.Type
import kotlin.reflect.KClass


//

object AnnotationUtils {
    fun <T : Annotation> findAnnotation(method: Method, annotation: Class<T>) : T? {
        return findAnnotation(method.declaringClass, method, annotation)
    }

    @Suppress("UNCHECKED_CAST")
    fun <T : Annotation> findAnnotation(clazz: Class<*>, method: Method, annotation: Class<T>) : T? {
        val annotatedMethods = clazz.methods.filter { it.signatureEquals(method) }
            .flatMap { it.annotations.filter { a -> annotation.isInstance(a) }.map { a -> a as T } }
        return if (annotatedMethods.isEmpty()) {
            val children = clazz.interfaces.mapNotNull { findAnnotation(it, method, annotation) }
            if (children.isEmpty()) {
                clazz.superclass?.let { findAnnotation(it, method, annotation) }
            } else {
                children.first()
            }
        } else {
            annotatedMethods.first()
        }
    }
    fun <T : Annotation> hasAnnotation(clazz: Class<*>, method: Method, annotation: Class<T>): Boolean {
        return null != findAnnotation(clazz, method, annotation)
    }


    private fun Method.signatureEquals(other: Method): Boolean {
        return name == other.name && parameterTypes.contentEquals(other.parameterTypes)
    }
}

//
// ============= remote service =============//

data class Tuple(val name: String, val age: Int, val rate: Double){
    fun size(): Int{
        return 3
    }
    fun get(index: Int): Any? {
        return when(index) {
            0 -> name
            1 -> age
            2 -> rate
            else -> null
        }
    }
}

data class Request(val method: String, val param: Any?)
data class Response( val returns: Any?)
annotation class Converter(val value: KClass<out Action>)

class RemoteService() {
    fun getRow(req: Request): Tuple{
        //do somthing
        println("remote call : ${req.method} ")
        println("remote received : ${req.param} ")
        return Tuple("hama",22,0.5)
    }

}

interface Action {
    fun doAction(args: Array<out Any>?): Any?
}


fun CreateAction(method: Method, args : Array<out Any>?): Action {
    val converterClazz = AnnotationUtils.findAnnotation(method, Converter::class.java)!!
    val obj = converterClazz.value.primaryConstructor?.call()
    return obj as Action
}

class ProxyInvocationHandler()
    : InvocationHandler {

    override fun invoke(proxy: Any?, method: Method, args: Array<out Any>?): Any? {
        val methodName = method.name
        return if (method.declaringClass.isInterface) {
            val action = CreateAction(method, args)
            val req = Request(methodName, action.doAction(args))

            val remote =  RemoteService()
            val returns = remote.getRow(req)
            return cast(returns, method.genericReturnType)
        } else {
            (args.let { method.invoke(this, *(args ?: emptyArray())) } ?: method.invoke(this))
        }
    }

    fun cast( value : Any? , typeClass: Type): Any? {
        if (value?.let { it::class.java } == typeClass) {
            return value
        }
        return when {
            value is Tuple -> {

                Reflection.createKotlinClass(typeClass as Class<*>).primaryConstructor
                        ?.takeIf { it.parameters.size == value.size() }
                        ?.let { it.call(*it.parameters.mapIndexed { i, parameter -> cast(value.get(i) , parameter.type.javaType) }.toTypedArray()) }
                        ?:value

            }
            else ->  when {
                typeClass == Void.TYPE -> null
                typeClass == Int::class.java -> when (value) {
                    is Long -> value.toInt()
                    else -> value
                }
                typeClass == Double::class.java -> when (value) {
                    is Int -> value.toDouble()
                    else -> value
                }
                else -> value
            }
        }
    }
}




data class DataTableTupleRow(val name : String, val age: Int, val rate : Double)


interface Table {
    fun getRow(param : String): DataTableTupleRow
}
interface DataTable: Table {

    @Converter(DataConvertor::class)
    override fun getRow(param : String): DataTableTupleRow

    class DataConvertor : Action {
        override fun doAction(args: Array<out Any>?): Any? {
            val arg = args?.get(0)
            return (arg as String).toInt()
        }
    }

}

fun main() {
    val loader = DataTable::class.java.classLoader
    val handler = ProxyInvocationHandler()
    val table = Proxy.newProxyInstance(loader, arrayOf(DataTable::class.java) , handler) as Table
    val result = table.getRow("12")
    println(result)

}

진행중..

 

import jdk.nashorn.internal.ir.Block
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.thread
import kotlin.concurrent.withLock

class Future {
    lateinit var result: String
    var isDone = false

    val lock = ReentrantLock()
    val condition = lock.newCondition()

    fun get() : String {
        if(isDone) return result   // (1) 

        lock.withLock {
            condition.await()
        }

        return result
    }
    fun set(result: String) {
        lock.withLock {
            this.result = result
            this.isDone = true
            condition.signalAll()
        }

    }
}

class Work {
    fun doSomething(): Future {
        val future = Future()

        thread(start = true, name = "mythread1") {
            Thread.sleep(3000)
            future.set("ok")
        }

        return future;
    }
}

fun main() {
    println ("================= start ================")

    val work = Work()
    val future = work.doSomething()
    Thread.sleep(5000) 
    val result = future.get()

    println("result: ${result}")
}

Future패턴의 구현이다. 

위의 (1) 번 주석의 코드가 왜 필요한지 생각해보자. 
저것을 안하면 어떻게 될 까? 

그렇게 되면 이 코드는 데드락이 걸린다.
 set()을 먼저하게 되는데, 이때 condition.signalAll을 해서 await된 메인 쓰레드를 깨우려고 해바짜, 메인쓰레드 진입전이기 때문에 헛수고가 되버리며, 5초 후에  get이 들어오고 나서 await를 하게된 메인 쓰레드를 깨워줄 시그널은 없게 된다.

+ Recent posts