Swift는 escaping 클로저와 non-escaping 클로저에 차이를 두고 있다. escaping 클로저는 한번 호출되고나면 리턴값으로 클로저를 반환하는 함수이다. 클로저는 인자로 받은 함수 스코프를 escape한다.

클로저를 escape 하는 것은 종종 아래 예시처럼 비동기 컨트롤 플로우와 연관되있다. 
  • 함수가 백그라운드 작업을 시작하고 즉시 리턴하면, 완료 핸들러를 통해 백그라운드 작업의 결과를 알린다.
  • 뷰 클래스가 버튼 탭 이벤트를 다르기위해 프로퍼티에 클로저를 저장해둔다. 이 클래스는 사용자가 버튼을 탭 할때마다 클로저를 호출한다. 클로저 프로퍼티는 세터를 escape한다.
  • 여러분은 DispatchQueue.async를 사용하여 디스패치 큐에서 비동기 실행을 위한 작업을 스케줄링한다. 이 테스크 클로저는 비동기에 호출된 이후에도 소멸되지 않은 채 살아있다.
DispatchQueue.sync와 대조되는데, 이것은 리턴되기 전에 테스크 클로저의 실행이 끝나기 전까지 기다린다. 이 클로저는 절때 escape하지 않는다. 표준 라이브러리에 map, 다린 일반적인 sequence, 그리고 collection 알고리즘에도 동일하다.

escaping 클로저와 non-escaping 클로저의 차이가 왜 중요할까?
간단히 말해 메모리관리 때문이다. 클로저가 붙잡고있는 모든 오브젝트는 강참조로 들고 있으며, 클로저 안에서 self의 프로퍼티나 self의 메소드에 접근하려 한다면 이 모든것들이 묵시적으로 self 파라미터를 다루기 때문에 self까지 포함하여 들고 있는다.

이러한 방식은 굉장히 참조 사이클(reference cycle)을 마주치기 쉬운데, 이것이 왜 컴파일러가 클로저 안에서 명시적으로 self를 쓰게 만드는지에대한 이유이다. 명시적으로 쓰게 만듦으로서 당신에게 잠재적인 참조 사이클에대해 생각해볼 수 있게 해주고, 붙잡고 있는 항목들을 이용해 손수 해결할 수 있게 해준다.

그러나 non-escaping 클로저로는 참조 사이클을 만드는게 불가능하다. 클로저는 함수 리턴시점까지 붙잡아둔 모든 오브젝트를 릴리즈(release) 시킬 것이라는 것을 컴파일러가 보장한다. 이러한 이유로 escaping 클로저에서만 명시적으로 self를 사용하여 참조할 것을 요구한다. 이 점이 non-escaping 클로저 사용을 더 확실히 즐겁게 해준다.

non-escaping 클로저의 또다른 장점은 컴파일러가 더 적극적으로 퍼포먼스 최적화를 수행할 수 있다는 점이다. 예를들어 클로저의 라이프타임을 알고 있을때 몇몇 리테인(retain)과 릴리즈(release) 호출은 생략할 수 있다. 또, non-escaping 클로저라면 클로저의 컨텍스트를 위한 메모리가 힙이 아닌 스택에 담아둘 수 있다.(현재 Swift 컴파일러가 이러한 최적화를 시키는지는 잘 모르지만 2016년3월 버그리포팅에서 그렇게 하지말자고 제안이 들어왔었다)

디폴트로 클로저는 non-escaping이다.
Swift3부터 non-escaping 클로저가 디폴트이다. 만약 클로저 파라미터에 escape 시키고 싶으면 그 타입에다가 @escaping 지시자를 써야한다. 예를들어 DispatchQueue.async (escaping)와 DispatchQueue.sync (non-escaping) 선언이 있다.

Swift3 전까지는 좀 다른 방식으로 동작했었는데, escaping이 디폴트이고 오버라이드에 @nonescape를 추가할 수 있었다. 이러한 새로운 동작은 디폴트에의해 더 안전해진다는 면에서 더 좋은데, 이제 함수 인자는 반드시 참조 사이클의 잠재성이 있다는 것을 명시적으로 표시해주어야한다. 따라서 @escaping 지시자는 이 기능을 사용하는 개발자에게 경고를 해주는 역할을 한다.

...그러나 오직 즉석 함수 파라미터(immediate function parameters)로서
디폴트에의한 non-escaping 규칙에 대해 주목할 것이 있다. 이는 직접 함수 매개 변수 위치의 클로저에만 적용된다. 즉, 함수 타입을 가지는 모든 함수 인자에 적용된다. 다른 모든 클로저는 escaping하고 있다.

직접 파라미터 위치가 무슨 뜻일까?
예제를 한번 보자. 가장 간단한 예제로는 map이 있다. 이 함수는 직접 클로저 파라미터를 받는다. 우리가 보았듯 클로저는 non-escaping이다. (여기서 실제 map의 표시를 말하려는게 아니므로, 그것들을 조금 생략하겠다)

함수 타입의 변수들은 항상 escaping이다.
반대로, 변수나 프로퍼티가 함수 타입을 가질 수 있다. 이때는 명시적인 지시 없이 자동으로 escaping이 된다.(사실은 @escaping을 넣으면 에러가 뜬다) 이렇게 이해할 수 있는데, 변수에 값을 할당하면 묵시적으로 값을 변수의 범위로 escape할 수 있기 때문이다. 이것은 non-escaping 클로저로 허가될 수 없다. 그러나 드물게 지시되지 않는 함수는 파라미터에서의 의미가 아닌 다른 곳에서는 다른 의미를 가지기 때문에 혼란스러울 수 있다.

옵셔널 클로저는 항상 escaping이다.
더욱 놀라운 점은, 파라미터로 쓰이지만 다른 타입(튜플이나 enum case, 옵셔널 같은)으로 감쌓여진 클로저들 또한 escaping이라는 것이다. 이 경우에 클로저는 더이상 직접 파라미터가 아니므로 자동으로 escaping된다. 그 결과 Swift3에서는 파라미터가 옵셔널이면서 동시에 non-escaping한 곳에 함수인자로 받는 함수를 만들지 못한다. 아래의 다소 인위적인 예제를 생각해보자. transform 함수는 정수 n과 옵셔널 변환 함수인 f를 받아 f(n)를 반환하거나 f가 nil이면 n을 반환한다.

여기서 ( (Int) -> Int )?가 Optional<(Int) -> Int>의 축약이기 때문에 함수 f는 escaping이며, 따라서 함수 타입은 직접 파라미터 위치에 있지 않다. 이 결과는 우리가 바라는 것이 아닌데, 여기서 f가 non-escaping 될 수 없는 이유가 없기 때문이다.

옵셔널 파라미터를 디폴트 구현으로 대체하자.
Swift 팀은 이 한계를 알고있고, 미래의 배포에서 고치기로 계획했다. 그전까지 우리가 이것을 알고 있어야한다. 현재 강제로 옵셔널 클로저를 non-escaping할 방법은 없지만, 많은 경우에 클로저에다 디폴트 값을 제공하여 옵셔널 인자를 피할 수 있을 것이다. 우리 예제에서는 디폴트 값이 항등 함수(identity function)이고, 이 함수는 간단하게 인자를 바꾸지않고 그대로 반환한다.

옵셔널과 non-escaping 변형을 제공하기 위해 오버로드를 사용하자
디폴트 값을 제공하기 힘든 경우라면, Michael Ilseman이 오버로드를 사용할 것을 제안했다. 여러분은 함수의 두기자 변형을 만드는데, 하나는 옵셔널(escaping) 함수 파라미터를 받고, 하나는 non-옵셔널, non-escaping 파라미터를 받는다.

어떤 함수가 호출되었는지 설명하기 위해 print 상태를 추가했다. 여러 인자로 이 함수를 테스트해보자. 당연하게도 nil을 보내면, 그 인풋과 일치하는 것이 하나밖에 없으므로 타입 체커가 첫번째 오버로드를 선택한다.
동일하게, 옵셔널 함수 타입을 가지고 있는 변수를 보낸다.
그 변수가 non-옵셔널 타입일지라도, Swift는 여전히 첫번째 오버로드를 선택할 것이다. 그 이유는 변수에 저장된 함수는 자동으로 escaping되고, 따라서 non-escaping 인자를 예상한 두번째 오버로드와는 일치하지 않는다.

그러나 클로저 표현식을 보낼때 이것은 바뀐다. 즉 함수 리터럴이 이 자리에 있을때 말이다. 이제 두번째 오버로드 non-escaping이 선택된다.

리터럴 클로저 표현식으로 higher-order 함수를 호출하는 것은 굉장히 일반적이므로, 대부분의 경우 선택적으로 여전히 nil을 보낼 수 있게 해줌으로서 여러분을 즐거운 길(참조 사이클을 생각할 필요 없는 non-escaping)로 안내해줄것이다. 이런 방식으로 한다면 왜 두가지 오버로드가 필요한지 증명할 수 있을 것이다.

타입에일리어스(typealiases)는 항상 escaping이다.

마지막으로 한가지 알고 있어야 하는 것은 Swift3에서는 타입에일리어스에 escaping 혹은 non-escaping 지시자를 넣을 수 없다는 것이다. 함수 선언에서 함수 타입을 위해 타입에일리어스를 사용한다면 그 파라미터는 항상 escaping으로 생각될 것이다. 이 버그 수정은 이미 마스터 브런치에서 이루어졌고, 다음 배포에 적용될 수 있을 것이다. 


신고

WRITTEN BY
canapio
개인 iOS 개발, canapio

받은 트랙백이 없고 , 댓글이 없습니다.
secret

프로그래밍 패러다임 관점에서나 탄생 이유의 관점에서나 리액티브(Reactive)를 이해하기 위해서는 현재 개발자와 회사가 직면한 문제들과 10년전의 문제를 비교하여 생각해보아야한다.

개발자와 회사에 바뀐 두가지는 아래와 같다.
  • 하드웨어의 발전
  • 인터넷

"모닥불 주위에 모여 옛날일을 이야기하는 것"은 일부에의해 대화의 수준이 낮아질 수 있음으로 고려해야하는 반면, 우리는 모든 개발자가 직면한 문제를 고심하기 위해 직종의 역사를 탐구해볼 필요가 있다.

왜 이것들이 이제 달라졌을까
10년간 컴퓨팅 연구 끝에 발견한 의미있는 지식이 하나 있다. 리액티브 프로그래밍은 소프트웨어의 새 세대를 만들기 위한 시도이다.

1999년
1999년, 내가 캐나다 임패리얼 상업은행에 있고 처음 자바를 배우기 시작할 무렵이다.
  • 인터넷은 2.8억 유저에 달성했다.
  • JSEe는 여전히 썬(Sun MicroSystems)사의 꿈으로서 가슴속에 있는 상태였다.
  • 온라인 뱅킹이 5년정도 된 유아기 시점이었다.
1999년으로 돌아가면 나는 동시성에 대한 문제에 직면하고 있었다. 그 해결책은 스레드와 락(lock)에 관련이 있었으며, 유경험자의 개발자까지도 그 문제를 해결하기 힘들어하는 상황이었다. 자바의 특징은 "한번 작성하면, 어디서든 실행할 수 있다"인데, 여기서 "어디서든"은 JVM이 설치되어있는 OS에 한에서 "어디에든"이지, 클라우드나 IoT 세대를 위해 설계한 다른 동시접속의 개념이 아니다.

2005년
2005년이 오래전은 아니지만 컴퓨팅쪽과 인터넷쪽이 크게 바뀌었다. J2EE, SOA, XML이 인기를 끌었고 루비온레일즈가 J2EE의 고통받는 컨테이너 기반 개발 문제를 해결하기 위해 탄생하였다.

이때의 인테넷에는
  • 1억유저가 있었다.
  • 페이스북이 550만 유저를 가지고 있었다.
  • 유튜브가 탄생했다.(2005년 2월)
  • 트위터가 아직 없었다.(2006년)
  • Netflix가 비디오 스트리밍을 소개했다.(2007년)

2014년
이 글을 쓰고 있는 시점인 2014년은 Internet Live Stats에 의하면 약 2,950,000,000(29억 5천만)정도의 인터넷 유저가 있다고 한다. 중국이 혼자서 6억 4천만 인터넷 유저를 보유하고 미국이 2억 8천만을 보유하고 있었다.

오늘날 가장 인기있는 웹사이트이다.
  • 페이스북—13억 유저
  • 트위터—2억 7천 유저

시간이 흐르면서 한 웹사이트의 트래픽이 지난 20년전의 인터넷 전체 트래픽보다 많다.

1999년부터 2015년까지 인터넷, 페이스북, 트위터 유저 수1999년부터 2015년까지 인터넷, 페이스북, 트위터 유저 수



우리는 점점 확장과 예측에 관한 이슈가 중요해지고, 우리의 삶에서의 소프트웨어가 중요해짐을 쉽게 확인할 수 있다. 또한 과거의 패러다임이 현재까지 이어질 수는 없을 것이고, 분명 미래까지도 이어지기 힘들 것이다.

4가지 리액티브 요소
리액티브 앱은 4가지 요소로 구성되있다.

  • 반응성(responsive)있는 앱이 그 목표이다.
  • 반응성있는 앱은 확장가능(scalable)하고 탄력(resilient)있다. 반응성은 확장성과 탄력성 없이 불가능하다.
  • 메시지-주도(message-driven) 구조는 확장가능함과 탄력있음을 근간으로 하고 궁극적으로 반응성있는 시스템을 기반으로 한다.

반응성(Responsive)
"앱이 반응성 있다"는 것이 어떤 의미일까?

반응성있는 시스템은 시종일관으로 분명한 유저 경험을 보장하기위해 좋은 상황이나 나쁜  상황이나 상관없이 모든 유저에게 즉각 반응하도록 하는 것이다.

외부 시스템의 실패나 트래픽 급증과 같은 다양한 상황에서의 재빠른 처리분명한 사용자 경험탄력성확장성이라는 두 성질에 의존한다. 메시지-주도 구조는 반응성 있는 시스템을 위한 전반적인 근간을 제공해준다.

왜 반응성 있는 것이 메시지-주도 구조에 중요할까?

세상은 비동기적이다. 당신이 한 포트의 커피를 끓이는데 크림과 설탕이 없음을 깨닭았을때의 그 예시가 있다.

아래는 그 한가지 방법이다.
  • 한 포트의 커피를 끓이기 시작한다.
  • 커피가 끓는동안 가게에 간다.
  • 크림과 설탕을 산다.
  • 집으로 돌아온다.
  • 즉시 커피를 마신다.
  • 여유를 즐기면 된다.

또 다른 방법이다.
  • 가게에 간다.
  • 크림과 설탕을 산다.
  • 집으로 돌아온다.
  • 커피를 끓을 때까지 시계를 보면서 기다린다.
  • 카페인 금단현상을 겪는다.
  • 젠장

여러분이 볼 수 있듯, 메시지-주도 구조는 당신을 시공간으로부터 분리된 비동기 바운더리를 가능하게한다. 우리는 남은 이 포스트에서 비동기적 바운더리 개념에 대해 이야기해볼 것이다.

왈마트 캐나다(Wlamart Canada)에서의 일관성
Typesafe에 들어가기 전에, 나는 왈바트 캐나다의 플랫폼을 만든 Play and Scala 팀의 기술 리더였다.

우리의 목표는 분명한 사용자 경험의 일관성을 만드는 것이었다. 다음 것들에 관계없이 말이다.
  • 데스크탑, 테블릿, 모바일 기기등 어떤 기기에서도 walmart.ca를 서핑할 수 있다.
  • 현재 피크 트래픽이 튀어오르든 유지되든 관계없어야 한다.
  • 전체 데이터 센터의 손실과 같이 주요 기능이 실패해도 관계없어야 한다.

응답 시관과 전반적인 사용자 경험은 위 시나리오와 관계없이 일관성있다. 일관성은 당신의 웹사이트를 전달하는데 근본적으로 중요하며 오늘날 웹사이트는 당신의 브렌드임을 생각해야한다. 좋지않은 사용자 경험은 실제 상점이 아니라 온라인에서 일어나기 때문에 쉽게 잊어지거나 무시되지 않는다.

Glit에서의 반응성있는 소매(retail)
전자상거래 도매인에서의 일관성은 우연에의해 일어나지 않는다. Glit의 경우, 매일 저녁에 하루 세일을 공지하는데, 그 때 트래픽이 급증하는 플래시 스케일 사이트이다. 플래시 스케일 사이트의 사용자 경험에대해 이야기해보자. 만약 당신이 오전 11:58에 한번 Glit를 접속하고 오후 12:01에 한번 더 접속한다면 Glit는 일관성있게 정해진 응답 경험을 제공하고 이것을 리액티브를 사용하여 시행하였다. 스칼라 기반의 마이크로 서비스 구조로 마이그래이션한 Glit를 더 배우고 싶다면 interview with Eric Bowman of Gilt 여기를 보아라.

탄력있는(Resilient)
많은 앱들이 이상적 환경만을 고려하면서 설계, 개발하지만 사실 이상적이지 않을때도 많다. 이것은 매일 주요앱 기능이 실패하는 다른 보고서를 받거나, 해커에의해 서비스 멈춤, 데이터 손실, 지속적인 손상을 초례하는 다른 co-ordinated breach 시스템이 있을 수 있다.

탄력있는 시스템은 이상적이지 않는 상황에서도 반응성을 보장하기 때문에 바람직한 설계 요소와 구조 요소를 사용한다.

자바와  JVM은 다중 OS에서 한 앱을 문제없이 실행시키는 것에 대한 것이었다면, 201x년대의 상호연결된 앱은 앱단의 구성, 연결성, 보안성에 대한 것이다.

이제 한 앱은 웹사이트나 다른 네트워크 규약을 통해 통홥되어 여러 앱으로 구성되어있다. 오늘날 하나의 앱은 신뢰된 방화벽을 가진 바깥은 외부 서비스들에(10개, 20개, 혹은 더 많이) 의존하며 만든다.

이 복잡한 통합을 고려하면 얼마나 많은 개발자가 필요할까?

  • 모든 외부 의존성을 분석하고 모델링하는 사람
  • 통합된 각 서비스의 이상적인 응답 시간을 문서로 만들고 피크일때나 아닐때 모두 퍼포먼스 테스트가 초창기 기대와 일치하는지 확인하기위해 관리하는 사람
  • 모든 퍼포먼스, 실패, 핵심앱 로직으로 포함되는 다른 비기능적인 요구사항을 문서화하는 사람
  • 각 서비스의 모든 실패 시나리오를 다시 분석하고 테스트하는 사람
  • 외부 의존성의 보안성을 분석하고, 외부 시스템과 통합했을때 새로운 취약점이 있는지 알아내는 사람

탄력성은 가장 정교한 앱의 가장 약한 연결 중 하나이지만, 추가적으로 탄력있어야하는 것은 곧 끝난다.(원문: Resiliency is one of the weakest links of even the most sophisticated application, but resiliency as an afterthought will soon end.) 현대의 앱들은 이상적인 상황 보다는 다양한 현실세계에서 반응성을 유지해야하기 때문에, 앱의 핵심(core)이 반드시 탄력적이어야한다. 퍼포먼스, 내구성, 보안성은 모든 면에서 말이다. 여러분의 앱은 단지 몇 부분이 아닌 모든 부분에 걸쳐 탄력적이어야한다.

메시지-주도 탄력성
메시지-주도 핵심에서 가장 아름다운 제작방법은 여러분이 자연스럽게 작업에 필요한 것들을 한조각 한조각 얻어내는 것이다.

고립(Isolation)은 시스템의 자체 회복을 위해 필요하다. 잘 고립되있을때, 우리는 실패의 위험이나 퍼포먼스 특징, CPU와 메모리 사용 등과 같은 요인에 기반하여 여러 타입으로 나눌 수 있다.

정확한 위치는 마치 같은 VM에서 동작하게 한 것 처럼 서로다른 노드위에서 서로다른 프로세스들이 상호소통할 수 있게 해준다.

특정 목적용 에러 채널은 단지 에러 신호를 호출자에게 던저버리는게 아니라 다른 우리가 원하는 곳으로 보낼 수 있다. 

이러한 사실은 우리 앱에서 확고하게 에러 핸들링을 구체화하고 결함에 내성을 만들어준다. 이것은 아카의 감독 계층(Akka's supervisor hierarchies)처럼 구현함으로서 입증되었다.

조각(block)을 만드는 핵심은 이상적인 환경 뿐만 아니라 좋지않은 환경에서도 메시지-주도 구조가 탄력성에 기여하는 것에의해 제공하고 다음으로 반응성에 기여한다.

44억 달러의 탄력성 실수
2012년에 를 생각해보자. 소프트웨어가 향상되는동안 통합된 앱 방식은 점점 인기있고(fired up) 거래 규모가 점점 커지기 시작했다.

다음은 45분동안 일어나는 악몽같은 시나리오였다.

Knight의 자동교환 시스템이 잘못 거래하여 나스닥(NASDAQ)을 침수시켰고, 10억달러가치를 의도치않은 회사에 놓았다. 이러한 사고는 다시 반환(reverse)하는데 회사에 44억달러의 비용이 들게 되었다. 나스닥이 침수되고 거래의 범람을 고치는 동안 나스닥은 Knight 돕는 것을 중단했다. Knight의 주식은 하루만에 63%나 떨어졌으며 그들은 가까스로 살아남았고, 일부를 회복한 후에 투자자에의해 다음 인계를하고 살아갈 수 있었다. 

그때 Knight의 시스템은 동작했었지만 탄력성이 없었다. 탄력성이 없는 Knight 시스템은 문제를 확장시키는데 한 몫을 하였다. Knight는 최악의 버그가 발생했을때 그들의 시스템을 끌 수 있는 스위치(kill-switch) 매커니즘도 없는 상태였으므로, 나쁜 환경에서 그들의 자동 거래시스템이 45분만에 회사의 모든 주요 자산을 고갈시켜버린 것이다.

이것은 이상적인 환경을 위한 설계 정의이자 개발 정의였다. 이제 소프트웨어는 우리 개인 삶이나 회사에서 핵심 요소이다. 예측되지 못한 나쁜 상황의 시나리오 된다면 굉장히 많은 비용을 감당해야할 수 있다.

확장가능한(Scalable)
일관성있는 반응성 앱을 만들때 탄력성과 확장성을 잘 이용해야한다.

확장 가능한 시스템은 다양한 요구량의 상황(various load conditions)에서도 반응성을 보장하기 때문에 그에 맞춰 쉽게 업그레이드 시킬 수 있다.

온라인에서 물건을 팔아본 사람이라면 물건을 최대로 많이 팔때 가장 큰 트래픽이 생긴다는 사실을 알 것이다. 대부분의 경우(사이버상 공격을 제외하고) 트래픽이 폭발하는 것은 당신이 뭔가 잘하고 있을 때이다. 트래픽이 치솟았을대 사람들은 당신에게 돈을 주고 싶어하고 있는 것이다.

그럼 어떻게 치솟는(혹은 꾸준히 증가하는) 트래픽을 다룰까?

첫째로 당신의 패러다임을 먼저 고른다. 둘째로 그 패러다임을 구현할 수 있는 언어와 툴킷을 정한다. 많은 개발자가 종종 너무 가볍게 언어와 프레임워크를 선택한다. 한번 툴을 선택하면 그것을 다시 바꾸기 쉽지 않으므로 당신은 주요 투자가와 함께 그 결정을 내려야한다. 만약 여러분이 기술적인 선택 결정을 원칙과 분석에 기반하여 하고 있었다면 굉장히 잘하고 있는 것이다.

동시성을 위한 스레드-기반 제한
기술적인 선택에서 가장 중요한 것중 하나는 동시성 모델 프레임워크이다. 고수준에서 두개의 서로다른 동시성 모델이 있을 수 있다.
  • 전통적인 스레드-기반 동시성으로 콜스택과 공유 메모리를 기반으로 한다.
  • 메시지-주도 동시성

레일즈와같은 몇 인기있는 MVC 프레임워크는 스레드 기반이다. 이 프레임워크의 전형적인 특징은 아래와 같다.
  • 공유 가변 상태(Shared mutable state)
  • 요청당 한 스래드(A thread per request)
  • 가변 상태에 동시 접근(Concurrent access to mutable state)—이것(변수나 객체 인스턴스)은 또다른 복잡한 동기화 방법이나 락(lock)으로 관리한다.

루비와 같은 인터프리트 언어로 다이나믹 타입의 특징을 합치면 여러분은 쉽게 퍼포먼스와 확장성의 상한선에 도달할 수 있을 것이다( Combine those traits with a dynamically typed, interpreted language like Ruby and you can quickly reach the upper bounds of performance and scalability). 이것이 어떠한 스크립트 언어라도 그 본질은 같다고 말할 수 있을 것이다.

Out 혹은 Up?
앱 확장을 좀 다른 방법으로 생각해보자.

스케일업(Scale up)은 단일 CPU/서버의 리소스를 최대화 하는 것인데, 파워풀하고 희귀하고 값비싼 그런 컴퓨터를 종종 사야한다.

스케일아웃(Scale out)은 여러 저렴한 하드웨어를 연결하여 컴퓨테이션을 제공하는 것인데, 비용면에서 효과적이다. 그러나 당신의 시스템이 시공간 개념에 기반하였다면 매우 어려울지도 모르겠다. 위에서도 이야기했듯 메시지-주도 구조는 시공간으로부터 분리하기위해 필요한 비동기 바운더리를 제공하며, 필요에따라 스케일아웃 할 수 있는 유연성(elasticity)을 제공한다. 반면 스케일업은 이미 가지고있는 자원의 효율을 높히는 것이고, 유연성은 당신의 시스템이 바뀌기 원하는대로 새 자원을 추가할 수 있음에 관한 것이다. 필요에따라 스케일아웃 할 수 있는 능력은 리액티브 앱의 궁극적인 확장성의 목표이다.

공유 가변 상태, 스레드, 락 기반의 앱을 스케일아웃하는게 어렵기 때문에 리액티브 앱들을 스레드 기반으로 만드는것은 어려운 일이다. 개발자들은 한 머신에서 멀티 코어의 이점을 활용해야 할 뿐만 아니라, 특정 시점의 개발자들은 머신의 클러스터를 활용해야 한다. 그게 불가능할지라도, 공유 가변 상태 또한 스케일업하기 어렵게 만든다. 한번이라도 두개의 스레드에서 공유 가변 상태를 다뤄본 사람이라면, 스레드 세이프를 보장하는 과정이 얼마나 어려운지, 스레드 세이프를 위해 과한 작업을 하게되는 실적 패널티가 얼마나 큰지 이해할 수 있을 것이다.

메시지-주도(Message-driven)
메시지-주도 구조는 리액티브 앱의 근간이다. 메시지 주도 앱은 이벤트 주도 이거나 행위자 기반일 것이고, 혹은 이 둘 모두를 합친 것일 것이다.

이벤트 주도 시스템은 0개 혹은 그 이상의 Observer에의해 관찰된 이벤트 기반이다. 이것은 명령형 프로그래밍과는 좀 다른데, 호출자가 부른 루틴으로부터 응답을 블락된 상태로 기다릴 필요가 없기 때문이다. 이벤트는 바로 특정 장소를 지정하는게 아니라 나중에 일어날 어떤 결과를 지켜보고 있는 것이다.

행위자-기반 동시성은 메시지-전달 아키텍처의 확장이며 메시지는 수신자에게 전달된다. 메시지는 스레드 경계를 넘거나 실제 다른 서버의 다른 행위자의 메일함으로 전달 될 수 있다. 행위자가 네트워크를 통해 배포 될 수 있지만 여전히 동일한 JVM을 공유하는 것처럼 서로 통신 할 수 있으므로 요구에 맞게 스케일-아웃 할 수 있다.

메시지와 이벤트의 차이점은 메시지는 전송되는 것이고 이벤트는 일어나는 것이다. 메시지는 명확한 도착지가 있지만 이벤트는 0혹은 그 이상(0-N)의 Observer에의해 관찰되고 있을 것이다.

이벤트-주도와 행위자-기반 동시성에 대해 좀 더 세부적으로 들어가보자.

이벤트-주도 동시성
일반적인 앱들은 명령형 스타일(오퍼레이션 순서)로 개발되고 콜스택을 기반으로 개발한다. 콜스택의 주 기능은 프로세스에서 호출자가 블럭되고 리턴값과 한께 호출자에게 컨트롤을 돌려주는 동안, 주어진 루틴에서 호출자를 계속 쫓고, 호출된 루틴을 실행하는 것이다.

겉으로 보았을 땐 이벤트 주도 앱이 콜스택에 맞추는게 아니라 이벤트 트리거에 맞춘다. 이벤트는 0개 혹은 그 이상의 Observer에의해 지켜보고 있는 큐에 메시지로 인코딩 되어 있을 것이다. 명령형 스타일과 비교하여 이벤트-주도의 큰 차이점은 응답을 기다리는 동안 호출자가 한 스레드 위에서 블락되거나 멈추지 않는다. 이벤트 루프 자체는 단일 스레드 일 수 있지만, (단일 스레드 이기도 한)스레드 된 이벤트 루프가 들어오는 요청을 처리 할 수 있도록 허용하면서 호출 된 루틴이 업무를 수행하는 동안 (잠재적으로 IO 자체를 차단하면서) 동시성은 여전히 달성된다. 프로세스가 완전히 끝나지 않는한 요청을 블락시키는 대신에 호출자의 id가 요청 메시지의 바디와 함께 전달되므로 깨어있는 루틴이 이것을 선택하면 호출자는 응답과 함께 콜백될 수 있다. 

이벤트 주도 구조를 선택하는 이유는 콜백지옥(링크)이라는 것에 괴로워할 수 있기 때문이다. 콜백지옥은 메시지를 받는 놈이 정해져 있는 것이 아니라 익명의 콜백이기 때문에 발생한다. 콜백지옥을 해결하는 일반적인 방법은 이러한 문제가 생기는 이유를 잊고 코드에 표현된 이벤트 순서대로 디버깅하기 어려운것도 생각하지 않으면서 온전히 구문(aka, the Pyramid of Doom) 형태에만 초점을 맞춘다.

행위자-기반 동시성
행위자-기반 앱은 여러 행위자 사이에서 비동기 메시지를 보낸다.

행위자는 아래 속성들을 갖는다.
  • 메시지를 받기 위한 메일 박스
  • 각 타입별로 메시지를 어떻게 받는지 결정하기 위해 패턴매칭의 행위자 로직
  • 요청 사이에 컨텍스트를 저장하기 위한 고립된 상태
이벤트-주도 동시성처럼 행위자 기반 동시성에서는 콜스택은 피하고 가벼운 메시지 전달을 지향한다. 행위자는 메시지를 밖으로든 자기자신에게든 보낼 수 있다. 한 행위자는 그 큐의 첫번째 요청을 먼저 제공한 뒤에 처리가 긴 요청을 처리하기 위해 메시지를 자기자신에게 보낼 수도 있다. 행위자-기반 동시성의 큰 장점은 이벤트-주도 구조에서 얻은 장점을 얻을 수 있다는 점이다. 네트워크 경계를 통해 컴퓨테이션을 스케일-아웃하기도 쉽고, 행위자에게 직접 메시지를 전해주기 때문에 콜백 지옥을 피할 수도 있다. 이러한 강력한 컨셉은 설계, 구현, 유지보수하기 쉬우면서 확장성이 있는 앱을 만들 수 있게 해준다. 시공간에대해 생각하거나 깊게 감쌓인 콜백들에대해 생각하는것보다, 행위자 사이에 메시지 흐름이 어떻게 되는지만 고민하는게 더 낫다.

또 다른 주요 장점은 요소들끼리 느슨하게 연결된다는 점이다. 호출자는 응답을 기다리기 위해 스레드를 멈추지 않으므로 빨리 다른 일을 할 수 있다. 호출자에의해 켭슐화되있는 현재 루틴은 필요에따라 호출자를 다시 호출하면 된다. 이것은 다양한 가능성을 열어준다. 콜백 스택이 한 메모리 공간에 있기 위해 앱을 묶어버리지 않으며 행위자 모델은 프로그래밍적인 일보다 구성에 관련된 일을 추상화하여 만듦으로 여러 머신을 통해 루틴을 분배할 수 있다.

Akka는 행위자-주도 툴킷이고 타입 세이프 리액티브 플랫폼의 일부로서 JVM에서 높은 동시성, 분배, 실패에 대한 내성을 가진 행위자-기반 앱을 만들기 위한 런타임이다. Akka는 탄력을 위한 관리계층이나 확장성을 위한 일 분배와 같은 리액티브 앱 개발에 필요한 멋진 기능들을 탑재하고 있다. Akka에대해 더 깊게 보고싶으면 Let it Crash blog를 확인해보길 바란다.

또한 이 세션 일부의 자료로 사용된 Benjamin Erb’s Diploma Thesis의 글(Concurrent Programming for Scalable Web Architectures)을 읽어보기를 강력 추천한다.

결론
위의 모든 것들이 오늘날 앱 개발에 흠집을 내고, 왜 리액티브 프로그래밍이 단지 또다른 트렌드가 아닌지 이야기하며, 그러나 왜 현대 소프트웨어 개발자들이 배워야하는 패러다임인지도 이야기했다. 여러분이 선택하는 언어나 툴킷에 관계없이 반응성을 얻기위해 확장성과 탄력성 또한 탑재하는 것이 사용자의 기대를 충족시키는 유일한 방법이다. 이것은 몇년간 더욱 중요하게 떠오를 것이다.

저자에대해
Kevin Webber는 Lightbend에서 Enterprise Advocate이다. 그는 heritage 구조에서 리액티브 프로그래밍 원칙을 포괄하는 실시간 분배 시스템까지 큰 구조의 트랜지션을 돕는 것에 열정적이다. 남는 시간에 ReactiveTOProgramming Book Club Toronto를 운영한다. 그는 가끔 제 3자에서 자기 자신에 대해 글을 쓰기도 하는데, 이 단락이 그러한 순간이다. 


신고

'개발' 카테고리의 다른 글

[번역] 리액티브 프로그래밍이란?  (0) 2017.03.03
(번역)Android Architecture  (0) 2016.10.01

WRITTEN BY
canapio
개인 iOS 개발, canapio

받은 트랙백이 없고 , 댓글이 없습니다.
secret

NSCodingNSObjectProtocol이라는 클래스 프로토콜이 필요하다. 그리고 그 프로토콜은 구제체가 따를 순 없다. NSCoding을 사용해서 인코딩하고 싶다면 가장 쉬운 방법이 클래스로 만들어 NSObject를 상속받는 것이다.

나는 구조체를 NSCoding으로 감쌀 수 있는 말끔한 방법을 찾았고 거추장스러운 작업 없이 저장할 수 있다. 예제로서 Coordinate를 사용할 것이다.

두개의 스칼라 프로퍼티를 가지는 간단한 한 타입이다. 이제 NSCoding을 따르고 Coordinate를 감싸는 클래스를 만들어보자.

이 로직을 다른 유형으로 사용하는 것이 좋으며 단일 책임 원칙에 더 잘 지킨다. 눈치 빠른 독자는 위 클래스에 Coordinate 프로퍼티의 EncodableCorrdinate가 옵셔널이라는 것을 눈치 챘을 것이지만, 꼭 그럴 필요는 없음을 알 수 있을 것이다. 우리는 옵셔널이 아닌 Coordinate를 받는(혹은 실패하게 만드는) 생성자를 만들 수 있는데, 그렇다면 이미 init(coder:) 메소드는 불가능하다. 그리하여 EnabledCoordinate 클래스 인스턴스를 들고 있을때 항상 coordinate를 가지고 있음을 보장해주어야한다.

그러나 더블(Double)타입(혹은 어떤 원시 타입)을 인코딩 할 때 NSCoder 동작 방법의 특징으로 인해 Any?를 반환하는 decodeObejct(forKey:)로 추출해낼 수 없게 되었다. DoubledecodeDouble(forKey:)로 하여 특정 윈시타입에 특정 메소드가 있다. 불행히도 이 특정 메소드는 옵셔널을 반환하지 않으며, 키가 맞지 않거나 오류가 나면 0.0을 반환해 버린다. 이러한 이유 때문에 coordinate 프로퍼티를 옵셔널로 두기로 했고 옵셔널로 인코딩한다. 그리하여 나는 decodeObject(forKey:)를 사용해 Double?을 얻어냄으로써 추가적인 안정성을 보장했다.

이제 Coordinate 오브젝트를 인코딩/디코딩 하기 위해 EncodableCoordinate를 하나 만들고 NSKeyedArchiver로 디스크에 저장할 수 있다.

이러한 추가적인 오브젝트를 만드는게 이상적이진 않으며, 나는 "가능하면 나를 캐싱해줘" 글에서 사용한 SKCache와같은 오브젝트로 작업하기를 좋아한다. 그리하여 내가 인코더와 인코딩 된 사이의 관계를 형식화 할 수 있다면, 아마 매번 NSCoding 컨테이너를 만들지 않아도 될 것이다.

그 끝으로 두가지 프로토콜을 추가하자.
그리고 우리의 두가지 타입에 대해 적용시킨다.

이렇게하여 이제 타입 시스템은 오브젝트 쌍에서 타입과 값 사이에서 어떻게 변환하여 넣고 빼는지 안다.

블로그 포스팅에서 나온 SKCache 오브젝트는 Encoded 타입을 넘어 제네릭으로 업그레이드 되어왔다. 인코더 값 타입이 그 자신이라는 조건으로, 이것은 이 두 타입 사이에 쌍방향으로 변환할 수 있게 해준다.

이 타입에서 마지막 퍼즐조각인 savefetch 메소드가 남아있다. saveencoder를 잡아두고있고(사실 이것은 NSCoding을 따르는 오브젝트이다) path로 저장해둔다.

패칭은 약간의 컴파일러 댄스(compiler dance)를 포함한다. 우리는 언아카이브(unarchive)된 오브젝트를 T.Encoable로 캐스팅 해야하는데, 이것은 encoder 타입이다. 그리고 그 값을 잡아두고, 동적으로 그것을 캐스팅하여 T로 돌려준다.

이제 캐시를 사용하기위해 한 인스턴스를 만들고 Coordinate 제네릭으로 만든다.

이렇게 하면 좌표 구조체를 쉽게 저장하고 검색할 수 있다.
이것을 가지면 NSCoding을 이용해 구조체를 인코딩할 수 있고, 단일 책임 원칙에 따렴, 타입 세이프티하게 만들어준다. 


신고

WRITTEN BY
canapio
개인 iOS 개발, canapio

받은 트랙백이 없고 , 댓글이 없습니다.
secret