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를 하게된 메인 쓰레드를 깨워줄 시그널은 없게 된다.


interface Account {

}

class EthreumAccount(val gas: Int) : Account{

}

class FabricAccount(val org: String) : Account {

}

class BlockChain(val networks : Map<String,String>) {

    fun send(account: Account) {
        when(account) {
            is EthreumAccount -> {
                val gas = account.gas
                val network = networks.get("eth")
                println("send to ${network} with gas: ${gas}")
            }
            is FabricAccount -> {
                val org = account.org
                val network = networks.get("fab")
                println("send to ${network} with org: ${org}")

            }
            else -> {
                println("illegal parameter exception!!")
            }
        }
    }
}

fun main() {
    println ("================= start ================")
    val blockchain = BlockChain(mapOf(Pair("eth","address-1"),Pair("fab","address-2")))
    val account1 = EthreumAccount(3)
    blockchain.send(account1)
    val account2 = FabricAccount("navy")
    blockchain.send(account2)

}

- BlockChain 클래스에 너무 많은 책임이 몰려있다. 
- if문으로 새로운 account타입이 들어올 때마다 분기해줘야 한다. 

더블디스패치방식으로 리팩토링 해보자.

import jdk.nashorn.internal.ir.Block


interface Account {
    fun send(blockchain: BlockChain)
}

class EthreumAccount(val gas: Int) : Account{

    override fun send(blockchain: BlockChain){
        val network = blockchain.networks.get("eth")
        println("send to ${network} with gas: ${gas}")
    }
}

class FabricAccount(val org: String) : Account {

    override fun send(blockchain: BlockChain){
        val network = blockchain.networks.get("fab")
        println("send to ${network} with org: ${org}")
    }
}

class BlockChain(val networks : Map<String,String>) {

    fun send(account: Account) {
       account.send(this)
    }
}

fun main() {
    println ("================= start ================")
    val blockchain = BlockChain(mapOf(Pair("eth","address-1"),Pair("fab","address-2")))
    val account1 = EthreumAccount(3)
    blockchain.send(account1)
    val account2 = FabricAccount("navy")
    blockchain.send(account2)

}

아래와 같은 이런 타입 시그니처 본 적이 있나요?

Entity<E: Entity<E>> 

처음 이런 모습을 보았을때,  "머야 이 퐝당한 코드" 같은 생각이 드는건 어쩌면 당연합니다. 그리고 이것에 대해 알아 보기 위해 구글링등을 하기 시작 했을테고, 결국 이 블로그를 찾아 왔을 지도 모르겠네요. 그렇다면 잘 찾아 왔습니다.

도대체 이것은 뭘 까요? 보통 우린  interface Entity<T>이 정도로만 써 왔지 않습니까?
하지만 알고보면  매우 간단하니깐 겁먹지 말고 , 간단한 예를 통해서 이해해 봅시다.
이것을 이해하기 위한 기본적인 부분들도 설명을 하니깐 걱정마세요.

1. 인터페이스

먼저 코틀린에서의 인터페이스를 살펴 봅시다.

interface MyInterface { 
    fun bar() 
}

일반적으로 내용이 없는 메소드들을 선언합니다. 보통 이것을 상속받아서 사용하겠죠. 아래 처럼요.

class Child : MyInterface {
    override fun bar() {
        // body
    }
}

근데 코틀린에서는 인터페이스에 변수도 넣을 수 있으며, 메소드의 본문도 채울 수가 있어요.

interface MyInterface {
    val prop: Int // abstract

    fun foo() {
        //do somthing 
        print(prop)
    }
}

class Child : MyInterface {
    override val prop: Int = 29
}

이렇게 본문이 채워진 인터페이스의 메소드는 자식이 오버라이딩을 할 필요가 없어집니다. 

interface Named {
    val name: String
}

interface Person : Named {
    val firstName: String
    val lastName: String

    override val name: String get() = "$firstName $lastName"
}

data class Employee(
    // implementing 'name' is not required
    override val firstName: String,
    override val lastName: String,
    val position: Position
) : Person

인터페이스 자체를 상속받기도 합니다.


2. 일반적인 코드 

자 여기 사과와 오렌지 클래스가 있다고 합시다. 

data class Apple (val price : Int){
    fun compareTo(other: Apple) : Boolean {
        return this.price > other.price
    }
}

data class Orange (val price : Int){
    fun compareTo(other: Orange) : Boolean {
        return this.price > other.price
    }
}

각 과일들은 가격이 있으며, 서로 동일한 과일들끼리만 가격을 비교 할 수가 있다고 해봅시다.
먼가 중복되는걸 싫어하는 리팩토링의 화신인 우리로써는 이 코드가 탐탁치 않습니다.
네!! price와 compareTo를 추출하고 싶어지죠? 이렇게 만들어 봅니다. 

interface Fruits { 
    val price : Int
    fun compareTo(other: Fruits) : Boolean { 
        return this.price > other.price 
    }
}

data class Apple (override val price : Int): Fruits
data class Orange (override val price : Int): Fruits
   

좋습니다!! 중복된 코드들이 없어졌습니다. 보통 여기서 코드 만지기를 그만 두곤 하는데요. 
이런 선현의 지혜를 들어봤나요? 


"니가 좀 더 고생해서 후임자가 실수하기 어려운 코드를 만들어라 "

위의 코드는 아래 처럼 문제가 될 수 있습니다.

val apple1 = Apple(30)
val apple2 = Apple(50)
val orange1 = Orange(100)
val orange2 = Orange(200)

app.compareTo(or)  // 사과와 오렌지는 서로 비교하면 안되요!!

사과와 오렌지는 서로 다른 과일이기 때문에 비교하면 안되지만, 비교해 버렸습니다. 
우리는 컴파일 타임에 미리 이런 실수를 알아채길 원해요. 

3. 코틀린에서의 제네릭스

class Box<T>(t: T) {
	var value = t 
}

val box: Box<Int> = Box<Int>(1)

코틀린에서는 자바와 비슷하게 <T>이런식으로 타입매개변수를 지원합니다.

T: Any?

사실 위의 <T>는 T: Any? 의 줄임말입니다. T는 Any?타입을 상속받은 것들이라면 다 된다는 의미입니다. Upper Bounded 되었다고 합니다.

class Box<T : Number>(t: T) {
	var value = t 
}

val box: Box<Int> = Box<Int>(1)

즉 이렇게 <T: Number>로 제약을 가하면, Box<String>은 불가능합니다.컴파일타임에 문제를 알려주죠.

이제 다시 본론으로 들어가 봅시다.

interface Fruits<T> {
    val price : Int
    fun compareTo(other: T) : Boolean {
        return this.price > other.price
    }
}

data class Apple (override val price : Int): Fruits<Apple>
data class Orange (override val price : Int): Fruits<Orange>

위처럼 타입을 매개변수로 주니깐 apple1.compareTo(orange1) 이렇게 다른 과일끼리 비교하면 안된다고 알려줍니다.
하지만 여전히 문제가 있습니다. 어디 일까요?

interface Fruits<T> {
    val price : Int
    fun compareTo(other: T) : Boolean {
        return this.price > other.price   // 여기서 T타입에 price가 있는지 모릅니다.!! 에러 
    }
}

T타입은 무엇이건 될 수 있기 때문에, price가 없을 수도 있어요. 

data class Apple (override val price : Int): Fruits<Int>  // Fruits<Int> ??

그리고 Furits에 Int를 할당해도 컴파일에 문제가 없습니다. 저렇게 하면 안되는데 말이죠. 

4. recursive types 으로 제한(bound) 해서 해결하기   

자 이제 결론입니다!!! 집중하세요.
아래처럼 코드를 짜면 문제를 해결 할 수 있습니다. 

interface Fruits<T : Fruits<T>> {
    val price : Int
    fun compareTo(other: T) : Boolean {
        return this.price > other.price
    }
}

data class Apple (override val price : Int): Fruits<Apple>
data class Orange (override val price : Int): Fruits<Orange>

T 는 Fruits<T>의 제한을 받는 타입이어야만 해요. 즉 T타입은 Fruits를 상속받은 타입이어야 한다는 겁니다.
위에 코드를 보면 Apple과 Orange는 Fruits를 상속받았기 때문에 Fruits의 타입으로 들어 갈 수 있으며 (Int가 타입매개변수로 들어갈 수 도 있는 문제의 해결) T는 Fruits를 상속받는 것이기 때문에 price는 반드시 있게 됩니다( other.price문제 해결) 

5. 한계

다 잘된것 같았지만 결국 다음과 같은 구멍은 존재하게 되었습니다.
역시 의도치 않게 코드를 짜는 빌런은 항상 등장하게 마련이죠. ㅎㅎ

data class Apple (override val price : Int): Fruits<Apple>  // 좋습니다.
data class Orange (override val price : Int): Fruits<Orange> // 좋아요!
data class Banana (override val price : Int): Fruits<Apple> // 엇 이건 먼가요? 

바나나라는 새로운 클래스를 만들었는데, 상속은 Fruits<Apple>을 이용했네요. ;;;;;  
이 코드는 컴파일은 잘됩니다. 하지만 버그죠. 

자바와 코틀린에서는 이런 문제까지는 해결해주지 못하는 것으로 알고 있습니다.
다만 스칼라에서는 가능합니다.  (  self: E => 라는 방식을 통해서)

그럼 여기까지 recursive type bound에 대해서 알아보았습니다.

감사합니다.

* 해당 글은 리눅스 기준 입니다. (CentOS 7버전, Ubuntu18에서 잘됨)



InfluxDB 2.x 설치 

(docs.influxdata.com/influxdb/v2.0/get-started/?t=Linux) 

아래 2가지 방식 중에 패키지로 서비스로 시작하는것을 추천한다.

파일로 받아서 실행 하기)

  1. curl -s https://repos.influxdata.com/influxdb2.key | gpg --import -
  2. wget https://dl.influxdata.com/influxdb/releases/influxdb2-2.0.4-linux-amd64.tar.gz

   3. tar xvzf influxdb2-2.0.4-linux-amd64.tar.gz
   4. cd influxdb2-2.0.4-linux-amd64
   5. influxd  로 시작한다. 


패키지로 인스톨 및 서비스시작하기   (arm64 or amd64 시스템에 맞게 변경해야함)

- RedHat & CentOS (amd 64-bit)

https://dl.influxdata.com/influxdb/releases/influxdb2-2.0.4.x86_64.rpm
sudo yum localinstall influxdb2-2.0.4.x86_64.rpm

systemctl start influxdb 
systemctl enable influxdb 
systemctl status influxdb 

- Ubuntu & Debian (amd 64-bit)

https://dl.influxdata.com/influxdb/releases/influxdb2-2.0.4-amd64.deb
sudo dpkg -i influxdb2-2.0.4-amd64.deb

 

sudo service influxdb start
sudo service influxdb status


InfluxDB UI설정하기 

최신 InfluxDB는 UI도 수려하게 지원한다. 따라서 UI를 통한 셋업방법을 알아보자.

1. localhost:8086 으로 접속하면 UI에 접근할수있다.
2. 아래 설정을 넣는다. (매우 중요하다.)

  1. Username 넣어준다.   (예: hama) 
  2. Password 넣어준다.  (기억해두시라) 
  3. Organization 이름 넣어준다.  <-- 이전버전에는 없는 것이다.
  4. Bucket 이름 생성한다.  <-- 이전버전에는 없는 것이다. 
  5. Click Continue.

3. 메인 화면에서 Build a dashboard 를 선택한다. 

4. Create Dashboard 를 선택한다. 

5. Add Cell 을 선택한다. 

6. 
- from (bukcet) :  monitoirng 선택한다. 즉 bucket 이름이다
- filter (_measurement)  : folder_status 를 선택한다. .
- filter (_field) : size를 선택한다. 
- filter (tag) : tag를 선택한다. 
- 5s refresh : 5초마다 화면을 갱신해준다
- past 1h : 과거 1시간 전 데이터 부터 보여준다
- Submit : 적용한다. 

Influxdb의 ui를 통해 위와 같이 나오는 것을 볼 수 있다. (메뉴얼없어도 직관적으로 다룰 수 있을 것이다)

InfluxDB에 데이터 넣기 

대략 아래와 같이 코딩하자.  (참고 소스 : https://github.com/wowlsh93/monitoring)

module github.com/wowlsh93/monitoring

go 1.14

require (
    ....
	github.com/influxdata/influxdb-client-go/v2 v2.2.2
)

- influxdb 클라이언트 의존성은 v2.2.2 

//DBPATH: http://localhost:8086
//AUTHTOKEN: zYR-2G5BRmb5SDIkbKRUe2DXyon4rbXEzjoTwsSDHMUWeO3hTjasWBIGw8W7Dy_QxipDNWOj2g5MMD9le8-B3Q==

db := influxdb.Influxdb{
	Client :  influxdb2.NewClient(conf.DBPATH, conf.AUTHTOKEN),
}

- influxdb를 접근하기위해서는 db 경로와 auth 토큰이 필요하다. 
auth token 발급법은 docs.influxdata.com/influxdb/v2.0/security/tokens/create-token/ 링크에서 알수있다.
간략히 설명하면 UI에서 Data -> Tokens를 가면 발급받을 수 있다. (아래 빨간색 사각형)

package influxdb

import (
	"context"
	"github.com/influxdata/influxdb-client-go/v2"
	"time"
)

type Influxdb struct {
	Client influxdb2.Client
}

func (db* Influxdb) AddData(measurement string, curSize int64) {
	writeAPI := db.Client.WriteAPIBlocking("opusm", "monitoring")
	p := influxdb2.NewPointWithMeasurement(measurement).
		AddTag("storage", "total").
		AddField("size", curSize).
		SetTime(time.Now())

	writeAPI.WritePoint(context.Background(), p)
}
func (db* Influxdb) Close() {
	db.Client.Close()
}

- 위와 같이 influxdb에 값을 넣을 수 있다.
- "opusm" 은 조직이고
- "ledgermaster" 은 버켓이고 (참고로 이건 데이타베이스라고 생각하자.)
- Measurement 도 지정하자. (참고로 이건 테이블과 비슷하다고 보면 된다) 
- Tag, Field 도 넣어주자 (참고로 Tag는 인덱싱되는 컬럼, Field는 그냥 컬럼들이라고 생각하자, 둘다 key,value 쌍이다)
- Field 값은 필드 값은 strings, floats, integers, boolean 타입을 가진다. 
- 해당 timestamp에 field set, tag set 들이 구성된다.  (key, value 를 합쳐서 set이라고 한다) 

* tag 는 아래와 같이 사용된다. 

butterflies 와 honeybees의 갯수는 일반 필드이고, location 과 scientist는 tag인데 생각을 해보자!!  보통 우리는 과학자에 필터링을 걸 것어서 사용 할 것이다. scientist=perpetua 처럼 말이다. perpetua 과학자가 처리한 데이터만 골라서 보여주고 싶을 것이다. 이런 그룹짓고 싶은 필터링에 indexing을 거는 것이다 tag를 사용해서~~

* 포인트와 시리즈이라는 개념도 중요한데 시리즈에 대해 잘 이해가 안가는..

Telegraf 1.17   (influxdb2 용) 

(docs.influxdata.com/telegraf/v1.17/introduction/getting-started/)

1. 설치하기 

Ubuntu & Debian
wget https://dl.influxdata.com/telegraf/releases/telegraf_1.17.3-1_amd64.deb sudo dpkg -i telegraf_1.17.3-1_amd64.deb

RedHat & CentOS
wget https://dl.influxdata.com/telegraf/releases/telegraf-1.17.3-1.x86_64.rpm
sudo yum localinstall telegraf-1.17.3-1.x86_64.rpm

Linux Binaries (64-bit)
wget https://dl.influxdata.com/telegraf/releases/telegraf-1.17.3_linux_amd64.tar.gz
tar xf telegraf-1.17.3_linux_amd64.tar.gz


2. 기본설정하기 

2-1)
vi /etc/telegraf/telegraf.conf 에서 
[[outputs.influxdb_v2]]
urls = ["$INFLUX_HOST"]
token = "$INFLUX_TOKEN"
organization = "$INFLUX_ORG"
bucket = "telegraf"

을 해주면 influxdb2로 출력해준다. 

2-2))
telegraf -sample-config --input-filter cpu:mem --output-filter influxdb > telegraf.conf
cpu 사용량 및 메모리 사용량에 대한 메트릭을 읽게한다. 아래의 output출력으로 정보를 보낼 수 있다.


3. 시작하기 

systemctl start telegraf <-- 시작하기
sudo systemctl status telegraf  <-- 확인하기 

UI의 telegraf 버킷에 각종 system 값들이 들어온것을 볼 수 있다. 

4. 특정폴더사이즈 감시 

4-1) 
#!/usr/bin/env bash
du -bs "${1}" | awk '{print "[ { \"bytes\": "$1", \"dudir\": \""$2"\" } ]";}'

를 metrics-exec_du.sh 이름으로 만든다.

4-2) 아래를 telegraf.conf파일에 추가한다.

[[inputs.exec]]
  commands = [ "YOUR_PATH/metrics-exec_du.sh /var/lib/influxdb/data" ]
 timeout = "10s"
 name_override = "quorum-test"
 name_suffix = ""
 data_format = "json"
 tag_keys = [ "dudir" ]

4-3) systemctl restart telegraf  로 다시 시작한다.  

du 가 추가된 것을 볼 수 있다.

Grafana 8.0.6


1. 설치하기 grafana.com/grafana/download

Red Hat, CentOS, RHEL, and Fedora(64 Bit)

wget https://dl.grafana.com/oss/release/grafana-8.0.6-1.x86_64.rpm
sudo yum install grafana-8.0.6-1.x86_64.rpm

Ubuntu and Debian(64 Bit)

sudo apt-get install -y adduser libfontconfig1
wget https://dl.grafana.com/oss/release/grafana_8.0.6_amd64.deb
sudo dpkg -i grafana_8.0.6_amd64.deb

2. 실행하기  grafana.com/docs/grafana/latest/installation/rpm/#2-start-the-server

sudo systemctl daemon-reload
sudo systemctl start grafana-server
sudo systemctl status grafana-server
sudo systemctl enable
grafana-server

* 방화벽 해제 (centos에서 외부에서 3000포트로 접근하려는데 방화벽이 실행중이라면)
sudo systemctl stop firewalld

3. Grafana UI 다루기 grafana.com/docs/grafana/latest/getting-started/getting-started/

브라우저에서 다음 주소로 Grafana UI 에 들어간다. => http://localhost:3000/

admin/admin 으로 들어간후에 비밀번호 바꿔준다. 

메인화면은 위와 같다. 

4. Data Source 추가하기 

Data sources선택 

InfluxDB 선택 

* 이전버전 InfluxDB 데이타소스 방식과 매우 많이 달라졌다. (4버전대가 편했는데....) 
- 이름은 아무거나 만들어주고
- 여기선 Query Language 를 Flux로 한다. 이게 새로 추가된 방식이다. (InfluxDB 2.x대에는 이걸 써야한다)
- HTTP에 URL을 http://your ip address:8086

나머지는 건드리지 않았다. (혹시 Basic auth 켜있으면 꺼준다)

- 조직은 influxdb 설정에서 만든것을 넣어준다. 
- 토큰은 influxdb 에서 만든 auth token 문자열을 넣어준다.
- 버킷에는 influxdb 설정에서 만든 것을 넣어준다.

나머지는 건드리지 않았다. 
Save& Test를 해주며 끝마친다. 

5. 데시보드 추가하기 

Dashboard선택 

일단 Add an empty panel 을 선택한다.

이전 버전(4.x대 이전)과는 굉장히 많은 차이가 있다.

- Flux language syntax 를 이용하여 쿼리하여 데이터를 보여준다.
- bucket 으로 부터 3시간 이전 데이터부터 2시간 이전데이터까지를 보여준다. 
- 새로 만들어진 Flux 스크립트 기반으로 모니터링 환경을 꾸미려면 관련 학습이 선행되어야 할 것 같다. 

Getting start with Flux => https://docs.influxdata.com/influxdb/v2.0/query-data/get-started/

이로써 기초적인 설치와 세팅이 끝났다.


6. Flux 예제 


-  example-bucket 이란 이름의 버킷으로 부터 데이터를 가져온다.
- 1시간 전 부터 시작하는 데이터를 가져 온다
- measurement 는 "cpu" 를 가져온다.
- tag 키가 cpu이고 tag 밸류가 "cpu-total" 인 포인트만 가져온다. 
- 1분간의 데이터를 평균(mean)하여 가져온다. (즉 그래프에는 1시간 전부터 시작하여 1분당 한개의 값만 나올 것이다.)


+ Recent posts