Promise(약속)와 Deferred(지연)로 기존의 코드 리펙토링
자바 개발자 중에 외국의 구루(Guru)급 개발자 및 저자인 마틴 파울러가 쓴 '리펙토링'이라는 책이 있다. 언제 한 번 이 책을 어느 집구석에서 직접 훑어본 적이 있었는데, 책이 너무 추저워서 그냥 내팽개쳤었다. 그런데 이 책이 인터넷에서 다시 구하려고 해도 중고가가 부르는 게 값이었다. 번역판 정가가 3만 원이었는데, 어떤 중고책 소유자가 10만 원 이상으로 책정하고 있어서 깜짝 놀란 적이 있었다(그런데 지금은 그것도 팔리고 없었다). 실제로 이 책은 이미 절판되어서 보고 싶어도 그 추저운 책을 다시 볼 수밖에 없지만, 그 집구석은 다시 갈 수가 없다. 리펙토링에 대해서는 유지보수의 효율성을 제고하기 위해서 거쳐야 할 단계인데, 아래와 같은 내용을 방금 언급한 책의 독자평에서 확인할 수 있었다.
책의 해당 예제를 리팩터링 하는 과정을 따라 해 보면서 리펙토링의 목적이 소프트웨어를 더 이해하기 쉽고 수정하기 쉽게 만드는 것임을 알게 되며, 몇 가지 기법을 바로 배우게 됩니다.
그런데 예제를 리팩터링 하는 작업을 완료하고 보면 분명 코드 효율성과 구조는 체계화되었으나 성능은 오히려 떨어질 수 있음을 알게 되는데, 일부 프로그래머들이 리팩터링 자체의 목적이 성능 향상을 위한 것으로 오해하지만 사실 리팩터링 작업은 성능 최적화를 위한 것이 아닙니다.
리팩터링을 실시하면 오히려 단기적으로 소프트웨어는 더 느려지지만 리팩터링을 통해 소스의 추가 수정이 용이하게 즉 튜닝이 가능하게 만들어 놓으므로 나중에 속도가 나오게 튜닝할 수 있는 것입니다."
위와 같은 목적으로 coffeeOrder App의 비동기식 코드를 사용하는 새 패턴을 배우고자 기존의 코드를 리팩터링 하였다. 새로운 개념은 디퍼드(Deferrd)와 프로미스(Promise)이다. 디퍼드는 '연기한, 거치한'이라는 뜻으로 아래 로직과 같이 $.ajax에서 디퍼드 객체를 반환하고, 디퍼드에 있는 then 메서드를 통해 디퍼드가 처리될 때 실행될 콜백을 등록한다. 콜백이 실행될 때, 서버의 응답으로 되돌려준 값이 전달된다(아키노, 2017).
무슨 말인지 선뜻 이해하기가 힘들어서 다른 객체인 프로미스(Promise)를 살펴보고 다시 두 개념을 한 번에 조합해서 로직의 순서를 따라가 보자. 프로미스 객체는 아래와 같이 항상 대기(Pending), 수행(fulfilled), 거절(rejected)의 세 상태 중 하나의 상태에 있다(아키노, 2017).
금일 점심때 잠깐 IELTS 영어 학원에서 레벨테스트를 받고 상담을 받았는데, 스피킹과 라이팅에서 보통의 비영어권의 종합 7점대 이상의 수험자라도 실수하는 것이 묻는 말에 대한 답변만을 하지 않고 장황하게 얘기하다가 결국엔 토픽에서 벗어나는 것이라고 한다. 이것을 오프 더 토픽(Off the topic)이라고 상담강사가 얘기해줬다. 지금 프런드 웹 개발 실습과정의 진행을 위해 참조하는 번역본을 보면 지금까지 익숙한 개발 용어를 통해 개념 환기가 되는데 반해 Github에서 다운로드한 온라인 원서를 보니 같은 의미의 용어이지만 익숙하지 않은 영어 단어가 나열되어 쉽게 맥락에서 벗어나는 것은 비단 입문 개발자의 콤플렉스만이 아닐 거다. 어렵지만 처음부터 영문의 설명을 이해하기 위해 계속 맥락을 짚는 연습을 해야 이해할 수 있겠지만, 사실 원서의 내용이 단번에 머리에서 처리되지 않는 까닭은 모국어로도 해당 개념에 대한 전반적인 이해도가 떨어지기 때문이다.
원서에서 'let the caller chain a then off of that Promise'라는 문맥이 자꾸 오늘 상담하면서 들은 'Off the topic'이라는 문구와 오버랩되는 바람에 'let off'라는 구동사(Pharse verb)가 떠올라 "그 caller chain에서 then을 벗어나게 하다"라고 해석하게 만들었다. 그런데 번역본에는 "프로미스 객체를 반환하여 호출자가 프로미스에 then을 '연결할 수 있다'라고" 의역되어 있다. chain이 동사로 쓰여 주어인 'the caller(호출자)'로 하여금 목적어인 a then을 묶다(→연결하다)라고 의역했는데 다시 보니 맞는 번역인 거 같다. 그리고 이어서 바로 목적격 보어 off(chain이 타동사로 쓰였고 보통은 to가 나와서 '~에 매다'라고 해석하는데)가 나왔으므로 뒤따라 나오는 'that Promise' 객체에서(of) 벗어나서(→ 반환하여) 연결시키다'로 해석했다.
프로미스 객체에 대해 해당 번역 부분을 좀 더 자세히 살펴보겠다.
모든 프로미스 객체에서 프로미스가 수행되었을 때 트리거(곧바로 수행하는)가 이루어지는 then 메서드가 있다. then 메서드를 호출하고 이것을 콜백 함수에 전달할 수 있다. 즉 한 프로미스가 수행되면 그것의 동작에 의해 then 메서드를 전달받은 콜백 함수는 실행된다. 이러한 '비동기식 작업'(아래 참조)이 이루어질 때 프로미스가 전달받은 어떤 값이라도 해당 콜백 함수에게 전달한다.
이 애플리케이션에서의 '비동기식 작업'이란 아래와 같다.
폼 제출 → 주문 생성 → 서버 저장 → 주문 출력 추가 → 폼 초기화
[커피 주문을 추가하는 비동기식(서버의 응답에 관계없이 계속 주문 가능) 흐름]
한꺼번에 여러 개의 then 메서드 호출자를 연결시켜서 하나의 지점으로 만들 수도 있다. 콜백을 전달받아 수행하는 함수를 일일이 작성하는 대신에 프로미스 객체를 반환할 때, 프로미스(Promise) 객체를 호출(실행)함으로써 여러 then 메서드와 동시에 연결시킬 수 있다. [필자의 의역]
어떻게 보면 번역본의 저자가 좀 더 효율적으로 번역한 거 같기는 하다. 단지 주어와 목적어의 순서가 뒤엉키어서 주어 입장에서는 객체를 받아서 연결시킨다고 말한 번역본의 내용과 필자가 정정한 의미는 동일한 맥락이다. 하지만 이 책의 역자(개발 경력자)는 해당 소스로 직접 구현은 안해봤는지 한 코드 소스에서 지워야 하는데 지우지 않은 부호가 나와서 언급하지 않으래야 않을 수가 없었다. 코드에 대한 검증이 안이루어진 번역본에 대해서는 누구를 탓해야 하나? 더군다나 원작에서의 소스는 아니다 다를까 맞게 되어 있다면? 욕은 전부 역자 몫이다. 많은 IT 서적의 번역 품질에 대한 리뷰가 극과 극을 가른다. 그렇다면 본인이 현업에서 직접 활용하지 않고 또한 그 분야의 실무자에게 직접 해당 내용에 대해 베타 테스트를 하지 했으면 좋겠다.
아무리 번역에 일가견이 있더라도 해당 번역을 하려는 책 내용의 테스트에 대한 시행착오의 스키마를 가지고 있지 않은 역자라면 번역하려는 글의 맥락에 대한 이해도를 독자에게 쉽게 전달해줄 수는 없다. 그러면 차라리 원본 서적을 보는 게 '일억 오천 배'는 낫다. 왜 번역을 제2의 창작이라고까지 얘기하겠는가? 그것은 번역 이후에는 원작자와는 완전히 다를 수도 있는 번역가의 스키마에 따라서 얼마든지 다른 내용으로 책이 편집될 수도 있기 때문이다. 하지만 해당 분야에서 직접 시행착오를 겪어본 사람이 필요에 의한 번역을 한다면 앞서 필자가 번역본의 맥락에 대해 다르게 해석할 수 있는 정도의 버퍼는 양산할 수 있더라도, 큰 이해의 틀에서 접근할 때 독자가 굳이 원서를 찾아봐야 할 강박관념은 덜어줄 수 있다.
필자는 심지어 한국 보안 전문가 중에 본인을 해커로 자부하는 사람이 번역한 실습 형태의 외국의 IT 보안 서적에서조차 책 속의 소스가 구현되지 않는다는 리뷰까지 접했었다. 그러면 무엇을 해킹하라는 말인가(해킹은 불법이라서 일부러 작동은 안 되게 표기했다는 말인가)! 소스는 독자한테 베타 테스트해라고 던진 건가?(차라리 책 쓰려는 작가가 본인의 방구석에서 이웃의 Wi-Fi 암호를 해킹한 사례를 보여줄 수 있는 소스가 책으로 출판되는 것이 독자의 구미를 더 맞춰줄 것이다. 다음날 그 저자의 양손에 수갑이 차져있겠지만 말이다;)
번역본의 소스코드에서 잘못된 철자가 있었는데, 아래 캡처 화면의 세미콜론(;)을 제거하지 않아서(이것을 코딩하는 소스 화면에는 제거 표기가 안되어 있었지만, 그다음 첨부사진의 해당 라인이 보이는 곳에는 또 제거되어 있었다), myCafe.createOrder.call 콜백 함수에 .then 메서드의 호출을 시도하려는 것이 안된 것이다. 다행히 #12 섹션부터는 아예 원서를 보고 시작해서 해당 소소의 에러를 디버깅할 필요 없이 잡을 수 있었다. 그리고 이번 섹션의 분량이 번역에 대한 본인의 개똥철학으로 더욱 장황하게 되었다(스-파게리!).
이어서 실습을 진행하면, 디퍼드 객체의 반환 부분이다. 디퍼드 객체는 jQuery의 $.ajax 메서드($.post와 $.get을 포함)에 의해 반환된다. 디퍼드 객체도 프로미스 객체처럼 수행과 거절 두 상태를 위한 콜백을 등록시킬 수 있는 메서드가 있다. jQuery의 Ajax 메서드에 의해 생성된 디퍼드를 반환하도록 RemoteDataStore 업데이트를 시작한다(아키라, 2017). 그러면 아래와 같은 임시로 테스트한 Ajax의 디퍼드('거치')한 애플리케이션의 처음 시작화면을 볼 수 있다.
결국 디퍼드(Deferred, 거치한) 객체를 활용하면 다음과 같은 리스크를 관리(이것이 소스의 추가 수정(유지보수)이 용이하게끔 만들어 나중에 속도가 나오게 튜링이 가능한 리펙토링, Refectoring이다)할 수 있다.
비동기식(서버의 응답(주문 완료)을 기다릴 필요 없이 지속적인 주문 요청이 가능) 단계에 의존적임.
커피 주문을 위한 모듈을 콜백이라고 불리는 함수 인자를 통해 상호작용함으로써 발생하는 콜백 중첩 현상
이런 방법으로 인해 향후 다루기 불편해지고 발생의 소지가 있는 오류(폼 핸들러의 addSubmitHandler의 코드량 증대 ->"스파게티 코드")
그래서 이 책의 저자는 처음에 Promise(여기서의 쓰임은 약속이라는 일대일 대응의 의미보다는 '뭔가의 조짐이 좋다'라는 promising의 뉘앙스가 더 어울린다) 객체를 통해 아주 복잡한 비동기식 코드를 쉽게 관리할 수 있도록 설계하는 방법, 즉 리펙토링에 대한 개념을 보여주는 것이었구나를 알 수 있다.
프로미스는 비동기식의 주문 흐름에서 현재의 단계에서 오류 없이 성공했을 때, 다음 단계를 수행하는 데 주로 관심을 가지고 이를 간단하게 만든다. 콜백 인자에 의존하는 대신, 모듈을 더 세분화시킬 수 있도록 프로미스 객체를 반환한다(아키라, 2017). 즉 커피를 주문받는 바리스타(카페 주인)가 사정에 의해 주문한 커피의 제공을 거절할 수도 있는 기능을 추가하여 로직의 흐름을 세분화시킬 수 있다.
추가적으로 기존의 RemoteDataStore(원격 저장소)뿐만 아니라 DataStore(로컬 저장소)에도 Promise 객체를 줘서 서로 호환이 가능하도록 하는 실습과정까지 끝마친 후 해당 소스 코드를 본인의 github repository에 commit 시켰다.
이번 실습 과정은 필자가 완전히 이해해서 진행하기에는 자바스크립트를 상당히 깊이 있게 사용할 줄 알아야 하는 내용이었다(마지막까지 디버깅하느라 힘들었다). 이 책의 다음 내용은 Node.js를 이용한 채팅 프로그램을 만드는 것인데, 보통은 클라이언트단에서 널리 쓰이는 자바스크립트를 서버단에서도 활용할 수 있는 라이브러리인 Node.js를 소개하는 것이 목적이다. Node를 사용하면 js(자바스크립트) 코드에서 파일 시스템, 데이터베이스 그리고 네트워크에 접근이 가능하기에 합쳐서 Node.js라고 부른다. 어쨌든 자바 언어를 통해 소켓 프로그래밍을 한 경험이 있는데 이 웹소켓(socket.io)을 설정해서 채팅 서버 기능을 만들어 보는 장이다. 취약점 도출에도 도움이 되는 내용(3-way handshaking established process)이 있어서 다음 섹션에서 마저 진행하겠다.
참조
1) 크리스 아키노. (2017). FRONT-END WEB DEVELOPMENT: The big NERD ranch guide. (2016). 한국 번역판 역자 이지은(2017). 제대로 배우는 프런트엔드 웹 개발. 서울 : BJ(비제이 퍼블릭).
2) PDF document from github, ‘FRONT-END WEB DEVELOPMENT: The big NERD ranch guide.’