event driven
이벤트기반 프로그래밍
사전지식
이벤트 기반 프로그래밍에 앞서 블로킹,논블로깅,동기,비동기를 살펴보자
3가지 측면에서 고려해야 한다
- 어플리케이션 - 네트워크 프로그램(유저 프로세스, 스레드), system call
- 커널(OS) - 어플리케이션의 IO요청을 수행, 송/수신 버퍼
- I/O 작업 - cpu 자원 안씀, device
어플리케이션(프로그래밍) 중심
블로킹/논블로킹
입/출력, 소켓동작 방식 socket 프로그래밍에서 해당 소켓의 I/O Blocking/Non-Blocking 모드를 결정할 수 있으며 프로그래밍 코드도 확연한 차이(다른 객체를 사용)가 있다.
블로킹
요청작업이 성공(하거나 에러가 발생하기) 전까지 응답을 안줌 스레드 대기
IO작업 수행 중 중지 > 별도 스레드 생성 > 컨텍스트스위칭 비용증가 > 비효율적
블로킹 방식이 항상 나쁜 것이 아니다. 장시간 IO를 점유하여 처리하는 케이스(파일전송등)에서는 별도의 블로킹 채널을 생성하여 사용하는 것이 좋을 때도 있다.
논블로킹
요청작업이 성공 여부와 관계없이 결과(티켓) 리턴 작업종료 이벤트 발생시 이벤트를 처리하는 메소드를 미리 등록하여 스레드가 처리하도록 하게 한다.
프로그래밍 방식의 변화
Sync I/O -> Multi Thread -> Thread Pool -> Non-Blocking(selector) -> Event-Driven -> Reactor
동기/비동기
메소드(함수), 서비스 호출 방식
작업완료까지 대기 완료되어야만 결과를 알수 있음. 직관적. 이해가 쉬움
즉시 리턴(결과를 알수 있는 티켓발행으로 대신함) 요청자는 기다리는 시간에 다른 작업 수행, 효율적 자원 사용 필요한 시점에 결과 확인
별도 스레드가 작업을 수행 후 이벤트발생/ 등록된 콜백함수 수행
프로세스와 커널 관계 중심
블로킹
- I/O에서 블로킹 형태의 작업은 유저 프로세스가 커널에게 I/O를 요청하는 함수를 호출하고, 커널이 작업을 완료되면 함수가 작업 결과를 반환한다.
- I/O 작업이 진행되는동안 유저 프로세스는 자신의 작업을 중단한채 대기해야한다.
- I/O작업이 CPU자원을 거의 쓰지 않기 때문에 이런 형태의 I/O는 리소스 낭비가 심하다.
논블로킹
- I/O작업을 진행하는 동안 유저 프로세스의 작업을 중단시키지 않는다.
- 유저 프로세스가 커널에게 I/O를 요청하는 함수( recvfrom 등 )를 호출하면, 함수는 I/O를 요청한 다음 진행상황과 상관없이 바로 결과(EWOULDBLOCK:커널버퍼에 데이터 비었음 또는 recvBuffer에서 데이터복사)를 반환한다.
- 요청에 대한 처리는 I/O의 진행시간과는 관계없이 빠르게 동작하므로 유저프로세스는 중지/대기가 없다
- 따라서 단일 스레드로 여러개의 소켓(세션)을 처리 가능, 그러나 버퍼가 준비되었는지를 계속 확인(recv)해야 한다 > 리소스 남용
I/O 이벤트 통지 모델
Synchronous vs asynchronous는 Non-Blocking I/O Model에서의 통지 모델로 접근한다.
이벤트 통지 모델은 Non-Blocking에서 제기된 문제를 해결하기 위해서 고안 I/O처리를 할 수 있는 소켓(혹은 파일 디스크립터)을 가려내서 가르쳐준다면, 다시말해 입력 버퍼에 데이터가 수신되어서 데이터의 수신(read)이 필요하거나, 출력버퍼가 비어서 데이터의 전송이 가능한 상황(write)을 알려준다면, 위에서 이야기한 구조보다 더 단순하고 효과적으로 다중 I/O모델을 처리할 수 있을 것이다.
I/O 이벤트를 통지하는 방법은 크게 동기형 통지모델과 비동기형 통지모델로 나눌 수 있다.
동기(synchronous)와 비동기(asynchronous)
서로 메시지를 주고받는 상대방이 어떤 방식으로 통신을 하는가에 대한 개념이다. I/O 통지모델에서 대화하는 주체들은 '커널'과 '프로세스'이다. 프로세스는 커널에게 I/O처리를 요청하고, 커널은 프로세스에게 I/O 상황을 통지한다. 우선 I/O 요청은 '프로세스'가 수행하는 것이기에 동기나 비동기나 차이가 없이 동일하다 결국에 '커널'이 '프로세스'에게 어떤 방식으로 요청에 대한 응답을 통지하느냐에 따라 동기/비동기 방식이 결정됨
동기형 통지 모델
유저 프로세스 주체로 IO 이벤트가 있는지 커널에게 주기적으로 확인
동기형 통지모델에서는 '프로세스'가 커널에게 지속적으로 현재 I/O 준비 상황을 체크한다. 즉 커널이 준비되었는지를 계속 확인하여 동기화 하는 것이다. 따라서 동기형 통지모델에서 Notify를 적극적으로 진행하는 주체는 '유저 프로세스'가 되며 커널은 수동적으로 유저 프로세스의 요청에 따라 현재의 상황을 보고한다.
비동기형 통지 모델
커널 주체로 유저프로세스에게 IO 이벤트 통지
이와 반대로 비동기형 통지모델은 일단 커널에게 I/O작업을 맡기면 커널의 작업 진행사항에 대해서 프로세스가 인지할 필요가 없는 상황을 말한다. 유저의 프로세스가 I/O 동기화를 신경쓸 필요가 없기에 비동기형이라고 부를 수 있다. 따라서 비동기형 통지모델에서 Notify의 적극적인 주체는 '커널'이 되며, '유저 프로세스'는 수동적인 입장에서 자신이 할일을 하다가 통지가 오면 그때 I/O 처리를 하게 된다.
리눅스의 IO모델
AIO - 프로세스가 블로킹 또는 요청완료를 기다리지 않고 많은 IO 동작을 수행하게 하도록 하며, 요청한 IO작업이 완료되면 알림을 받고 IO 작업 결과를 가지고 동작함
동기/비동기 - 블로킹/논블로킹에 따라 4가지로 분류
Synchronous blocking I/O : application blocks - the context switches to the kernel - read is then initiated - response returns - the data is moved to the user-space buffer - the application is unblocked
Asynchronous blocking I/O : non-blocking I/O with blocking notifications the blocking select system call(어플리케이션이 block(), select())이 사용됨. 많은 디스크립터의 알림(read가능, write가능, error발생)을 제공
- Synchronous non-blocking I/O : application make numerous calls to await completion, non-blocking is that an I/O command may not be satisfied immediately,
- Asynchronous non-blocking I/O (AIO) : perform other processing, a signal or a thread-based callback can be generated
참조
http://www.ibm.com/developerworks/linux/library/l-async/
그럼 다시 프로그래밍 측면으로 돌아가 보자.
이벤트 기반 프로그래밍
네티는 네트워크 관련 다양한 이벤트를 미리 정의하였다. 이벤트가 발생하면 알림(notification)을 발생하여 등록된 이벤트 처리 구문을 자동으로 수행시킨다 2가지 패턴으로 이벤트 통지를 받아 이벤트를 수행한다. 리액터(reactor) 패턴과 퓨처(future) 패턴이다
리액터(reactor) 패턴
전형적인 이벤트를 처리하는 패턴(event handling pattern) 으로 리액터 패턴이 있다. 이벤트에 반응하는 객체(reactor)를 만들어, 이벤트가 발생하면 application대신 reactor가 대신 처리한다. reactor는 이벤트가 발생하길 기다리고, 이벤트가 발생하면 event handler에게 이벤트를 전달한다. event handler는 상황에 맞는 이벤트 처리 로직을 작성해야 한다.
네티의 이벤트 핸들러
네티에서 정의한 이벤트 핸들러 인터페이스를 구현(overriding)하는 방식이다 이벤트가 발생하면 통지(notificatoin)를 받아 이벤트 핸들러에 작성한 인터페이스가 호출된다 이벤트핸들러는 채널파이프라인에 필터와 같이 등록한다.
퓨처(future) 패턴
함수 호출을 하면 결과 성공 여부를 확인할 수 있는 future 객체를 즉시 리턴한다. 이 future 객체를 통해 성공/실패 여부, 에러발생 여부를 확인할 수 있다. 그러나 주기적으로 확인(while문 등)하는 불편함이 발생한다. 이를 해소하고자, 원하는 이벤트(작업완료, 에러발생)가 발생하면 그 이벤트에 맞는 객체(Listener) 를 미리 등록하여 이벤트 통지를 받아 수행하도록 프로그래밍 할 수 있다.
- 이벤트리스너는 옵저버패턴이다.
- 퓨처 패턴은 프로미스 패턴(promise pattern)이라고 불리우기도 한다
Spring Framework의 이벤트 처리
Spring framework에서도 이벤트 처리를 위한 매커니즘 및 인터페이스가 존재한다. 스프링 프레임워크가 제공하는 이벤트리스너를 구현하기만 하면 된다. 이벤트리스너의 실행은 NIO 이벤트루프그룹에서 수행한다.(병렬수행)
이벤트는 이벤트발생(publish)와 이벤트처리(consume)하는 방식으로 동작한다.
이벤트발생(publish)
스프링 프레임워크는 이벤트 발생을 위한 객체를 제공한다. ApplicationEventPublisher 를 주입받아서 사용한다.
@Autowired
ApplicationEventPublisher eventPublisher;
이벤트처리(consume)
이벤트를 처리하기 위한 이벤트 핸들러
는 다음 인터페이스를 구현한다.
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener
다음은 인터페이스를 구현한 예제이다.
public class WsHandler extends TextWebSocketHandler implements ApplicationListener<MessageEvent>
@Override
public void onApplicationEvent(MessageEvent event) {
sendMessage(event.getMessageEventData().notificationFormat());
logger.info("sendMessage : " + event.getMessageEventData().toString());
}
위 코드는 웹소켓 처리 핸들러 객체에 스프링의 이벤트리스너(ApplicationListener)를 구현(implements)한 코드이다. 이벤트를 받으면 웹소켓을 통해 클라이언트(브라우저)로 메시지를 송신하는 코드이다
이벤트 처리 방법
일반적으로 이벤트를 처리하기 위한 2가지 매커니즘이 있다.
1. 이벤트리스너 + 이벤트 처리 스레드
- UI 처리 프레임워크가 주로 사용
- 이벤트가 발생하면 이벤트 처리 스레드가 이벤트 리스너에 등록된 이벤트 메서드의 로직을 수행
- 이벤트 처리 스레드는 보통 단일 스레드
2. 이벤트 큐 + 이벤트 루프 스레드
- 이벤트를 큐에 등록하면 이벤트 루프가 큐에 접근하여 처리
- 이벤트 루프는 이벤트를 수행하기 위하여 이벤트 큐에서 이벤트를 꺼내와서 수행하는 무한 루프 스레드
- 단일(순서보장, 멀티코어 비효율, node.js)/다중 스레드로 할 수 있다
- 이벤트 결과를 돌려주는 방식으로 콜백패턴과 퓨처패턴으로 나뉘며 네티는 모두 지원
nenens 이벤트 처리
nenes는 netty와 spring mvc websocket을 사용하고 있다. 네티는 이벤트 기반으로 동작하는 네트워크 프레임워크이며 websocket 또한 이벤트 핸들러로 기반으로 작동한다. 네티는 nenes와 nenea 와 통신하는데 사용되며, websocket은 사용자의 브라우저와 spring web server 간 소켓통신에 사용된다 따라서 nenea - nenes - browser 간에 이벤트를 전달하기 위해서 netty와 websocket 간에 이벤트 전달과 처리가 필요하다.
이를 지원하기 위해 spring framework를 이용한다.
spring 프레임워크에는 이벤트를 생산과 소비하는 구현체(ApplicationListener
, ApplicationEventPublisher
)가 존재한다
이를 통해 네티에서 발생한 이벤트를 전달하여 사용자에게 실시간(real-time)으로 알림(notification)으로 전달할 수 있다.