이 글은 Programming in Scala 4/e
Chapter.32를 읽고 작성한 글입니다.
다중코어 프로세서가 널리 퍼지면서 멀티 스레드 환경이 조성되었고 동시에 동시성(concurrency)
에 대한 관심도 늘어났다. Java에서는 공유 메모리(shared memory)와 락(lock)을 기반으로 동시성을 제공한다.
하지만, 멀티스레드 환경에서 공유 메모리와 락을 관리하는 것은 매우 어렵다. 하지만, 스칼라의 표준 라이브러리는 Future
를 사용해 변경 불가능한 상태를 비동기적으로 변환하는 것에 집중하는 방식으로 어려움을 피할 수 있게 해준다.
스칼라의 Future는 계산 결과의 완료 여부와 관계없이 결과값의 변환을 지정할 수 있다. 각 변환은 원래의 Future를 지정한 함수에 따라 변환한 결과를 비동기적으로 담은 것을 표현하는 새로운 Future를 만든다.
스칼라의 Future
는 공유 데이터와 락의 필요성을 줄여주고, 때로는 아예 없애줄 수도 있는 동시성 처리 방식
을 제공한다. 만약 스칼라의 메서드를 호출하면 그것은 "기다리는 동안" 계산을 수행해 반환한다. 만약 그 결과가 Future라면 그것은 비동기적으로 진행할 다른 계산을 표현하는 것이다. 그리고 이러한 계산은 별도의 스레드를 통해 이루어진다. 따라서 Future에 대한 많은 연산에는 비동기적으로 실행하기 위한 전략을 제공하는 암시적인 실행 컨텍스트가 필요하다.
위와 같이 사용할 수 있다. scala.concurrent.Future
라는 패키지 뿐만 아니라, JVM의 전역 실행컨텍스트 스레드풀을 사용한다. 암시적인 실행 컨텍스트를 스코프로 가져오는 작업을 위해scala.concurrent.ExecutionContext.Implicits.global
을 import시키고, 퓨처를 만들면 된다.
만약 비동기적 실행을 위한 암시적인 실행 컨텍스트가 없다면 어떻게 될까?
위와 같이 Future 팩토리 메서드가 암시적인 실행 컨텍스트가 없어 오류가 나는 것을 알 수 있다.
다시 이 예제로 돌아오자. 이 코드는 20초 동안 스레드를 휴식시킨 후 10 + 10을 실행시키는 코드이다.
fut
을 실행시키자마자 isCompleted
메서드를 실행시키면 false라는 결과값을 얻으며, fut.value
역시 None이 된다. 하지만, 20초가 지난 후 두 메서드를 실행하면 각각 true와 20이라는 값을 받을 수 있다. 여기서 주목해야할 점은, fut.value
의 결과값이 Option[scala.util.Try[Int]]라는 점이다. 즉, value가 반환한 옵션에는 Try
가 들어있다.
Try는 T 타입의 값이 담겨 있는 성공을 나타내는 Success이거나, 예외(java.lang.Throwable의 인스턴스)가 들어 실패를 나타내는 Failure 중 하나이다.
Try의 목적은 동기적 계산에 있는 try 식이 하는 역할을 비동기 계산을할 때 사용하는 것이다.
따라서 계층 구조는 위와 같다. 동기적 계산의 경우 try-catch
문이 메서드가 던지는 예외를 그 메서드를 실행하는 스레드에서 잡을 수 있다. 즉, try-catch문은 예외를 같은 스레드 내에서 잡을 수 있다는 특징이 있다.
하지만, 비동기 계산에서는 계산을 시작한 스레드가 다른 작업을 진행하는 경우가 종종 있으므로 catch절에서 예외를 처리할 수 없다. 따라서 비동기적 활동을 나타내는 Future를 사용해 작업하는 경우에는 Try를 사용해서 해당 활동이 값을 만들어 내는데 실패해 예외를 발생시키면서 갑자기 완료되는 경우를 대비해야 한다.
위와 같이 0으로 나누면 예외가 발생해야 한다. 비동기적으로 실행해도 예외가 잘 처리됨을 알 수 있다.
스칼라의 Future는 Future 결과에 대한 변환을 지정해서 새로운 Future를 얻을 수 있다.
(1) 새로 만들어지는 Future는 원래의 비동기 계산
(2) 비동기 계산의 결과에 대한 반환
이렇게 2가를 조합한 것으로 표현한다.
어떤 비동기 연산을 기다리며 블록시키고 그 연산의 결과를 받아서 계산하는 대신, 퓨처에 대해 map을 사용해 다음 계산을 엮어넣으면 된다. 그렇게 된다면 map
이 Future를 받아와 Future를 리턴시킨다. 밑의 예시를 보면 더 이해가 쉽다.
위 코드에서 fut
은 위쪽의 예시와 같다. 그리고, result
는 fut
를 받아와 map
을 사용하여 1을 증가시키는 코드이다. 맨처음 fut.value
와 result.value
를 실행하면 모두 100초가 지나지 않았으므로 None을 반환한다. 이때, 눈에 띄는 점은 result의 결과값이 Try
라는 점이다.
위쪽에서 Try를 정리하며 Try는 비동기 환경에서 동기 환경의 try-catch와 유사하게 동작함을 알아보았었다. 즉, Future의 결과값에 적용되는 것을 보았다. map으로 Future를 인자를 받아 새로운 퓨처를 반환하는것이 보이는 것이다.
100초 후, value를 실행했을 때 result는 Future의 결과값에 추가적인 연산을 한 값을 리턴한다는 사실을 알 수 있다.
이 때 주의해야할 점은, 수행한 연산들(퓨처의 생성, 10 + 10의 계산, 20 + 1의 계산)이 모두 다른 스레드
에서 진행되었다는 사실이다.