Promise와 Future 두 가지 모두 동시성 프로그래밍에서 선언만 미리 해두고 나중에 결과를 받을 수 있는 비동기 호출에서 다루는 객체에 대한 것이다.
Wikipedia에 따르면, 1970년대에 최초로 promise와 future의 개념을 제안했던 연구자들은 다음과 같이 개념을 정의하고 있다.
a future is a read-only placeholder view of a variable
Future는 변수의 읽기전용 위치지정 뷰이다
a promise is a writable, single assignment container which sets the value of the future
Promise는 Future의 값을 정하는 쓰기가능한 단일 대입 컨테이너이다
사전적 정의에서는 promise가 대입 "컨테이너"인데, 실제로는 이렇게 다뤄지지 않고 있기에 다른 측면에서 살펴보기로 한다.
Promise와 future를 엄격하게 구분하지 않는다면,두 가지를 합친 개념으로서 성공 콜백과 예외 콜백을 달아두고 사용하는 객체를 promise라고 부를 수 있다. Javascript는 이런 관점에서 promise를 이야기하고 있다.
다음과 같은 코드를 보면, promise가 어떤 것인지 알 수 있다. 여기서 result 파라미터가 future가 되는 셈이다.
myPromise.then((result) => {
...
}).catch((error) => {
...
});
그러나, 성공 콜백과 예외 콜백이 promise의 필수 요건은 아니어서, 단순히 future를 얻기만 하더라도 promise라고 할 수 있다. (아래 C++ 예제 참고)
그러므로 어떤 로직이 비동기적으로 동작하고 성공하면 future를 얻고 실패하면 선택적으로 예외를 얻을 수 있게 되어 있다면 그걸 promise라고 일반화할 수 있다.
좀 더 간단하게 개념을 정리하자면, future는 미래에 받아낼 값
에 대한 선언이고, promise는 그 future를 받기로 한 약속
인 것이다. 돈을 빌려주는 거래에 비유하자면, future는 나중에 돌려받을 돈이 되겠고(아직 내 손에 돈이 있진 않지만 어쨌든 돌려받기로 했으니까), promise는 돈을 돌려받기로 한 약속이 되겠다. promise는 성공과 실패에 대한 콜백을 가질 수 있는데, 돈을 성공적으로 돌려받으면 그 돈을 어디에 쓸지를 미리 계획을 세워두는 게 성공 콜백이고, 돈을 돌려받지 못했을 때 어떻게 대응할지에 대한 계획이 예외 콜백이라고 할 수 있겠다.
Wikipedia에 나온 사전적 정의는 현실 세계의 소프트웨어 개발자에게는 큰 도움이 되지 않는다.현실 세계의 promise와 future는 사전적 개념과 확장된 개념들이 혼합되어서 사용되고 있다고 볼 수 있다.
일반적으로 정적 타이핑 언어에서는 promise와 future를 구분하지만, 동적 타이핑 언어에서는 promise와 future를 별도로 구분하지 않기 때문에 실제로는 같은 개념으로 취급하고 있다. 예를 들면, Javascript의 promise는 promise이자 future라고 할 수 있다. Java는 정적 타이핑 언어임에도 불구하고 예외적으로 CompletableFuture라는 개념을 가지고 있고 이것을 중심으로 해서 비슷한 기능을 제공하고 있다.
C++에서는 promise와 future를 모두 제공하고 있다.
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread( [&p]{ p.set_value_at_thread_exit(9); }).detach();
this_thread::sleep_for(chrono::seconds(1));
try {
f.get();
} catch (const std:exception& e) {
...
}
std::promise는 쓰레드라는 저수준 도구에 직접적으로 의존하기 때문에, 추상화 수준이 걸맞지 않은 단점을 가지고 있다. lambda 표현식을 사용할 수 있긴 한데, 여전히 thread를 통해서 실행해줘야 한다.
차라리 std::packaged_task를 이용하고 여기에서 std::future를 받아서 사용하는 방식이 더 제대로 추상화되었다고 할 수 있다.
std::packaged_task<int(int, int)> task(
[](int a, int b)
{
return std::pow(a, b);
});
std::future<int> result = task.get_future();
task(2, 9);
여기서 std::packaged_task가 std::promise보다 좀 더 추상화된 일반적인 promise처럼 사용되고 있는 것을 알 수 있다.
쓰레드에 promise를 전달하는 것이 필수는 아니라서 좀 더 편리하게 사용할 수 있다. 비동기적 실행을 위해서는 std::packaged_task나 std::promise를 쓰레드에 전달해야 하지만 이건 std::async를 이용해서 대체 구현 가능하다.
아예 std::async를 이용해 비동기적인 promise를 정의하고 future로 직접 반환하는 방법도 가능하다.
future<long long> f = async([](int a, int b) { return (long long) pow(a, b); }, 2, 9);
std::async 기능을 이용하면 1) 비동기적으로 또는 2) 결과를 요청받을 때까지 최대한 지연해서 실행할 수 있다.
Java는 공식적으로 Promise를 제공하고 있지 않기 때문에, CompletableFuture를 중심으로 해서 promise처럼 사용한다.
CompletableFuture<String> future
= CompletableFuture.supplyAsync(() -> "future example");
future.get();
Future만 선언해두고 promise에 대해서는 모르는 척 하고 있다. supplyAsync()를 통해 promise가 해야 할 기능을 제공하고 있음을 알 수 있다. C++보다 나은 점은 promise에 lambda 표현식을 직접 사용할 수 있다는 것이다.
Promise 객체를 생성하면서 비동기적으로 수행할 작업을 정의한다. 생성된 promise는 then()과 catch() 메소드에서 성공 콜백과 예외 콜백을 정의해서 처리할 수 있다. result가 future라고 할 수 있다.
let myPromise = new Promise(function(resolve, reject) {
try {
...
resolve("완료");
} catch (error) {
reject(new Error("에러 발생!"));
}
});
myPromise.then((result) => {
console.log(result);
}).catch((error) => {
console.error(error);
});