저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의
앞서 시간 윈도우와 개수 윈도우에 대해 알아봤었는데, Esper는 이 외에도 다양한 종류의 뷰를 제공하고 있다. 이번 글에서는 몇 가지 뷰를 정리해보았다.

이전 글
Esper에서는 뷰는 이벤트를 유지하는 공간이다. EPL에서 사용중인 뷰에 담긴 이벤트 목록을 구하고 싶다면 다음의 코드를 사용하면 된다.

SafeIterator<EventBean> iter = eps.safeIterator(); // eps는 EPStatement
while (iter.hasNext()) {
    EventBean bean = iter.next();
    SomeEvent event = (SomeEvent)bean.getUnderlying());
}
iter.close();

1. 윈도우 뷰


앞서 EPL 기초 글에서 4개의(시간, 시간 배치, 길이, 길이 배치) 윈도우 뷰를 설명했었다. 여기선 몇 개의 윈도우 뷰를 추가로 설명하겠다.


외부 시간 윈도우(externally-timed window)


시간 윈도우가 이벤트의 발생 시점과 엔진 시간을 기준으로 윈도우에 포함될 이벤트를 결정한다면, 외부 시간 윈도우는 계산된 시간을 기준으로 윈도우에 포함될 이벤트를 결정한다. 다음은 설정 예이다.


select avg(responseTime) as avg from AccessLog.win:ext_timed(accessTime, 3 seconds)


win:ext_timed의 첫 번째 파라미터는 시간을 계산할 때 사용할 표현식으로 밀리초 단위의 값을 사용한다. 위 EPL에서는 AccessLog의 accessTime 값을 사용하였다. 위 EPL을 실행하면 win:ext_timed는 다음과 같은 방식으로 동작한다.

  • 새로운 AccessLog 이벤트 n이 들어오면, n.accessTime 값을 기준으로 이전 이벤트 x에 대해 x.accessTime 값이 3초 지난 x를 찾는다.
  • 윈도우에 x를 제거한다.

예를 들어, 신규 이벤트가 발생하는 순서에 따라서 윈도우에 보관되는 이벤트는 다음과 같이 달라진다.


신규 이벤트 

윈도우에서 제거되는 이벤트 

윈도우에 포함된 이벤트

N1 (accessTime=100)

-

N1 

N2 (accessTime=2500)

N1,N2 

N3 (accessTime=3400) 

N1 (3400 - 100 > 3000) 

N2,N3 

N4 (accessTime=3700)

-

N2,N3,N4  

N5 (accessTime=5300)

-

N2,N3,N4,N5

N6 (accessTime=5700) 

N2 (5700 - 2500 > 3000) 

N3,N4,N5 

* 새로운 이벤트가 들어와야 윈도우에서 제거되므로, 이 점에 유의해서 사용해야 한다.


외부 시간 배치 윈도우(externally-timed batch window)

외부 시간 배치 윈도우는 이벤트 발생 시점에 표현식을 기준으로 배치를 처리한다. 

select avg(responseTime) as avg from AccessLog.win:ext_timed_batch(accessTime, 2 seconds)


win:ext_timed_batch의 첫 번째 파라미터는 타임 배치를 처리할 기준 시간이다. 다음은 위 EPL을 실행할 때 이벤트 순서에 따라 배치 윈도우에 들어가는 이벤트와 결과 생성을 보여주고 있다.


신규 이벤트 

배치 윈도우

결과 생성

N1 (accessTime=100)

N1


N2 (accessTime=2500)

N2 

N1의 결과

N3 (accessTime=3400) 

N2,N3


N4 (accessTime=3700)

N2,N3,N4


N5 (accessTime=5300)

N5

N2,N3,N4의 결과

N6 (accessTime=5700) 

N5,N6


N7 (accessTime=7400)

N7 

N5,N6의 결과 

* 새로운 이벤트가 들어와야 윈도우에서 제거되므로, 이 점에 유의해서 사용해야 한다.


시간 누적 윈도우(time-accumulating window)


시간 누적 윈도우는 지정한 시간 안에 이벤트가 들어오지 않으면 윈도우에서 이벤트를 제거한다. 예를 들어, 아래 코드는 마지막 이벤트가 들어온지 2초 동안 이벤트가 들어오지 않으면 윈도우에서 이벤트를 제거한다.


select rstream * from AccessLog.win:time_accum(2 sec)


최초 길이/시간 윈도우(first length, first time)


최초 n개의 이벤트만 윈도우에 유지하거나 시작 이후 지정한 시간 동안의 이벤트만 윈도우에 유지하고 싶다면, 다음의 두 윈도우를 사용한다.


select * from AccessLog.win:firstlength(10)


select * from AccessLog.win:firsttime(2 sec)



2. 표준 뷰


Unique 뷰 / Firstunique 뷰

Unique 뷰는 지정한 식을 기준으로 단일 값을 보관한다. 예를 들어, 아래 코드를 보자.

select * from StockTick.std:unique(code)

위 EPL은 같은 code 값을 가지는 이벤트 중 가장 마지막 이벤트를 포함하는 뷰를 생성한다. 예를 들어, 이벤트의 code 값에 따라 뷰에 포함되는 이벤트는 다음과 같이 달라진다.

발생 이벤트 

뷰에 포함된 이벤트 

S1(code='1')

S1 

S2(code='1')

S2

S3(code='2')

S1,S3

S4(code='1')

S3,S4

S5(code='2')

S4,S5

S6(code='2')

S4,S6


Firstunique 뷰는 지정한 식을 기준으로 단일 값 중 최초 이벤트를 보관한다. (Unique 뷰는 단일 값 중 마지막 이벤트를 보관하는 것과 다르다.)


select * from StockTick.std:firstunique(code)



그룹 데이터 윈도우

그룹 데이터 윈도우는 지정한 표현식을 기준으로 그룹핑 된 데이터 윈도우를 생성한다. 앞서 살펴본 뷰와 차이점이 있다면 반드시 다른 뷰를 서브 뷰로 조합해서 사용해야 한다는 점이다. 예를 들어, 다음의 뷰를 보자.

select code, avg(cost) as avg from StockTick.std:groupwin(code).win:length(3)

위 EPL은 code 프로퍼티를 이용해서 이벤트를 그룹핑한다. 그리고, 각 그룹별로 길이가 3인 서브 뷰를 만든다. 즉, 각 code 별로 길이가 3인 뷰가 만들어지는 것이다. 그런데, 위 EPL에서 주의할 점은 뷰만 그룹으로 생성된다는 거지, avg 연산자 그룹별로 되는 것은 아니라는 점이다.

실제 그룹별로 값을 구하려면 다음과 같이 group by 를 함께 사용해줘야 한다. 

select code, avg(cost) as avg from StockTick.std:groupwin(code).win:length(3)
group by code


3. 통계 뷰

단변량(Univariate) 통계

단변량 통계 뷰는 숫자 프로퍼티에 대한 통계 데이터를 제공하는 뷰다. 다음은 사용 예이다.

select average from StockTick.stat:uni(cost)

stat:uni()는 숫자 식을 첫 번째 파라미터로 갖는다. 위 코드의 경우 cost 프로퍼티를 값으로 갖는다. 위 EPL에서 select 절에 있는 average는 stat:uni 뷰가 제공하는 프로퍼티로서, 평균 값을 제공하는데 사용된다. average 프로퍼티 외에 stat:uni 뷰가 제공하는 프로퍼티는 다음과 같다.

  • datapoints: 값의 개수
  • total: 값의 합
  • average: 값의 평균
  • variance: 분산
  • stddev: 샘플 표준편차
  • stddevpa: 표준편차

단변량 통계 뷰는 다른 뷰와 함께 사용될 수 있다. 예를 들어, 다음 코드는 code로 그룹핑 한 뷰의 서브 뷰로 stat:uni 뷰를 사용했는데, 이 경우 average는 code 그룹 별로 cost의 평균이 된다.


select average from StockTick.std:groupwin(code).stat:uni(cost)


stat:uni가 제공하는 프로퍼티 외에 다른 프로퍼티(예, 이벤트의 프로퍼티)를 사용하려면 다음과 같다. stat:uni 뷰에 파라미터를 추가로 지정해주면 된다.


select avergae, code from StockTick.std:groupwin(code).stat:uni(cost, code)

select * from StockTick.std:groupwin(code).stat:uni(cost, code, price)


select 절에서 '*'을 사용하면 stat:uni가 제공하는 프로퍼티 및 두 번째 이후에 지정한 파라미터를 선택한다.


회귀Regression 뷰


stat:linest 뷰를 이용하면 두 식의 선형회귀를 구할 수 있다. 다음 식은 price와 total의 회귀 연산을 해서 회귀선의 기울기를 구한다.


select slope from StockTick.win.time(30 min).std:linest(cost, total)


std:linest()는 slope를 포함해서 다음의 프로퍼티를 제공한다.

  • slope: 기울기
  • YIntercept: Y 절편
  • XSum, YSum, sumX, sumY: X, Y의 합
  • sumXY: X*Y의 합
  • sumXSq, sumYSq: X 제곱의 합, Y 제곱의 합
  • XAverage, YAverage: X, Y 평균
  • XVariance, YVariance: X, Y 분산
  • XStandardDeviationPop, YStandardDeviationPop: X, Y 표준 편차
  • XStandardDeviationSample, YStandardDeviationSample: X, Y 샘플 표준 편차

상관계수 뷰


stat:correl 뷰는 두 식 간의 상관계수를 구한다.


select correlation from StockTick.stat:correl(cost, total)


4.확장 뷰


정렬 윈도우 뷰


특정 식을 기준으로 정렬 기준으로 상위 n개의 이벤트만 유지하고 싶을 때 ext:sort 뷰를 사용한다. 다음은 ext:sort 뷰의 예이다.


select sum(total) from StockTick.ext:sort(10, total desc)

select sum(total) from StockTick.ext:sort(10, total desc, timestamp asc)


ext:sort() 첫 번째 파라미터로 정렬된 상태로 보관할 이벤트 개수를 지정하고, 두 번째 이후로는 정렬 기준으로 지정한다. 위 코드에서 첫 번째 EPL은 total 프로퍼티 값이 큰 상위 10개 StockTick 이벤트를 기준으로 sum(total)을 생성하게 된다. 정렬 기준으로 두 개 이상 지정하고 싶다면, 각 정렬 기준을 콤마로 구분해서 지정하면 된다.


랭킹 뷰


랭킹 뷰는 정렬 윈도우 뷰와 유사하다. 차이점이 있다면, 랭키 뷰는 유일식을 기준으로 한 개의 이벤트만 유지한다는 점이다. 예를 들어, 다음 EPL을 보자.


select sum(total) from StockTick.ext:rank(code, 10, price desc)


이 EPL은 price 기준으로 상위 10개의 StockTicke 이벤트를 뷰에 보관한다. 그런데, code 값이 같은 이벤트는 한 개만 보관한다. 예를 들어, 다음의 순서로 이벤트가 발생했다고 하면,

  • S1(code='C1', price=1000) -> S2(code='C2', price=800) -> S3(code='C1', price=1200)
뷰에 남는 이벤트는 S2와 S3가 된다. S1의 price 값이 S2보다 크지만, S3와 S1이 같은 code 값을 갖기 때문에 랭킹 뷰에는 S3만 남게 된다.


Posted by 최범균 madvirus

댓글을 달아 주세요

관련 글


컨텍스트


주식 종목 별로 최근 10분 동안의 평균 가격을 구하려면 다음과 같이 group by 사용햇다.


select code, avg(cost) as avg from StockTick.win:time(10 min) group by code


컨텍스트를 만들면 group by를 사용하지 않고 이벤트를 분류해서 분류된 파티션 별로 결과를 만들어 낼 수 있다. 컨텍스트는 이벤트를 컨텍스트 파티션으로 분류해 주며, EPL에 컨텍스트를 적용하면, 한 개의 EPL을 컨텍스트 파티션 별로 적용할 수 있다. 따라서, 주식 코드 별로 컨텍스트 파티션을 생성하고, 이 컨텍스트를 이용해서 EPL을 실행하면 group by 등의 쿼리를 사용하지 않아도 종목 별로 평균이나 추이 등을 분석할 수 있다.


컨텍스트 만들고 사용하기


컨텍스트는 다음과 같은 문장을 이용해서 생성한다.


epService.getEPAdministrator().createEPL(

       "create context CodeSegment partition by code from StockTick");


위 코드는 StockTick 이벤트를 code 값으로 분류하는 CodeSegment라는 컨텍스트를 생성한다. 값으로 파티션을 생성하는 방식과 해시값으로 파티션을 생성하는 방식 등 몇 가지 종류의 컨텍스트를 제공하는데, 이에 대한 내용은 뒤에서 다시 정리해 본다.


파티션을 생성했다면, EPL에서 다음과 같이 context 절을 이용해서 컨텍스트를 적용할 수 있다.


EPStatement eps = epService.getEPAdministrator().createEPL(

        "context CodeSegment " +

        "select code, avg(cost) as avg from StockTick.win:time(3 sec) ");


위 코드는 CodeSegment 컨텍스트를 사용하는데, 이 경우 각 EPL은 code 값으로 분류된 파티션 별로 적용된다. 따라서, 위 EPL의 select는 각 code 별로 최근 3초 시간 윈도우의 평균 값을 생성한다.


컨텍스트 종류

  • 키 기반 컨텍스트
  • 해시 키반 컨텍스트
  • 카테고리 컨텍스트
  • 논오버래핑 컨텍스트
  • 오버래핑 컨텍스트


컨텍스트 종류: 키 기반 컨텍스트


키 기반 컨텍스트는 이벤트의 특정 프로퍼티를 이용해서 이벤트를 파티션으로 분류한다. partition by 를 이용해서 파티션 키를 지정한다.


create context CodeSegment partition by code from StockTick


두 개 이상의 키를 사용할 수도 있다.


create context AccessLogSegment partition by domain and sesessionId from AccessLog


여러 이벤트를 이용해서 컨텍스트를 생성할 수도 있다.


create context CodeSegment2 partition by 

compCode from Announcement, code from StockTick


위 컨텍스트는 StockTick 이벤트와 Announcement 이벤트를 이용해서 컨텍스트를 생성한다. 각 이벤트에서 지정한 프로퍼티 개수와 타입은 동일해야 한다. 위 컨텍스트 내에서 실행되는 EPL은 코드는 같은 회사 코드를 갖는 주가와 공시 이벤트를 묶어서 처리할 수 있게 된다. 예를 들어, 아래 EPL에서 StockTick 이벤트와 Announcement 이벤트는 이미 동일한 회사 코드를 갖고 있으므로, code와 compCode가 같은지 여부를 비교할 필요가 없다.


context CodeSegment

select s from Announcement.win:time(10 min) a, StockTick t

where t.rate > 10


컨텍스트 종류: 해시 기반 컨텍스트


해시 기반 컨텍스트는 지정한 프로퍼티의 해시 값을 이용해서 이벤트를 분류한다. 해시 기반 컨텍스트를 사용하려면 다음과 같이 coalesce by와 해시 함수를 함께 사용하면 된다.


create context CodeSegment coalesce by

consistent_hash_crc32(code) from StockTick granularity 32

preallocate


consistent_hash_crc32는 CRC 32 알고리즘을 이용해서 해시 코드를 생성한다. hash_code를 사용하면 자바의 해시 코드를 이용한다. granularity는 파티션의 최대 개수를 지정하며, preallocate를 사용하면 미리 파티션을 생성해 놓는다. 키 기반 컨텍스트와 마찬가지로 여러 이벤트를 이용해서 정의할 수 있다.


위 코드를 이용해서 생성한 해시 기반 컨텍스트는 하나의 파티션에 한 개 이상의 code가 존재하게 됨에 유의하자.


컨텍스트 종류: 카테고리 컨텍스트


카테고리 컨텍스트는 프로퍼티를 이용해서 카테고리를 생성하고 이를 기준으로 분류한다. 다음은 카테고리 컨텍스트의 생성 예를 보여주고 있다. "group 표현식 as 카테고리이름"을 이용해서 카테고리를 정의한다.


create context AccessLogCategory

group responseTime <= 1000 as normal,

group responseTime > 1000 and responseTime <= 2000 as slow,

group responseTime > 2000 as tooslow

from AccessLog


카테고리 기반 컨텍스트는 이벤트를 몇 개의 카테고리로 나눈다. 위 코드는 3개의 파티션을 생성하는데, 이벤트는 응답 시간에 따라 normal 파티션, slow 파티션, tooslow 파티션에 속하게 된다.


이 컨텍스트를 사용하는 EPL은 다음과 같이 context.label을 이용해서 카테고리 이름을 구할 수 있다.


context AccessLogCategory

select context.label from AccessLog


컨텍스트 종류: 논오버래핑 컨텍스트


논오버래핑 컨텍스트는 시작과 끝 조건에 따라 컨텍스트가 시작되거나 끝나는 컨텍스트이다. 컨텍스트가 시작되면 끝 조건 전까지 컨텍스트 파티션은 1개만 존재한다. 끝 조건을 충족해서 컨텍스트가 끝나면 파티션은 0개가 된다.


논오버래핑 컨텍스트는 start와 end를 이용해서 시작 조건과 끝 조건을 지정한다. 시작 조건과 끝 조건에는 이벤트, 패턴, 그론탭, 시간 간격 등이 올 수 있다. 다음은 레퍼런스 문서에 있는 몇 가지 예이다.

  • create context NineToFive start (0, 9, *, *, *) end (0, 17, *, *, *)
    크론탭 표현식을 이용한 시작/종료 시점을 지정한다. 9시에 컨텍스트를 시작하고 17시에 끝낸다.
  • create context PowerOutage start PowerOutageEvent end pattern [PowerOnEvent -> timer:interval(5 sec)]
    PowerOutageEvent가 발생하면 컨텍스트를 시작하고, PowerOnEvent 발생 후 5 초가 지나면 끝낸다.
  • create context Every15minutes start @now end after 15 min
    @now는 지금을 의미하는 어노테이션으로, 컨텍스트는 지금 시작해서 15분 후에 끝낸다. 컨텍스트가 종료되면 @now에 의해 다시 컨텍스트가 바로 시작.
시작 조건은 @now 또는 크론탭이나 이벤트 필터 등의 표현식이 온다.

EPL에서는 context.startTime과 context.endTime을 이용해서 컨텍스트의 시작 시작과 끝 시간을 구할 수 있다.

컨텍스트 종류: 오버래핑 컨텍스트


오버래핑 컨텍스트는 시작과 종료 조건을 지정하는 건 논오버래핑 컨텍스트와 같다. 차이점이 있다면, 오버래핑 컨텍스트는 시작 조건을 충족할 때 마다 새로운 컨텍스트 파티션을 생성한다는 점이다. initiated와 terminated를 이용해서 컨텍스트 시작과 종료 조건을 지정한다. 다음은 레퍼런스 문서에 있는 몇 가지 예이다.

  • create context CtxTrainEnter initiated TrainEventEnter as te terminated after 5 min
    TrainEventEnter 이벤트가 들어올 때 마다 새로운 컨텍스트를 시작하고, 5분이 지나면 해당 컨텍스트를 종료한다.
  • create context CtxEachMinute initiated @now and pattern [every timer:interval(1 min)] terminated after 1 min
    "@now and"는 컨텍스트를 즉각 시작하고, 시작 조건이 충족되면 새로운 컨텍스트를 시작한다. 위 EPL은 컨텍스트를 즉각 시작하고, (1분이 지나면 조건을 충족하므로) 매 1분이 지날 때 마다 컨텍스트를 시작한다. 각 컨텍스트는 1분 이후에 종료된다.
  • create context OrderContext initiated distinct(orderId) NewOrderEvent as newOrder terminated CloseOrderEvent(closeOrderid = newOrder.orderId)
    NewOrderEvent가 들어오면 컨텍스트를 시작한다. 단, orderId를 기준으로 컨텍스트가 이미 존재하면 새로운 컨텍스트는 시작되지 않는다.

컨텍스트 종료시 결과 생성하기


output을 사용하면 컨텍스트가 종료될 때 결과를 생성할 수 있다. 다음처럼 output - when terminated 구문을 사용하면 된다.


context CtxEachMinute select avg(temp) from sensorEvent output snapshot when terminiated



컨텍스트를 사용할 때의 장점


레퍼런스 문서에 따르면 컨텍스트 사용시 다음과 같은 장점이 있다고 한다.

  • 하나의 컨텍스트를 여러 EPL에 적용할 수 있으므로, 그룹핑하기 위한 중복 부분을 제거할 수 있다.
  • EPL을 더 읽기 쉽게 만들어준다.
  • 두 개 이상의 컨텍스트를 조합(중첩)할 수 있다.
  • 파티션이 시간 상 겹칠 수 있다.
  • 엔진이 컨텍스트 단위로 락을 관리하기 때문에 동시성을 높일 수 있다.


Posted by 최범균 madvirus

댓글을 달아 주세요


이래 저래 데이터 처리 관련된 기술을 조사하던 중 Esper란 놈을 알게 되었다. 몇 가지 글을 읽어보니 기회가 되면 사용해보고 싶은 욕구가 생겼다. 나처럼 Esper에 관심있는 분들을 위한 퀵스타트 문서를 정리해 본다.


관련 시리즈:


Esper란?


Esper 사이트에 따르면 Esper란 다음과 같은 것이다.

Esper is a component for complex event processing (CEP) and event series analysis.

Esper는 실시간으로 발생하는 이벤트를 분석하고 처리하기 위한 컴포넌트로서, 다음과 같은 방식으로 동작한다.


외부에서 발생한 이벤트를 Esper 엔진에 전달하면, 이벤트를 분석한다. 이벤트를 분석할 때 EPL 이라는 언어를 사용하는데, 이 언어를 이용해서 조건에 맞는 이벤트를 찾고 처리한 결과를 데이터를 생성한다.


EPL 언어는 SQL과 유사한 구조를 갖고 있기 때문에 조금만 노력하면 금방 익힐 수 있을 것 같다. EPL과 SQL을 비교해 보면, SQL에 존재하는 데이터에 대해 쿼리를 실행하는 방식이라면 EPL은 실시간으로 발생되는 이벤트에 대해 쿼리를 실행하는 방식이라고 말 할 수 있다.


Esper 다운로드


Esper를 사용하려면 Esper를 다운로드 받고 필요한 jar 파일을 복사하면 된다. 그런데, Esper는 EPL 파싱을 위해 Antlr을 사용하고 로깅을 위해 Commons Logging을 사용하고 있기 때문에 실제로는 Esper 배포판뿐만 아니라 의존 모듈도 함께 다운로드 받아야 한다.


메이븐을 사용하고 있다면 다음과 같이 의존을 추가해주면 된다.


<dependencies>

<dependency>

<groupId>com.espertech</groupId>

<artifactId>esper</artifactId>

<version>4.11.0</version>

</dependency>

</dependencies>


간단한 예제 만들어보기


여기서 만들어 볼 예제는다음과 같은 간단한 예제이다.

  • 현재가가 전날 종가대비 10% 이상 오른 종목을 통지한다.

이벤트로 사용될 클래스


먼저 할 작업은 이벤트로 사용될 클래스를 작성하는 것이다.


public class StockTick {


    private String name;

    private String code;

    private int cost;

    private int fluctuation;

    private double rate;


    public StockTick(String name, String code, int cost, int fluctuation, double rate) {

        this.name = name;

        this.code = code;

        this.cost = cost;

        this.fluctuation = fluctuation;

        this.rate = rate;

    }


    public String getName() {

        return name;

    }


    public String getCode() {

        return code;

    }


    public int getCost() {

        return cost;

    }


    public int getFluctuation() {

        return fluctuation;

    }


    public double getRate() {

        return rate;

    }

}


위 코드는 한 종목의 이름, 코드, 현재주가, 전일비, 등낙율을 담고 있다.


조건을 충족하는 종목을 찾아주는 코드


아래 코드는 Esper를 이용해서 전날 종가보다 10% 이상 상승한 종목을 출력해주는 기능을 제공하는 클래스이다.


import com.espertech.esper.client.*;


public class StockFinder {

    private EPServiceProvider epService;

    private EPStatement eps;

    private StockFoundListener listener;


    public void setup() {

        Configuration config = new Configuration();

        // 1. StockTick 클래스를 Esper가 사용할 이벤트 타입으로 등록

        config.addEventType("StockTick", StockTick.class);


        // 2. config를 이용해서 EPService 생성

        epService = EPServiceProviderManager.getProvider("StockTick", config);


        // 3. epService를 이용해서 EPL 생성

        eps = epService.getEPAdministrator().createEPL(

                "select * from StockTick t where t.rate >= 10");


        // 4. EPL의 결과를 받는 리스너 등록

        eps.addListener(new UpdateListener() {

            @Override

            public void update(EventBean[] newEvents, EventBean[] oldEvents) {

                StockTick stockTick = (StockTick) newEvents[0].getUnderlying();

                if (listener != null) listener.found(stockTick);

            }

        });

    }


    public void setStockFoundListener(StockFoundListener listener) {

        this.listener = listener;

    }


    public void sendStockTick(StockTick tick) {

        // 5. EP런타임에 이벤트 전달

        epService.getEPRuntime().sendEvent(tick);

    }

}


// StockFoundListener

public interface StockFoundListener {

    public void found(StockTick stockTick);

}


위 코드에서 3번 항목의 EPL은 아래와 같은데, 이를 보면 SQL과 유사한 것을 알 수 있다.


select * from StockTick t where t.rate >= 10


참고로, 이 쿼리는 앞서 1번 과정에서 등록한 StockTick 이벤트에 대해 그 이벤트의 rate 값이 10 보다 크거나 같으면 선택하라는 의미이다.


EPL에 의해 선택된 이벤트에 접근할 때 사용되는 것이 리스너이다. Esper는 EPL에 의해 선택된 데이터를 등록된 UpdateListener에 전달한다. 위 코드의 경우 3번 과정의 EPL에서 생성한 결과를 4번에서 등록한 listener에 StockTick 객체를 전달하도록 했다.


초기화를 했다면 EPRuntime의 sendEvent() 메서드를 이용해서 이벤트를 Esper에 전달하면 된다. 위 코드의 경우 다른 부분에서 지속적으로 주가 데이터를 읽어와 StockTick 객체를 생성한 뒤에 StockFinder의 sendStockTick() 메서드를 이용해서 StockTick 객체를 Esper 런타임에 전달하게 될 것이다.


간단 테스트 코드


실제 원하는 대로 동작하는지 아주 간단한 테스트 코드를 만들어보자.


public class StockFinderTest {


    private final StockFinder stockFinder = new StockFinder();


    private StockTick lastFound = null;

    private StockFoundListener listener = new StockFoundListener() {

        @Override

        public void found(StockTick stockTick) {

            lastFound = stockTick;

        }

    };


    @Before

    public void setup() {

        stockFinder.setup();

        stockFinder.setStockFoundListener(listener);

    }


    @Test

    public void shouldFound() {

        StockTick tick1 = new StockTick("name", "code", 109, 9, 9.0);

        stockFinder.sendStockTick(tick1);

        assertThat(lastFound, nullValue());


        StockTick tick2 = new StockTick("name", "code", 110, 10, 10.0);

        stockFinder.sendStockTick(tick2);

        assertThat(lastFound, equalTo(tick2));

    }



위 코드에서 listener 필드는 StockFinderListener 구현 객체를 갖는데, 이 객체는 found() 메서드에 전달된 stockTick 객체를 lastFound 필드에 할당한다. 따라서, StockFinder가 10% 이상 상승한 종목을 찾아서 통지를 하면 lastFound 필드에 그 StockTick 객체가 할당된다.


테스트 메서드에서 tick1은 전날 종가 대비 등락율이 9%이므로 StockFinder는 통지하지 않는다. 따라서, lastFound는 null 이어야 한다. 반면에 tick2는 등락율이 10%이므로 StockFinder가 통지하게 되고, 이에 따라 lastFound 필드에 tick2 객체가 전달된다. 따라서, lastFound와 tick2는 같아야 한다.


아주 약간 흥미를 더한 코드 만들기


단순히 어제 종가 대비 10% 상승한 종목을 찾는 일은 Esper를 사용하지 않아도 쉽게 할 수 있는 일이다. 단순히 이런 걸 하려고 Esper를 사용하진 않을 것이다. 실제로 Esper는 복잡한 상황과 조건을 다룰 수 있는 EPL을 제공하고 있다. 예를 들어, 최근 5초 동안 주가가 5% 이상 상승한 종목을 알고 싶다면 다음과 같은 EPL을 사용할 수 있다.


select first(*) as tick1, last(*) as tick2 

from StockTick.win:time(5 seconds) 

group by code 

having first(*) != last(*) and (last(cost) - first(cost)) / first(cost) >= 0.05


위 EPL을 간단히 설명하면

  • 2줄: StockTick 이벤트를 최근 5초 기준으로
  • 3줄: code를 이용해서 그룹핑 하고,
  • 4줄: 그룹에서 최근 5초 이내 첫 번째와 마지막 이벤트가 다르고, 첫 번째 이벤트의 cost보다 마지막 이벤트의 cost가 5% 이상 값이 크면,
  • 1줄: 그룹의 첫 번째 이벤트와 마지막 이벤트를 선택한다
위 EPL로부터 결과를 받는 UpdateListener는 다음과 같은 코드를 이용해서 최근 5초 동안 5% 이상 상승한 종목을 구할 수 있다.

eps = epService.getEPAdministrator().createEPL(
        "select first(*) as tick1, last(*) as tick2 from StockTick.win:time(5 seconds) " +
        "group by code having first(*) != last(*) and (last(cost) - first(cost)) / first(cost) > 0.05"
);
eps.addListener(new UpdateListener() {
    @Override
    public void update(EventBean[] newEvents, EventBean[] oldEvents) {
        StockTick tick1 = (StockTick) newEvents[0].get("tick1");
        StockTick tick2 = (StockTick) newEvents[0].get("tick2");
        ... 필요한 작업
    }
});

StockTick의 code 값을 이용해서 그룹핑을 하는데, code값이 각각 "code1"과 "code2"인 StockEvent가 아래 그림고 같은 순서로 발생했다고 하자. (시간은 초, code1 및 code2는 해당 초에 발생한 이벤트의 cost 값, 결과는 UpdateListener에 전달된 tick1과 tick2의 값이다.



위 그림에서, 5초가 흐른 시점에 code 값이 "code2"인 StockEvent가 발생을 했고, 이 이벤트의 cose 값은 302 이다. 즉, 최초 2초가 흐름 시점에는 code1과 관련된 2개의 이벤트가 발생했고, 첫 번째 이벤트의 cost는 100, 마지막 이벤트의 cost는 109이므로 5% 이상 상승한 값이다. 그래서 UpdateListener는 0초에 발생한 이벤트와 2초에 발생한 이벤트를 각각 tick1과 tick2로 받는다.


이와 비슷한 방식으로 11초 기준으로 "code2" 관련 StockEvent가 발생하는데, 이 시점에 최근 5초 동안 "code2"의 시작 이벤트는 7초 시점의 305 이벤트이고, 마지막 이벤트는 11초 시점의 323 이벤트이다. 323은 305보다 5% 이상 큰 값이므로, 결과로 305 이벤트와 323 이벤트가 선택된다.

참고자료

Esper를 이용하면 두 종류의 이벤트를 병합한다거나 조인을 하거나 특정 패턴을 찾아내는 등 다양한 방식으로 이벤트를 처리할 수 있다. 또한, 필요에 따라 외부 데이터를 함께 사용할 수도 있다. 이런 다양한 정보를 얻는 가장 좋은 방법은 레퍼런스 문서를 읽어보는 것이다. Esper에 관심이 있다면, http://esper.codehaus.org/ 사이트에서 레퍼런스 문서를 구해 차분히 읽어 보면 도움을 얻을 것이다.



Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 춥고배고파 2014.03.09 23:39 신고  댓글주소  수정/삭제  댓글쓰기

    StockFinderListener class 는 어디에 있는건가요?

  2. 춥고배고파 2014.03.10 22:11 신고  댓글주소  수정/삭제  댓글쓰기

    눈이 이상해서 그런지 찾지 못하겠네요 ㅠ..ㅠ

    • 최범균 madvirus 2014.03.11 08:59 신고  댓글주소  수정/삭제

      아,,, StockFinderListener는 인터페이스인데, 제가 그 인터페이스 코드 자체를 넣진 않았네요. StockFinderTest 클래스의 listener 필드에 다음과 같이 임의 객체를 생성하는 부분에서, StockFinderListener 인터페이슨느 found() 메서드 하나만 정의하고 있는 인터페이스입니다.

      private StockFinderListener listener = new StockFinderListener() {
      @Override
      public void found(StockTick stockTick) {
      lastFound = stockTick;
      }
      };

      즉, StockFinderListener는 다음과 같습니다.

      public interface StockFinderListener {
      public void found(StockTick stockTick);
      }

  3. 흠... 2014.03.12 16:52 신고  댓글주소  수정/삭제  댓글쓰기

    StockFoundListener클래스도 보이지 않네요! 어디에서 찾아야하나요~?