회원 정보 목록 화면과 회원 정보 상세 화면을 반복해서 왔다 갔다 하는 직원이 있다고 하자. 또, 특정 조건으로 회원 목록을 검색해서 1페이지, 2페이지, 3페이지 등 페이지 이동을 하는 직원이 있다고 하자. 이 두 경우 회원 정보를 지속적으로 조회하는 것으로서, 회원 정보를 몰래 수집하는 이상 행동으로 의심해 볼 수 있다. 이런 이상 행동에는 일정 패턴이 반복되는 경우가 많은데, 이런 상황을 발견할 때 유용하게 사용할 수 있는 것이 EPL 패턴이다.
관련 글:
- Esper 초보 시리즈 1 - 퀵스타트
- Esper 초보 시리즈 2 - EPL 기초
- Esper 초보 시리즈 3 - Output을 이용한 출력 제어
- Esper 초보 시리즈 4 - Insert into, 조인, 서브쿼리
- Esper 초보 시리즈 6 - 컨텍스트
EPL 패턴
EPL은 발생하는 이벤트들이 특정 패턴에 매칭되는지 찾아준다. 다음은 매우 간단한 EPL 패턴의 사용 예다.
EPStatement eps = epService.getEPAdministrator().createEPL(
"select l from pattern[every l=List ]"
);
eps.addListener(new UpdateListener() {
@Override
public void update(EventBean[] newEvents, EventBean[] oldEvents) {
MemberList l = (MemberList)newEvents[0].get("l"));
...
}
});
위 코드에서 from 절 뒤에 "pattern [ 패턴 ]" 형식으로 패턴을 지정하고 있다. 위 패턴은 매우 단순한 패턴으로, 모든 List 이벤트에 대해 매칭되는 패턴이다. 위 EPL을 실행하면 List 이벤트가 발생할 때 마다 UpdateListener가 실행된다.
위 패턴은 "select l from List l" EPL과 동일한 결과를 발생시키는데, 실제 위와 같이 단순한 패턴을 사용하지는 않는다. EPL은 다양한 패턴 연산자와 구성 요소를 제공하고 있으며 이들을 조합해서 흥미로운 패턴을 만들어낼 수 있다.
패턴의 구성 Atom과 연산자
패턴을 구성하는 주요 Atom(EPL 레퍼런스 문서를 보면 Atom이라고 부름)에는 다음의 두 가지가 있다.
패턴 Atom |
예 |
이벤트 필터 Atom |
이벤트: List 조건에 맞는 이벤트: List(userId = 'madvirus') |
시간 기반 Atom |
타이머: timer:interval(10 sec) 스케줄: timer:at(*, 6, *, *, *) |
Atom에 대한 연산자는 다음의 4가지 종류가 존재한다.
종류 |
연산자 |
반복을 제어하는 연산자 |
every, [num], until 등 |
논리 연산자 |
and, or, not |
이벤트의 발생 순서 |
-> |
표현식 유효 조건 |
where timer:within, where timer:withinmax, while 등 |
연산자의 우선순위는 다음과 같다.
- 조건 접미사: where timer:within, where timer:withinmax, while
- 단항 연산자: every, not, every distinct
- 반복: [num], until
- and
- or
- 순서: ->
연산자 우선순위를 이해하지 않으면 패턴을 다른 의미로 읽거나 작성하게 되므로, 우선순위에 유의해서 패턴을 작성해야 한다.
이벤트 필터
다음 EPL을 보자.
select L from pattern [L=List]
위 EPL에서는 List는 이벤트를 위한 Atom 이다. 패턴에 명시한 이벤트를 참조하려면 '태그이름=이벤트Atom'의 형식으로 태그를 사용해야 한다. 위 코드에서는 'L'이 태그가 된다.
위 EPL을 실행하면 최초에 발생한 List 이벤트만 패칭되며, 이후 발생하는 List 이벤트에 대해서는 매칭되지 않는다. 따라서, List 타입 이벤트가 L1, L2, L3의 순서로 발생하면 L1 이벤트에 대해서만 UpdateListener를 통해 통지 받을 수 있다.
모든 List 이벤트에 대해 매칭되려면 every 연산자를 사용해야 한다.
select L from pattern [every L=List]
위 EPL은 모든 List 이벤트에 매칭된다.
특정 조건을 만족하는 이벤트를 걸러내고 싶다면 다음과 같이 필터 구문을 사용하면 된다.
select L from pattern [every L=List(uri='/member/list', userId='bkchoi')]
"->"를 이용한 발생 순서 매칭
-> 연산자는 매우 유용한 연산자이다. 이벤트의 발생 순서를 지정할 때 -> 연산자를 사용할 수 있다. 아래 패턴은 List 이벤트 다음에 Detail 이벤트가 발생한 경우 매칭된다.
select L, D from pattern [L=List -> D=Detail]
다음은 이벤트 순서와 위 패턴에 매칭되어 선택된 결과를 표시한 것이다.
- L1, L2, D1, D2, L3, D3 : (L1, D1)
위 결과를 보면 첫 번째로 패턴에 매칭된 이벤트 집합(L1, D1)만 선택된 것을 알 수 있다.
모든 이벤트 List 이벤트에 대해 매칭되도록 하려면 every 연산자를 사용하면 된다.
select L, D from pattern [every L=List -> D=Detail]
위 패턴은 모든 List 이벤트에 대해 뒤에 Detail이 오면 매칭된다. every 연산자가 -> 보다 우선순위가 높으므로 위 패턴은 [(every L=List) -> D=Detail]과 같다. 다음은 이벤트 흐름에 대해 매칭되어 선택되는 이벤트 집합을 표시한 것이다.
- L1, L2, D1, D2, L3, D3 : (L1, D1), (L2, D1), (L3, D3)
위 매칭 결과를 보면 (L1, D1)과 (L2, D1)이 선택된 것을 알 수 있다. L1과 L2에 대해 D1이 모두 매칭됐는데, 겹치는 경우 매칭되지 않도록 하고 싶다면 다음과 같이 @SuppressOverlappingMatches 패턴 어노테이션을 사용한다.
select L, D from pattern @SuppressOverlappingMatches [every L=List -> D=Detail]
위 패턴을 사용하면 이벤트 발생 순서에 따라 다음과 같이 (L1, D1)만 선택되고, (L1, D1)과 겹쳐지는 (L2, D1)은 매칭되지 않는다.
- L1, L2, D1, D2, L3, D3 : (L1, D1), (L3, D3)
-> 연산자를 사용할 때 주의할 점은 -> 연산자 앞 뒤에 지정한 이벤트가 반드시 바로 직후에 발생되야 하는 것은 아니라는 점이다. 예를 들어, 다음과 같은 패턴과 이벤트 순서를 생각해보자.
- 패턴: select L, D from pattern [every L=List -> D=Detail]
- 이벤트: L1, V1, D1, D2, L3, D3
and와 or 연산자
and는 순서에 상관없이 두 표현식이 모두 true면 매칭된다. 예를 들어, 다음 패턴은 List 이벤트와 Detail 이벤트가 도착하면 매칭된다.
select L, D from pattern [L=List and D=Detail]
순서가 상관이 없기 때문에 다음의 두 순서로 이벤트가 들어와도 매칭되서 선택되는 이벤트는 L2, D1 이다. 한 번만 선택되는 이유는 패턴에 every가 없기 때문이다.
- L2, D1, D2, L3, D3
- D1, L2, L3, D3
- L1, L2, D1, D2, L3, D3
select L, D from pattern [every (L=List and D=Detail)]
- L1, L2, D1, D2, L3, D3
매칭되는 결과 이벤트는 (L1, D1)과 (L3, D2) 이다.
select L, D from pattern [every (L=List or D=Detail)]
not은 표현식이 false인 경우 true가 된다. 처음 Statement가 시작될 때에 "not 표현식"은 true 상태로 시작하며, 표현식이 true가 될 때 false가 된다. not은 단독으로 사용되기 보다는 and와 함께 사용된다. 예를 들어, 발권을 하한 뒤 상영관에 입장하기 전에, 표에 있는 할인 쿠폰으로 간신을 구매하지 않은 사용자를 찾고 싶다고 해 보자. 이는 다음과 같은 패턴으로 찾아낼 수 있다.
select i from pattern [
every i=IssuedTicket -> (Entering(ticketId = i.id) and not UseTicketCoupon(couponId = i.couponId))
]
Every 연산자
every 연산자는 동작 방식을 잘 이해해서 사용해야 한다. every 연산자를 어떻게 사용하느냐에 따라 패턴 매칭 결과가 달라지는데, 이에 대한 설명은 레퍼런스 문서에 잘 나와 있다. 그래서 레퍼런스 문서에 있는 내용을 발췌해서 아래 표로 정리해 본다.
* 이벤트가 A1, B1, C1, B2, A2, D1, A3, B3, E1, A4, F1, B4 순서로 발생했다고 가정
every 사용 예 |
설명 | 매칭 결과 |
every (A -> B) |
A 이벤트 발생 후, B 이벤트가 발생하면 매칭된다. 매칭된 후 새로운 매처(매칭 검사기)를 시작한다. A가 매칭된 후, 다음 B가 나올 때 까지 새로운 매처가 시작되지 않는다. 즉, A 이벤트 이후에 B 이벤트가 발생하기 전까지의 다른 A 이벤트는 매칭 대상에서 제외된다. | (A1, B1), (A2, B3), (A4, B4) |
every A -> B (every A) -> B |
모든 A에 대해, A 이벤트 발생 후 B 이벤트가 발생하면 매칭된다. | (A1, B1), (A2, B3), (A3, B3), (A4, B4) |
A -> every B A -> (every B) |
패턴 Statement가 시작된 뒤, 첫 번째 A 이벤트에 대해 모든 B가 매칭된다. | (A1, B1), (A1, B2), (A1, B3), (A1, B4) |
(every A) -> (every B) |
모든 A 이벤트에 대해, A 이벤트 발생 후 모든 B 이벤트에 매칭된다. | (A1, B1), (A1, B2), (A1, B3), (A1, B4) (A2, B3), (A2, B4) (A3, B3), (A3, B4) (A4, B4) |
반복 연산자
특정 표현식이 지정한 횟수만큼 반복되면 매칭되도록 하고 싶다면 반복 연산자를 사용하면 된다. 반복 연산자는 '[횟수]' 형식으로 사용한다. 아래 코드는 예이다.
EPStatement eps = epService.getEPAdministrator().createEPL(
"select s from pattern [every [2] s=SlowResponse ]"
);
eps.addListener(new UpdateListener() {
@Override
public void update(EventBean[] newEvents, EventBean[] oldEvents) {
for (EventBean eb : newEvents) {
// 길이가 2인 배열
SlowResponse[] responses = (SlowResponse[]) eb.get("s");
...
}
}
});
위 패턴을 사용하면 SlowResponse 이벤트가 2번 발생할 때 마다 매칭된다. 매칭 결과를 받는 리스너의 코드를 보면 "s"에 보관된 값이 SlowResponse 배열임을 알 수 있다.
반복 연산자와 not을 사용하면 다음과 같은 패턴을 만들 수도 있다.
select s, o from pattern [
every [3] ( (s=SlowResponse or o=OvertimeResponse) and not NormalResponse)"
]
위 패턴은 SlowResponse나 OvertimeResponse가 3회 발생하고, 그 사이에 NormalResponse가 발생하지 않은 경우 매칭된다. 즉, 느린 응답이 연속해서 3회 발생하는 상황을 찾아내주는 패턴이다.
반복 연산자와 until
until 연산자는 until 뒤의 표현식이 충족될 때 매칭된다. 아래 패턴은 NormalResponse가 발생하기 전까지 SlowResponse 이벤트를 반복한다.
select s from pattern [ every (s=SlowResponse until NormalResponse) ]
NormalResponse 이벤트 발생 전에 SlowResponse 이벤트가 5번 발생하면 s는 SlowReponse 객체 5개를 갖는 배열이 할당된다.
범위를 갖는 반복 연산자는 다음과 같은 형식을 갖는다.
- [3:8] - 최소 3번에서 최대 8번까지 반복
- [3:] - 최소 3번 반복
- [:8] - 최대 8번 반복
* S1 S2 N1 S3 S4 S5 N2 순서로 이벤트가 발생했다고 가정 (S:Slow, N:Normal)
패턴 예 |
결과 |
every ([2:] s=SlowResponse until NormalResponse) |
(S1, S2) *N1 발생 시점에 생성 (S3, S4, S5) *N2 발생 시점에 생성 |
every ([:3] s=SlowResponse until NormalResponse) |
(S1, S2) *N1 발생 시점에 생성 (S3, S4, S5) *N2 발생 시점에 생성 |
every ([:2] s=SlowResponse until NormalResponse) |
(S1, S2) *N1 발생 시점에 생성 (S3, S4) *N2 발생 시점에 생성 |
every ([3:] s=SlowResponse until NormalResponse) | (S3, S4, S5) *N2 발생 시점에 생성 |
타이머 패턴 가드
where timer:within 가드는 표현식 뒤에 위치하며, 일정 시간 안에 표현식이 true가 되면 매칭되고 지정한 시간이 지나면 매칭되지 않는다. 다음은 타이머 가드의 사용 예이다.
select s from pattern [s=SlowResponse where timer:within(2 sec)]
위 패턴은 최초 2초 안에 SlowResponse 이벤트가 발생하는지 여부를 확인한다. 최초 2초 안에 이벤트가 발생하면 해당 SlowResponse 이벤트가 선택되고 타이머가 종료된다. 2초가 지나면 타이머가 종료되고 더 이상 패턴은 사용되지 않는다.
최근 2초간 발생한 SlowResponse 이벤트를 구하고 싶다면 다음과 같이 every 연산자를 함께 사용한다. 아래 패턴을 실행하면 2초 안에 SlowResponse가 이벤트가 발생할 때 마다 select 결과를 받게 된다.
select s from pattern [(every s=SlowResponse) where timer:within(2 sec)]
아래 코드는 어떤 의미일까?
select s from pattern [every ((s=SlowResponse -> SlowResponse) where timer:within(2 sec))]
위 코드는 최초 시작시 타이머가 시작되어 (SlowResponse -> SlowResponse)가 2초 이내에 발생하는지 여부를 확인한다. 타이머가 종료되면 every 연산자에 의해 새로운 타이머가 시작된다. 위 패턴을 사용했을 때 이벤트 발생 시점과 select 결과는 아래 그림과 같이 이뤄진다. 아래 그림에서 화살표는 타이머의 시작과 끝을 나타낸다.
위 그림과 동일한 시점에 이벤트가 발생했을 때, 아래 코드는 어떤 결과를 만들까?
select s from pattern [every s=SlowResponse -> SlowResponse where timer:within(2 sec)]
// 우선순위에 따라 evern s=SlowResponse -> (SlowResponse where timer:within(2 sec)) 와 동일
이 패턴은 모든 SlowResponse 이벤트가 발생한 후 2초 이내에 SlowResponse 이벤트가 발생하면 매칭된다.
아래 패턴은 어떨까?
select s from pattern [every s=SlowResponse -> (not NormalResponse where timer:within(2 sec)) ]
얼핏 생각하면 SlowResponse 이벤트 발생 후 2초 이내에 NormalResponse가 발생하지 않으면 매칭될 것 같지만, 실제로는 SlowResponse 이벤트가 발생하는 순간에 바로 매칭된다. 실제로 특정 이벤트가 발생하고 나서 일정 시간 안에 다른 이벤트가 발생하지 않는 패턴을 만들고 싶다면 다음에 설명할 time:interval Atom을 사용해야 한다.
시간 Atom
위 코드는 OrderForm 이벤트가 발생한 후, 같은 id를 갖는 OrderComplete 이벤트가 10분 안에 발생하지 않으면 매칭된다. 즉, 주문 양식까지 들어왔는데 10분 동안 결제를 하지 않은 경우를 찾아내는 패턴이다.