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문은 없어졌지만, 먼가 장황스럽다고 느껴질수도 있다. VisitorPattern의 의도는 데이터 구조와 처리를 분리하는패턴인데, 사실 visitor패턴은 Gof의 모든 패턴중에 가장 논쟁이 많아질 수 있는 패턴이라 보여진다. Benefit은 member에 대해 속속들이 알아야 할 수가 있으며, 원래 객체(자료구조의 반대개념)가 가진 단점인 타입 추가에는 유연하나, 기능추가엔 많은 변경을 요하는 문제점에다가 여기서는 요소추가가 많아질 경우 바꿔야 할 경우까지 많아 진다. 다만 요소는 고정될 가능성이 크고, 로직부분이 많아질 가능성이 큰 시스템에서 유용 할 수 있으니 트레이드오프를 잘 계산해서 사용하면 될 듯하다.
컨베이어 공장에서 자동차가 완성 될 때 단계별로 조금씩 완성되어 가는 모습을 볼 수 있다. 이런 비슷한 경우는 소프트웨어 개발에서도 흔하며, 그 결과로 관련 패턴들도 매우 다양하게 있지다. Gof의 Chain of Responsibility패턴은 모든 과정을 거치는게 아니라, 처리 할 수 있으면 처리하고, 못하면 다음 녀석에게 넘기는 "의도"를 말한다. 그리고 이 글의 제목이기도 한 Intercepting Filter의 경우는 (웹개발자 라면 친숙한 HTTP가 호출되어서 최종 컨트롤러 까지 갔다가[inbound], 다시 HTTP리턴을 해주는[outbound] 모습을 상상해 보자) 단계별로 모든 처리를 진행해서 타겟까지 호출되고, 타겟에서 최종 처리된 결과를 역순의 단계로 다시 돌아오는 모형에서 중간 중간 필요한 Filter를 사용자가 쉽게 넣을 수 있게 해주는 의도를 가지고 있다.
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 하는 것일 뿐이라면 그냥 한줄로 짜도된다. - 패턴이나 코드들은 상황에 따라서 짜야하는게 맞다. 유명 패턴이라고 맞는게 절대 아니다.
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 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 => 라는 방식을 통해서)
- 위와 같이 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를 사용해서~~
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출력으로 정보를 보낼 수 있다.
* 이전버전 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 스크립트 기반으로 모니터링 환경을 꾸미려면 관련 학습이 선행되어야 할 것 같다.
- example-bucket 이란 이름의 버킷으로 부터 데이터를 가져온다. - 1시간 전 부터 시작하는 데이터를 가져 온다 - measurement 는 "cpu" 를 가져온다. - tag 키가 cpu이고 tag 밸류가 "cpu-total" 인 포인트만 가져온다. - 1분간의 데이터를 평균(mean)하여 가져온다. (즉 그래프에는 1시간 전부터 시작하여 1분당 한개의 값만 나올 것이다.)