Future(미래)와 Promice(약속)은 비동기처리에 있어서 중요한 개념입니다.
미래에는 블록킹이 없기때문에 어느시점 사용가능 해지는 함수의 결과(성공또는 실패등을)
를 담을수 있는 용기라고 보면 됩니다.
Scala에서 지원하는 키워드이지만 일반적으로 결과에대한
비동기적인 핸들이라고 보면 되겠으며 C#/JAVA에서도 유사한 모델을 가지고 있습니다.
이것은 AKKA의 요소가아닌 기존 개발 플랫폼의 비동기처리 요소로
나중에 Actor, Akka Stream등과 연동되는 중요한 요소로 사용이 될수 있습니다.
관련 참고 원문: 이러한 컨셉은 Java(8)/.net(4.5)에서도 지원을 합니다.
- https://docs.scala-lang.org/overviews/core/futures.html (scala)
- http://docs.scala-lang.org/sips/completed/futures-promises.html ( scala )
- https://msdn.microsoft.com/en-us/library/ff963556.aspx (.net)
자바7의 java.util.concurrent.Future에 익숙하다면 scala.concurrent.Future가 자바 클래스를 감싼것으로 생각할수도 있지만
실제로는 그렇지 않다. java.util.concurrent.Future 클래스는 폴링을 필요로하며 결과를 얻기위해 블로킹 get 메서드를 사용해야만
한다. 하지만 스칼라의 퓨쳐는 블로킹이나 풀링을 사용하지 않고 함수결과를 조합할수 있으며
JAVA8의 CompletetableFuture<T>가 오히려 여기서 설명하는 퓨쳐와 유사하다.
Future를 사용한 비동기처리 목표
Future와 Promice를 사용하는 목적은, 여러절차가 포함된 복잡한 비동기처리를
콜백헬을 방지하고, 조금더 도식화 할수있는 방법을 제공해줍니다.
콜백헬 예 : http://callbackhell.com/
Future
퓨쳐는 읽기전용이며, 퓨쳐에 담긴 값을 외부에서 변경할수 없습니다. 함수 실행이 끝나면
퓨쳐에 성공 또는 실패에 따른 결과가 들어갑니다. 퓨쳐에 담긴 결과는 완료된 다음부터는
바뀌지 않으며, 외부에서 여러번 읽을수 있고 매번 같은 값을 돌려줍니다.
결과가 들어갈 용기가 있으므로 비동기적으로 실행되는 여러 함수들을 쉽게 조합할수가 있습니다.
퓨쳐는 여러함수의 결과가 다음 함수의 입력이되어야할시, 여러 함수가 병렬로 연결되며
나중에 여러 함수의 결과를 조합해야하는 파이프라이닝(pipelining)에 좋은 도구입니다.
Future Sample-Scala
var request = EventRequest(ticketNr) //요청을만든다 var response = EventResponse = callEventService(request) //응답이 완료될때까지 기다린다 var event:Event = reponse.event //최종 이벤트값을 읽는다. val trafficRequest = TrafficRequest{ destination = event.location, arrivalTime = event.time ) var trafficResponse = callTrafficService(trafficRequest ) //이벤트값을 가지고 다시 트래픽정보를 요청한다.
var request = EventRequest(ticketNr) //요청을만든다 val futureRoute : Future[Route] = Future{ callEventService(request).event }.map{ event => val trafficRequest = TrafficRequest{ destination = event.location, arrivalTime = event.time ) callTrafficeService(trafficRequest).route //교통 루트를 반환한다. }
Future Sample
@FunctionalInterface interface FuncB { public int calc(int a, int b); } @FunctionalInterface interface FuncA { public int calc(int a); } FuncA F1 = (int a) -> a+1; FuncA F2 = (int a) -> a+1; FuncA F3 = (int a) -> a+1; FuncB F4 = (int a,int b) -> a+b; Integer input=1; CompletableFuture<Integer> futureB = CompletableFuture.supplyAsync(() -> F1.calc(1) ); CompletableFuture<Integer> futureD = CompletableFuture.supplyAsync(() -> F3.calc(F2.calc(1)) ); CompletableFuture<Void> combinedFuture = CompletableFuture.allOf(futureB, futureD); combinedFuture.get(); Integer result = futureB.get() + futureD.get(); System.out.println(result);
//Labda Func<int, int> F1 = x => x + 1; Func<int, int> F2 = x => x + 1; Func<int, int> F3 = x => x + 1; Func<int, int, int> F4 = delegate ( int x, int y ){ return x + y; }; //Block with task Task<int> futureA = Task.Factory.StartNew<int>(() => F1(1)); int c = F2(1); int d = F3(c); int f = F4(futureA.Result, d); Console.WriteLine("ResultA:" + f); //Continuation Tasks var futureB = Task.Factory.StartNew<int>(() => F1(1)); var futureD = Task.Factory.StartNew<int>(() => F3(F2(1))); var futureF = Task.Factory.ContinueWhenAll<int, int>( new[] { futureB, futureD }, ( tasks ) => F4(futureB.Result, futureD.Result)); futureF.ContinueWith(( t ) => Console.WriteLine("ResultF:" + t.Result) ); Console.WriteLine("Code...End");
Promice (미래를 위한 약속)
퓨쳐가 읽기전용 용기라고하면, 퓨쳐자체를 반환하는것을 작성하고 싶을때는 Promice를 사용합니다.
Future 용기는 java/.net에 대응가능하지만 Promice를 직접적으로 대응하는 키워드는 없는것으로 보입니다.
Promice는 퓨쳐에 비교하여 다음과 같은 특징을 가집니다.
- Promice는 미래를 위한 약속이기때문에 단한번만 완료될수있습니다.(다시 완료시 예외발생)
- Promice는 성공과 실패처리에대해 분리하여 처리할수 있습니다.
Promice 샘플 - KafkaSend
def sendTokafka(recode: ProducerRecod): Futrure[RecordMetadata] = { val promise : Promise[[RecordMetadata]] = Promise[RocordMetadata]() //RecordMetadata 타잎의 값을 돌려주는 약속을 만든다. val future:Future[RecordMetadata] = promise.future //future에 대한 참조를 가져온다 val callback = new CallBack(){ //송신성공시 카프카 콜백 def onCompletition(metadata: RecordMetadata, e:Exception): Unit={ if(e != null ) promise.failure(e) //오류기록 else promise.success(metadata) //성공기록 } } producer.send(record,callback) future }
퓨쳐를 조합하기
퓨쳐와 액터 조합하기
.net 에서 Actor 메시지와 Future(Task)를 조합한 샘플
public class ReActor : ReceiveActor { private ILoggingAdapter log = Context.GetLogger(); public ReActor() { string myPath = Self.Path.ToString(); Receive<string>(message => { Handle(message); }); Receive<DelayReply>(message => { Handle(message); }); } public void Handle(string str) //InMessage { Task.Run(async () => { await Task.Delay(1000); //어떠한 값을 기다림 DelayReply reply = new DelayReply(); reply.message = str; return reply; }).PipeTo(Self); } public void Handle(DelayReply data) //Out { string logtrace = string.Format("I'am {0} RE:{1}", Self.Path, data.message); log.Info(data.message); Sender.Tell(data); } }
이것은 기존에 가진 모듈을(액터모델에 맞지않은) 모두 액터로 변환하여 불필요하게 복잡성을
늘릴필요가 없을시 기존 비동기처리 코드를 재사용하여 파이프를 통해 액터와 유연하게 연동이 가능합니다.
이렇게 사용했을시 부가적인 이득은 설정만으로 여러장치에 확장이가능하며 클러스터화가 될수 있다란것입니다.
이것은 로컬 로직을 확장할수 있는 방법중에 하나가 될것입니다.