올해를 돌아보고 내년을 계획하자는 생각으로 글을 쓰기 시작했는데 너무 길어졌다. 그냥 먹고 지낸 이야기인데 다 쓰고 보니 두서없이 우울한 이야기가 많아서 올려야 하나 고민을 하다가 올린다.

View this post on Instagram

내 삶에서는 가장 큰 변화가 있던 해였다. 나를 누구보다도 잘 이해하는 민경 씨와 평생을 함께할 약속을 모두 앞에서 했다. 어떤 고민이라도 이 사람 앞에만 가져가면 금방이라도 해결할 자신감이 생기는데 일과 비자의 문제로 각자의 공간에서 생활하고 있어 아직은 함께 의논하는 데 어려움이 있긴 하다. 연초에는 이 일로 한국도 두 차례나 다녀와서 정신없었다. 짧은 시간이었지만 양가 가족 모두 새로운 만남과 함께 즐겁게 지낼 수 있었다. 그사이에 서류 작업도 해야 했고 여러 가지 신경 쓸 부분이 알게 모르게 많았다. 그런 탓에 이 일 외에는 다른 일에는 전혀 신경을 쓰지 못했다. 호주에서도 서류 작업을 기다렸던 때에도 내가 어떻게 할 수 없는 부분에 대해서 고민하는 일이 잦았고 정말 많은 스트레스를 받았던 기억이 또 떠오른다. 어느 나라든 서류 절차에 앞서 가져야 할 마음가짐은 역시 평정심이다.

큰 변화를 기다리면서 올 한 해는 정말 새로운 일을 하나도 하지 않았고 기존에 하던 일에서도 착실함을 잃었었다. 그만큼 회고를 쓰자고 마음 먹었을 때 내 부끄러운 부분을 얼마나 들춰내야 할지 걱정이 앞섰다. 내 삶을 부지런히 측정하지도 않았으니 무엇을 어떻게 개선해야 하나 고민하기도 어려울뿐더러 무슨 일을 했는지도 기억도 잘 나질 않았다. 한 해 동안 개발도 블로그도, 심지어 트위터도 열심히 하지 않았고 글을 쓰는 일에 거리감마저 생겨서 무엇 하나 적어둔 일이 별로 없었다. 항상 무엇을 해야 하는가 고민했지만, 어느 하나 행동으로 연결되는 일이 없었다. 이상한모임 활동에서도 이런 내 개인적인 상황이 자꾸 영향을 줘 모두에게 피해가 되는구나 하는 마음이 자꾸 들어 피곤했다. 이런 기분이 시작부터 마지막 12월까지 들었고 슬랙을 포함한 커뮤니티 활동 모두 너무 힘들었다. 괴로운 나머지 흥미를 잃어버리는 것만큼 두려운 일이 없다.

View this post on Instagram

올해는 오랜 기간 다닌 회사를 그만두고 새 회사로 옮기는 것으로 시작했고 대학에서 1년을 채웠다. 이전 글에서도 쓴 것처럼 대학 내 부서라는 특수한 환경에서 다양한 업무를 접하고 있다. 큰 기업에서의 프로세스를 경험해보지 못했던 터라 사소한 것 하나도 서류 작업부터 시작하는 일은 한 해 꼬박하고도 어색하다. 합류 후 할 예정이던 첫 프로젝트는 내 코드 리뷰 후 비지니스 분석과 괴리가 지나치게 큰 상태라고 진단했는데 이미 예산을 많이 쓴 상태라서 그대로 접혀버렸다. 그렇게 프로젝트를 엎은 후에 반년 가까이 business as usual 만 했다. 그래서 연초에 들어왔는데 9월이나 되어서야 첫 프로젝트를 하게 되었다. Business Analyst와 Project Manager를 두고 일해본 경험은 처음이었고 정말 이렇게 개발에만 집중할 수 있다는 점이 너무나도 즐거웠다. 후반부에는 비지니스가 요구사항을 구현 이후에야 바꾸는 등의 문제가 있어서 모두를 답답하게 만들긴 했지만 11월 말에 프로젝트를 마무리하고 실제 사용자에게 상당히 좋은 피드백도 받게 되었다. 이번 프로젝트에서 기존 프로덕트에서의 사용성과 성능 문제를 대폭 해소한 덕분에 내년 로드맵에는 상용화에 대한 디스커버리 프로젝트도 잡혔다. 결과적으로는 모두에게 만족스러운 결과를 만들었지만, 이 과정 자체에서는 비지니스 탓에 스트레스가 좀 있어서 마냥 좋지만은 않은, 그런 복잡한 감정의 프로젝트였다.

그리고 중간에 대규모 조직개편이 있어서 팀이 변경되었고 기존에 있던 많은 사람이 일을 정리했다. 나야 컨트랙터로 일하고 있으니 이런 정치적인 문제에 휩쓸릴 필요는 없긴 했지만 사람 일이 그렇게 영향 안 받기란 쉽지 않은 것 같다. 개편 이후에 소속된 팀에서는 전에서의 팀과는 다르게 영 소속감을 느끼지 못하고 있다. 나와 같은 스택으로 일하던 사람도 그만둬버려서 PHP 개발자로는 유일하다. 모두가 자바 얘기하는데 혼자만 PHP하고 있으니 알게 모르게 소외감 같은 게 드는 건 어쩔 수 없는 것 같다. 팀 바뀌고 나서야 알았는데 PHP는 미니 앱이라며 FTP로 배포하라 해놓곤 자기네는 젠킨스고 뭐고 리소스 펑펑 쓰고 있어서 분한 기분마저 들었다. 나는 이 회사에서 살아남을 수 있을 것인가!

View this post on Instagram

입사 당시에는 프론트엔드가 75%고 25%는 PHP를 하게 될 것이라 했지만 점점 거리가 멀어지고 있었다. 얼마 전 끝낸 그 프로젝트에서는 그래도 ng1을 사용하고 있었다. 사내에서 프론트엔드 비중이 상당히 낮아서 요즘 버전으로 올리려는데도 말이 많았다. 말 많은 건 안 좋은 신호다. 일단 ng1도 제대로 된 코드로 작성되어 있지 않아 ng1 레퍼런스 프로젝트를 만든다 생각하고 개발했었다. 그런데 내년 초까지 하게 될 프로젝트는 순수하게 PHP인 데다 흔히 PHP로는 만들지 않는 그런 도구라서 별로 맡고 싶지 않은 프로젝트였다. 다른 개발자가 그만두지 않았다면 그 사람이 이 프로젝트를 했을 텐데 나 혼자만 남았으니 다 떠안게 된 케이스다. 이렇게 쓰고 나면 그냥 이직하면 되는 시점이긴 한데 이 프로젝트를 12월에 맡게 되었다. 방학이 시작되니 학교엔 사람 없고 사무실엔 대부분 휴가를 떠나 절반만 있고 나머지도 긴 긴 점심 먹으러 사라졌다. 업무를 물어볼 사람도, 그만둔다 만다 얘기할 사람까지도 다 휴가를 갔다. 12월은 없는 달이나 마찬가지였다.

만약 여기서 평생 다닐 생각이라면 (그게 목표라면) 정말 좋은 회사다. 성장에 대한 욕심도 별로 없고 최우선 고객은 대학이니 별로 경쟁도 없다. 가끔하는 구조조정에만 버텨내면, 즉 관리자 포지션으로 올라가지만 않는다면 나갈 일은 없을 것 같다. 15년, 20년 다닌 사람도 많은 것 보면 왠지 이해가 되는 환경이다. 게다가 학교는 다양한 시스템이 얽혀 있어서 도메인 지식을 많이 필요로 하더라. 게다가 수십 년의 데이터도 누적되어야 하기 때문에 끝없는 마이그레이션과 인티그레이션이 필요한 곳이라 한번 필수 인력이 되면 정말 나갈 일이 없다. 자체 데이터 센터에 모든 아카데믹 스태프의 이메일 아카이브만 몇 페타 저장되어 있단다. 쉽게 보일지 몰라도 그간 아이덴디티 체계도 여러 번 변경되었고 이메일 서버의 아키텍처도 여러 차례 변경되었으니 쉽게 열어볼 수 없는 그런 아카이브인데 근 몇 년 데이터에 대해서만 o365로 접근 가능한 상태란다. 이런 역사를 모르면 영영 알 수 없는 환경이다. 이런 환경에서 지난 구조조정에서 패키지 받고 우르르 나간 탓에 업무 공백이 상당히 많았는데 이런 배경이 그대로 드러난 것이다.

View this post on Instagram

그 전에 다니던 회사는 하루에도 여러 웹사이트/웹서비스를 봐야 할 정도로 바쁘고 CS도 직간접적으로 해야만 하는 상황이라 정말 바빴었다. 그래서 다음에 일하고 싶었던 곳은 웹서비스를 운영하는 그런 회사였다, 여기는 생각하던 그런 서비스 회사는 아니지만 오래 유지한다는 것이 어떤 의미인지 정말 크게 배우고 있다. 이런 대규모의 환경에서 지속 가능한 환경을 만들기 위해 도입한 수많은 프로세스도 이해가 간다. 매번 서류에 치이고 있더라도 말이다. 게다가 수많은 이해 관계 속에서 어떻게 비지니스를 끌고 나가는가, 하나의 프로젝트에 수많은 stakeholder를 두고 business engaging을 하고, 요구사항을 분석하고 프로젝트로 꾸리는 거하며, 서비스 제공자인 비지니스 입장에서 개발하다가도 노조와의 협약을 위반하지 않았는지 수시로 점검하는 부분들(새로운 기능은 물론 새로운 버튼을 넣는 것까지 문제가 될 수 있다는데), 그 와중에도 프로토타이핑으로 요구사항과 프로덕트의 괴리를 줄이려는 노력이라든지, 최종 사용자를 모셔놓고 사용자 경험이 어떤지 테스트를 하는 등의 작업은 짧은 호흡의 회사에서는 전혀 겪어보지 못한 경험이었다.

긴 스코프로 일을 하다 보니 버퍼로 주는 시간도 상당히 많았다. 게다가 대학이니 자유롭게 도서관도 사용할 수 있고 논문도 열람할 수 있었다. 궁금한 부분은 구글링으로도 해소할 수 있지만, 아카데믹 자료를 아무 때나 찾아볼 수 있다는 점은 정말 매력적이다. 대학에 학적 있을 때는 논문 검색을 몇 번이나 이용했나 싶었는데 역시 필요가 앞서야 한다. 게다가 논문이란 단어가 주는 벽이 좀 허물어졌다. 블로그에나 쓸 만한 글을 논문으로 낸 경우도 많이 봤는데 리뷰가 된 글이라 그런지 몰라도 하나를 읽어도 이건 이렇구나, 저건 저렇구나 이해하기 좋았다. 그렇게 읽고 PoC도 짜보고 하면서 버퍼를 나름 알차게 쓰려고 노력했다.

이직할 생각이 문득 들어서 연락도 여럿 해보고 인터뷰도 봤다. 하지만 오래 다니지 않을 거란 생각을 하니까 이직할 동인도 그다지 크지 않았다. 그게 문제였다. 적극적으로 알아보지도 않았던 데다가 다른 스택으로 알아보다 보니 아무래도 지금 받는 것보다 적었다. 그리고 학교 다니면 어떤 스택으로 더 하고 싶을지 모르는데 지금 덜컥 다른 경력 만들 필요가 있나 싶기도 하고 학비를 모은다고 생각하면 많이 받는 곳에서 그냥 가만히 있는 게 낫지 않나 싶었다. 지금 생각하면 스트레스를 받는 상황에서 별로 건강하지 못한 결정을 한 것 같다.

그렇게 회사에서 바쁘게 페이퍼나 서류 읽고 미팅 갔다 서류하고 코딩 조금 하면 퇴근할 시간이 되었다. 퇴근하면 녹초가 될 수밖에 없었다. 퇴근하고는 저녁 차려 먹고 넷플릭스 몇 편 보고 게임(Dota 2랑 인서전시) 한두 판 하면 잘 시간이 되었다.

지난해 낸 역서는 번역이 엉망이란 피드백을 몇 차례 들으니 출판사에도 죄송하고 원저자에게도, 구입한 분들에게도 미안했다. 그런 탓에 번역 자체도 잘 안하게 되어 번역글도 별로 올리지 않았다. 블로그에도 뭐라고 글을 써야 할지 막막해서 아무런 글을 올리지 못했다. 분기 회고를 쓰고 좀 열심히 해봐야지 했는데 하나도 하질 않았다. 페이스북에도 글을 거의 안 올렸고 트위터에도 별로 의욕이 생기지 않았다. 새로운 글도 책도 안 읽었다. 공부도 안했다. 리액트도 보겠다고 하곤 하나도 보지 않았고 한다고 했던 자격증 공부도 전혀 하질 않았다. 주말엔 자고 밀린 집안일 하기 바빴다. 가끔 텀블러에 짧은 글 쓰는 것 외에는 아무것도 안 했다. 사진도 재미없었고 영화도 지루했다. 토이프로젝트도 전혀 안 했고 이상한모임 활동도 열심히 하지 못했다. (그래서 더 미안하다) 아무것도 하지 않으면서도 커뮤니티에서, 트위터에서 별 사소한 말에 다 상처받고 우울한 기분이 늘 반복되었다. 기분이 우울하면 트위터에서 맛있는 음식 먹는 사진만 봐도 와 나는 맛있는 것도 안먹고 뭐하고 사나 더 깊은 우울감이 생기고 그랬다. 아무런 활동도 하고 싶지 않았다면서도 실제로 하지 않아서 스트레스받았다. (쓰고보니 이상한데 정말 그랬다.) 그래도 매일 속으로는 이렇게 펑펑 시간 보내면 안되는데 하면서도 계속 놀았다. 계속 이렇게 지내면 안 된다 생각하니 아무것도 안 하면서도 스트레스는 계속 받았던 것 같다. 장작이 없으니 불은 점점 사그라졌고 일상이 참 피곤하게만 느껴졌다. 그냥 열심히 지내면 되는데, 쉽게 생각할 순 있지만 그게 행동으로 옮겨지질 않았다. 번아웃이라고 말하긴 싫지만 그런 비슷한 상태로 올 한 해를 보냈다. 이런 바닥인 모습을 쓰고 싶지 않지만 돌아보니 올해 내가 그랬다. 그래도 이번 달에는 좀 신경 써서 덜 그러려고 노력하고 있지만.

View this post on Instagram

그나마 민경 씨가 와서 지낸 시간, 함께 여행을 다닌 시간이 가장 위로가 되었다. 처음으로 가본 시드니에서 오페라하우스를 다녀오기도 했고, 멜번에서도 같이 지낸 시간이 얼마나 소중했고 고마웠는지. 폰에 저장된 사진을 다시 열어보면 함께 보낸 시간이 너무 감사하기만 하다. 이렇게 고마운 사람을 위해서라도 다시 힘내고 열정있는 삶을 되찾아야 할 텐데. 함께 보낼 내년이 더 기대되는 이유기도 하다.

그래서 새해에는 무엇보다 욕심을 좀 버리고 성취 가능한 수준에서 계획을 짜야겠다는 생각을 했다. 당연한 얘기지만 늘 과하게 계획을 짜기도 했었고 매년 그냥 적고 보는 계획이 많았으니 이번엔 딱 세 가지만 해야지 생각했다.

책을 많이 읽기로 했다. 새해 전부터 시작하려고 리디북스에 사놓고 안 읽은 책부터 하나둘 읽어가고 있다. 여전히 읽으면서도 한쪽엔 트위터 열어 새로고침 하면서 괜히 우울한 기분을 유지하게 되는 것 같다. 왜 트위터만 보는데도 우울한 것일까. 그리고 멀티태스킹 그만해야 한다 정말.

글을 꾸준히 쓴다. 대신 블로그는 정리할까 고민하고 있다. 블로그도 있으면 좋긴 한데 블로그라는 양식에 이상하게 에너지를 많이 빼앗기는 느낌이다. 예를 들면 조회 수라든지 댓글이라든지 신경 안쓴다 해도 쓰이는 것 같다. 그래서 블로그와는 다른 환경으로 글을 쓸 수 있는 공간을 마련하고 싶다. 게다가 매달 나가는 고정비도 좀 줄이고 싶어서 좀 더 가볍거나 정적 사이트와 같은 형태로 바꿀까 생각하고 있다. 연말에 go 공부하고서 정적 페이지를 만드는 도구를 간단하게 만들어보고 있는데 아마 이걸 쓰지 않을까 고민한다. 연휴 동안 만들어서 적용해야지 했는데 붙여놓고 안 하니 좀 시큰둥해지긴 했다. 새로 만들면 좀 침울한 기분도 가시지 않을까. 뭐 이런 건 다 부수적인 부분이고 생각도 감정도 정리할 겸 글을 부지런히 쓰는 것에 중점을 뒀다. 책을 읽고서도 글로 정리하는 습관을 지니려고 한다.

이제 학교 갈 준비를 해야 하니까 영어 공부를 또 제대로 해야 한다. 그냥 막연하게 생각하고 있는데 토플 준비할까 생각하고 있다. 클래스만 들어도 문제없긴 하지만 뭔가 명확한 목표를 갖고 준비하는 게 좋을 것 같아서 시험 대비를 하는 편이 낫지 않나 싶었다. 구체적으로는 차차 정해볼 생각이다.

개발에는 딱히 새로운 목표를 세우지 않기로 했다. 정서적인 충전을 좀 먼저 해야 할 것 같다. 내년에는 생활 공간도 달라질 예정인데 이 변화를 자연스럽고 긍정적으로 받아들여 열심히 생활했으면 좋겠다. 그 전까지는 지금 회사에서 계속 있을 생각인데 더 힘들다 느껴지면 아마 정리하고 다른 일은 하지 않고 좀 쉴 것 같다. 이직도 생각하긴 했지만 새로운 직장에 적응될 차에 또 그만두는 건 별로 맘에 들지 않았다.

생각을 행동으로 옮기는 한 해가 될 수 있었으면 좋겠다. 힘내자.

리디북스서 글쓰기에 관한 책을 떨이로 팔길래 예전에 구입해두고 읽지 않고 있던 책이다. 나름 글을 잘 쓴다는, 말도 안되는 착각이 먼저 있었다. 많이 들어본 사람이 썼다면 무슨 내용을 썼을까 궁금하다고 바로 읽었을텐데 그러기엔 서민 교수님을 잘 몰랐기도 했다. 근래 책도 잘 안읽고 글도 선뜻 쓰기 두려워졌던 차에 이 책이 눈에 들어왔다. 얼마 전 페이스북에서 서민 교수의 일침 어쩌고 같은 영상을 봐서 그런지 다른 책보다 더 눈에 들어왔다.

전반부에서는 자신의 글쓰기 경험을 인용을 곁들여 무용담처럼 쓴다. 뒤에서는 잘된 글과 별로인 글의 예시를 들어 실제적인 해법을 많이 이야기한다. 쉽게 지루해질 수 있는 주제를 짧은 호흡으로 빠르게 이어간 덕분에 술술 읽을 수 있었다. 대부분 작문서에서 말하는 방식으로 많이 읽고, 많이 쓰고, 많이 생각한다는 구양수의 틀을 벗어나지 않는다. 하지만 이 방식을 고루하지 않고 현대를 사는 사람이라면 누구든 따라갈 수 있는 방식으로 친절하게 알려준다. 특히 글을 쓰는 과정과 피드백을 받는 과정을 반복하면서 마치 글로 실험하는 경험이 자주 나와 무엇을 하면 되고 하면 안되는지 실험 결과를 함께 살펴보는 기분이 든다.

책을 읽고 글을 쓰는데 있어서 여러 실천적인 방법을 배울 수 있었다. 글을 계속 쓰기 위해서 블로그나 일기를 쓰라는 이야기도 와닿았지만 책을 읽고 글을 쓰도록 서평 쓰기를 권하는 것도 흥미로웠다. (작가는 알라딘에서 활동한단다.) 커뮤니티에서 서평이란 양식 안에서 다양한 주제로 글쓰기와 피드백을 반복한다면 누구든지 없던 글 실력도 생길 것만 같았다.

신년에는 책을 더 읽어야겠다고 막연히 생각했었다. 사실 매년 막연하게 생각하고 실천에 옮기지 않았다. 짧게라도 서평을 작성하면 좀 더 부지런히 읽지 않을까 싶어 신년에는 서평 남기기도 계획에 더했다. 서민식 지옥훈련이 될 수 있었으면 좋겠다.

서민, 서민적 글쓰기, 생각정원 출판.

Published on December 6, 2017

TypeScript Handbook의 Namespaces and Modules를 번역했다.


용어에 대한 노트: 타입스크립트 1.5에서 기록해둘 만큼 중요한 명명법 변경이 있었습니다. “내부 모듈(Internal modules)”은 “네임스페이스”가 되었습니다. “외부 모듈(External modules)”은 이제 간단하게 “모듈(modules)”이 되어 ECMAScript 2015의 용어와 맞췄습니다. (module X {namespace X {와 동일하며 후자가 선호됩니다.)

개요

이 포스트는 타입스크립트에서 네임스페이스와 모듈을 사용해 코드를 조직하는 여러가지 방법을 설명합니다. 그리고 어떻게 네임스페이스와 모듈을 사용하고 일반적으로 겪을 수 있는 위험성에는 어떤 부분이 있는지 깊이 있는 주제도 살펴봅니다.

모듈에 대해 더 알고 싶다면 모듈 문서를 참고하세요. 네임스페이스에 대해 더 알고 싶다면 네임스페이스 문서를 참고하세요.

네임스페이스 사용하기

네임스페이스는 단순히 전역 네임스페이스에서의 자바스크립트 개체에 붙은 명칭입니다. 그래서 네임스페이스를 아주 단순한 구조로 사용할 수 있습니다. 여러 파일로 확장해서 작성할 수 있지만 --outFile로 합치는 것도 가능합니다. 네임스페이스는 웹 애플리케이션에서 코드를 구조화하기 좋은 방법이며 HTML 페이지 내에 모든 의존성이 <script> 태그로 포함됩니다.

이 접근 방법은 전역 네임스페이스 오염이 발생하기 때문에 특히 대형 어플리케이션의 경우는 컴포넌트의 의존성을 파악하기 쉽지 않습니다.

모듈 사용하기

모듈도 네임스페이스와 같이 코드와 선언을 모두 포함합니다. 주된 차이점은 모듈은 의존성을 _선언_한다는 점입니다.

또한 모듈은 의존성을 모듈 로더(module loader)를 통해 처리합니다. (예로 CommonJs/Require.js) 작은 크기의 JS 애플리케이션에서는 최적이라고 보기 어렵지만 대형 애플리케이션에서는 장기적인 관점에서 모듈성(modularity)과 유지 가능성(maintainability)에서 이득이 있습니다.

모듈은 코드를 더 쉽게 재사용할 수 있고 더 강하게 고립되어 있으며 번들링을 위해 더 나은 도구를 지원합니다.

Node.js 애플리케이션에서는 모듈을 사용하는 것이 기본적인 방법이며 코드를 구조화하는데 추천되는 접근 방식입니다.

ECMAScript 2015를 사용하면 모듈은 언어에서 제공하는 기능이며 호환 엔진 구현에서는 다 지원해야 합니다. 그러므로 새 프로젝트에서는 코드를 조직하는 방법으로 모듈을 사용하는 것을 추천합니다.

네임스페이스와 모듈의 위험성

여기서는 네임스페이스와 모듈을 사용했을 때 일반적으로 나타나는 다양한 위험에 대해 살펴보고 어떻게 피하는지 확인합니다.

/// <reference>를 사용한 모듈

모듈에서 import 문 대신에 /// <reference ... /> 문법을 사용하는 것이 가장 일반적인 실수입니다. 이 차이를 이해하려면 컴파일러가 import를 사용했을 떄 모듈을 위한 타입 정보를 어디서 가져오는지 이해해야 합니다. (예를 들면 import x from "..."; 또는 import x = require("...");에서의 "...".)

컴파일러는 .ts, .tsx를 먼저 찾은 후에 적절한 경로에 있는 .d.ts를 찾습니다. 특정 파일을 찾지 못한 경우 컴파일러는 _구현 없는 모듈 선언(ambient module declaration)_를 찾습니다. 이런 경우에는 .d.ts의 경로를 선언할 필요가 있습니다.

역자 주: ambient는 주변이란 의미인데 이 이슈의 설명을 기준으로 의역했습니다.

// myModules.d.ts
// .d.ts 파일 또는 모듈이 아닌 .ts 파일
declare module "SomeModule" {
  export function fn(): string;
}
// myOtherModule.ts
/// <reference path="myModules.d.ts" />
import * as m from "SomeModule";

reference 태그는 정의 없는 모듈의 선언에 필요한 선언 파일 위치를 지정할 수 있습니다.

이런 방법으로 여러 타입스크립트 예제에서 node.d.ts을 사용하는 것을 확인할 수 있습니다.

불필요한 네임스페이스

네임스페이스를 사용하는 프로그램을 모듈로 변경한다면 다음과 같은 형태가 되기 쉽습니다.

// shapes.ts
export namespace Shapes {
  export class Triangle { /* ... */ }
  export class Square { /* ... */ }
}

최상위 모듈을 Shapes로 두고 TriangleSquare를 감쌌는데 이런 방식을 사용할 이유가 없습니다. 이 모듈을 사용하는 입장에서는 혼란스럽고 짜증나는 방식입니다.

// shapeConsumer.ts
import * as shapes from "./shapes";
let t = new shapes.Shapes.Triangle(); // shapes.Shapes?

타입스크립트에서 모듈의 가장 중요한 기능은 다른 두 모듈이라면 절대 동일한 스코프에서 같은 명칭을 사용하지 않는다는 점입니다. 모듈을 사용할 때 직접 명칭에 배정하기 때문에 네임스페이스에서 심볼을 내보내며 일일이 감쌀 필요가 없기 때문입니다.

왜 모듈 내에서 네임스페이스를 사용할 필요가 없는지 고민하게 될지도 모르겠습니다. 네임스페이스는 일반적으로 구조의 논리적인 구분을 제공하고 명칭 충돌을 예방하기 위해 존재하는 개념입니다. 이미 모듈은 논리적으로 구분되어 있고 최상위 명칭은 코드를 불러오는 쪽에서 지정하기 때문에 개체를 내보내기 위해서 추가적인 모듈 계층을 더할 필요가 없는 것입니다.

이런 관점에서 작성한 예제는 다음과 같습니다.

// shapes.ts
export class Triangle { /* ... */ }
export class Square { /* ... */ }
// shapeConsumer.ts
import * as shapes from "./shapes";
let t = new shapes.Triangle();

모듈 사용의 트레이드오프

JS 파일과 모듈이 일대일 대응을 하는 것처럼 타입스크립트도 모듈 소스 파일과 생성된 JS 파일이 일대일로 대응합니다. 이 방식은 어떤 모듈 시스템을 사용하느냐에 따라서 여러 모듈 소스 파일을 합치는 작업이 불가능 할 수 있습니다. outFile 옵션을 commonjs 또는 umd인 경우 외에는 사용할 수 없습니다. 타입스크립트 1.8 이후 버전이라면 amd 또는 system에서도 사용 가능합니다.

얼마 전 이벤트 소싱 패턴에 대한 글을 작성했다. 글을 읽고나서 js로 간략하게 구현해봤던 내용을 글로 정리했다. 개념을 나눠 설명하기 위해 CQRS 부분은 다른 글을 통해 덧붙이려고 한다. 여기서 사용하는 구현은 프로덕션에서 사용하기에 부족한 점이 많기 때문에 개념 이해에 중점을 맞춰 코드를 보면 좋겠다. 여기서는 은행 계좌를 예시로 작성했으며 구현하는 부분은 다음과 같다.

  • AggregateRoot: 이벤트가 반영될 집합체
  • BankAccount: 집합체 구현
  • *Event: 각각의 이벤트
  • EventSourcingRepository: 이벤트를 사용해서 데이터를 다루는 리포지터리 클래스
  • InMemoryForTestingEventStore: 이벤트 저장소 동작을 확인하기 위한 클래스
  • EventStoreData: 이벤트 저장소에서 저장되는 데이터 개체 클래스

전체 코드는 gist에서 확인할 수 있으며 jsbin.com에서 테스트해볼 수 있다.


가장 먼저 집합체를 구현한다. 집합체는 일련의 이벤트를 투영할 수 있는 개체다. 은행 계좌를 열고, 닫고, 입출금을 한다면 그 정보의 집합체는 은행 계좌가 될 것이다. 먼저 AggregateRoot 클래스를 작성한다. 이 클래스는 이벤트를 받고 해당 메소드를 호출한다. js는 메서드 오버로딩이 없기 때문에 handle(event) 메소드가 apply{이벤트명} 메소드를 찾는다.

class AggregateRoot {
    apply(event) {
        this.handle(event)
        return this
    }

    handle(event) {
        var eventName = event.constructor.name
        var eventMethod = `apply${eventName}`

        if (! this[eventMethod]) {
            throw new TypeError(`${eventMethod} is not a function`)
        }

        this[eventMethod](event)
    }
}

은행계좌를 개설하는 이벤트를 작성한다.

class OpenedEvent {
    constructor(id: number, name: string) {
        this.id = id
        this.name = name
    }
}

은행 계좌 클래스를 추가한다. 이 클래스로 생성한 개체가 집합체가 되며 이벤트를 통해 갱신된다.

class BankAccount extends AggregateRoot {
    static open(id: number, name: string) {
        var bankAccount = new BankAccount
        bankAccount.apply(new OpenedEvent(id, name))
        return bankAccount
    }

    applyOpenedEvent(event) {
        this.id = event.id
        this.name = event.name

        this.closed = false
        this.balance = 0
    }
}

아래 예제에서 이벤트로 프로퍼티가 갱신되는 것을 확인할 수 있다.

var bankAccount = BankAccount.open(123456, 'Koala')
console.log(bankAccount.id, bankAccount.name) // 123456, 'Koala'

이제 여러 이벤트를 추가한다.

class WithdrawnEvent {
  constructor(id: number, amount: number) {
    this.id = id
    this.amount = amount
  }
}

class DepositedEvent {
  constructor(id: number, amount: number) {
    this.id = id
    this.amount = amount
  }
}

class ClosedEvent {
  constructor(id: number) {
    this.id = id
  }
}

메소드도 추가한다.

class BankAccount extends AggregateRoot {
    // ... 이전 코드
    withdraw(amount) {
        if (this.closed) {
            throw new Error(`${this.id} account is closed.`)
        }
        this.apply(new WithdrawnEvent(this.id, amount))
        return this
    }

    deposit(amount) {
        if (this.closed) {
            throw new Error(`${this.id} account is closed.`)
        }
        this.apply(new DepositedEvent(this.id, amount))
        return this
    }

    close() {
        if (!this.closed) {
            this.apply(new ClosedEvent(this.id))
        }
        return this
    }

    applyWithdrawnEvent(event) {
        this.balance -= event.amount
    }

    applyDepositedEvent(event) {
        this.balance += event.amount
    }

    applyClosedEvent(event) {
        this.closed = true
    }
}
var bankAccount = BankAccount.open(123456,  'Koala')
  .deposit(10000)
  .withdraw(1000)
  .deposit(3000)
  .close()

console.log(bankAccount.name, bankAccount.balance, bankAccount.closed ? 'closed' : 'opened')
// Koala 12000 closed

집합체를 사용해서 내부적으로는 이벤트를 통해 데이터가 갱신되고 있지만 개체의 일반적인 사용과 큰 차이가 없는 것을 확인할 수 있다. 각각의 메소드는 실제로 개체의 정보를 갱신하지 않고 이벤트를 생성하는 역할만 하며 이벤트를 적용하는 apply* 메소드에서만 실질적인 변화가 일어나고 있다.

정말 이 은행 계좌는 일련의 이벤트로 결과를 얻을 수 있을까? 다음 예를 보면 알 수 있다.

var events = [
    new OpenedEvent(123456, 'Koala'),
    new DepositedEvent(123456, 10000),
    new WithdrawnEvent(123456, 1000),
    new DepositedEvent(123456, 3000),
    new ClosedEvent(123456),
]

var bankAccount = new BankAccount
events.forEach(event => bankAccount.apply(event))

console.log(bankAccount.name, bankAccount.balance, bankAccount.closed ? 'closed' : 'opened')
// Koala 12000 closed

동일한 결과를 확인할 수 있다. 위 코드는 AggregateRoot에 다음처럼 추가한다.

class AggregateRoot {
    // ... 이전 코드
    initializeState(events) {
        events.forEach(event => this.apply(event))
    }
}

이제 일련의 이벤트를 다루고 저장할 수 있도록 리포지터리를 만든다.

class EventSourcingRepository {
    constructor(eventStore, aggregateType) {
        this.eventStore = eventStore
        this.aggregateType = aggregateType
    }

    load(id) {
        var events = this.eventStore.load(id)

        var aggregate = Object.create(this.aggregateType.prototype)
        aggregate.initializeState(events)
        return aggregate
    }

    save(aggregate) {
        var uncommittedEvents = aggregate.getUncommittedEvents()
        this.eventStore.append(uncommittedEvents)
    }
}

저장하지 않은 이벤트를 가져올 수 있도록 getUncommittedEvents()AggregateRoot에 구현한다. 또한 상태 초기화 시 저장하지 않은 이벤트로 다루지 않도록 initializeState 메소드도 변경한다.

class AggregateRoot {
    // ...
    uncommittedEvents = []

    getUncommittedEvents() {
        var events = this.uncommittedEvents
        this.uncommittedEvents = []
        return events
    }

    apply(event) {
        this.handle(event)
        this.uncommittedEvents.push(event)
        return this
    }

    initializeState(events) {
        events.forEach(event => this.handle(event))
    }
}

여기서는 예로 간단한 이벤트 저장소를 구현해서 사용한다. 이벤트를 저장하기 위한 구조로 EventStoreData를 다음처럼 작성한다.

class EventStoreData {
    constructor(rootId, event, createdAt) {
        this.rootId = rootId
        this.event = event
        this.createdAt = createdAt
    }
}

이벤트가 데이터베이스에 저장될 때 EventStoreData의 정의대로 저장된다고 생각해보자. 관계형 데이터베이스를 예로 든다면 이 클래스가 테이블의 스키마를 반영하고 있다고 볼 수 있다. 다음은 이 개체를 그대로 메모리에서 사용하는 예제 이벤트 저장소 클래스다.

class InMemoryForTestingEventStore {
    constructor(events) {
        this.data = events ? this.convertEvents(events) : []
    }

    load(rootId) {
        return this.data
            .filter(data => data.rootId === rootId)
            .map(data => data.event)
    }

    append(events) {
        var newData = this.convertEvents(events)
        this.data = this.data.concat(newData)
    }

    convertEvents(events) {
        return events.map(event => this.convertEventToData(event))
    }

    convertEventToData(event) {
        var createdAt = new Date().getTime()
        return new EventStoreData(event.id, event, createdAt)
    }
}

다음처럼 리포지터리로 저장하고 불러올 수 있게 되었다.

var eventStore = new InMemoryForTestingEventStore()
var repository = new EventSourcingRepository(eventStore, BankAccount)

var bankAccount = BankAccount.open(654321,  'Edward')
  .deposit(20000)
  .withdraw(1000)
  .withdraw(1000)

repository.save(bankAccount)
var loaded = repository.load(654321)
console.log(bankAccount.name, bankAccount.balance, bankAccount.closed ? 'closed' : 'opened')
// Edward 18000 opened

eventStore에 저장된 이벤트 저장소 데이터를 살펴보면 이 동작이 좀 더 와닿는다.

console.log(eventStore.data)
eventStore.data

앞에서도 말했지만 여기서 사용하는 구현은 프로덕션에서 사용하기에 부족한 점이 많다. 예를 들면 AggregateRoot에 최종 일관성을 확인하기 위한 version도 구현되어 있지 않고 EventData에 id도 정의되어 있지 않다. 패턴에 대해 이해가 되었다면 실제로 구현한 패키지를 확인하면 도움이 된다.

그리고 CQRS 패턴을 함께 사용하지 않는다면 이벤트소싱은 반쪽에 불과하다. 여기서 살펴본 이벤트소싱에 더해 CQRS를 적용하면 유연하고 강력한 아키텍처를 구성할 수 있다. CQRS 패턴은 다음 글에서 살펴보려고 한다.

최근 프로젝트에서 audit을 생성하는 코드를 작성하면서 이벤트 소싱 패턴을 찾아보게 되었다. 여러 포스트를 통해 접해본 내용이지만 실제로 구현해보지 않아서 크게 와닿지 않았었다. 특히 용어가 익숙하지 않았는데 읽으며 궁금해서 찾아봤던 순서대로 정리했다.


전통적으로 사용하는 CRUD 모델을 생각해보자. 데이터를 갱신하기 위해서는 데이터 저장소에서 해당 데이터를 가져오는 작업이 필요하다. 동시성 문제가 나타날 수도 있고 확장성을 낮추는 지점이 될 수도 있다. 이벤트소싱 패턴은 일련의 이벤트를 통해 데이터를 조작하는 접근 방식이다. 데이터에 영향을 주는 모든 동작을 이벤트라는 저수준의 데이터로 관리하는 것으로 여러 문제를 해결할 수 있다. 물론 단점도 여러가지 존재하므로 필요에 따라 적용해야 한다.

이벤트소싱 패턴도 성숙한 아키텍처이기 때문에 다양한 주제로 세분화되어 있고 각각의 키워드를 알아야 쉽게 찾아볼 수 있다. 예를 들면 이벤트를 저장하고 불러오는 방식 하나만으로도 큰 주제다. 특히 이 패턴을 제대로 쓰기 위해서는 CQRS를 빼놓을 수 없기 때문에 두 이야기가 뒤섞이기도 한다.

이벤트 소싱

앞서 적은 것처럼 이벤트소싱 패턴은 일련의 이벤트를 통해 데이터를 조작한다. 현재의 상태는 변화의 총합으로 표현할 수 있다. 이 패턴을 설명할 때는 쇼핑몰을 많이 예로 든다.

다음과 같이 일련의 이벤트가 있다고 생각해보자.

id root_id event
1 1 카트 생성함
2 1 상품1 추가함
3 1 상품2 추가함
4 1 상품2 제거함
5 1 배송정보 입력함

이 이벤트를 개체에 하나씩 적용한다면 최종적으로 생성된 카트에 상품2가 추가되어 있고 배송정보까지 입력된 개체를 얻을 수 있게 된다. 기존 CRUD 모델과 비교한다면 이미 정형화된 모델에 상태를 저장하는 과정에서 나타나는 문제를 해결할 수 있다. CRUD에서는 카트에 추가되었다가 제거된 상품 목록을 뽑고 싶다고 했을 때 별도로 그 특정한 상태를 저장하지 않는다면 어려운 작업이 될 것이다. 게다가 작업을 한다 하더라도 그 작업 이후의 데이터에 대해서만 볼 수 있는 한계점이 있다. 이처럼 정형화되지 않은 데이터인 이벤트를 저장하고 있다는 점에서 더욱 유연한 변화가 가능하다.

여기서 사용되는 개체는 도메인 모델이고 흔히 집합체(aggregate)로 불리며 root_idaggregateRoot로 삼아서 각 개체로 전환한다.

var cart = events.reduce((aggregate, event) => aggregate.apply(event), new CartAggregate);
console.log(cart.getItems()) // ['상품1']
console.log(cart.shippingInfoExists()) // true

내 경우에는 다음과 같은 궁금점이 생겼다.

  • 이벤트는 어떻게 데이터로 저장하고 복원하지?
  • 이벤트가 많이 쌓이면 느려지지 않나?
  • 데이터가 필요할 때마다 매번 전부 이벤트를 돌려봐야 한다면 번거롭지 않을까?
  • 이벤트는 어떻게 다시 재생하지?

이벤트 저장(Eventstore)

이벤트 저장에 사용할 수 있는 저장소는 크게 세 가지로 분류된다.

  • 이벤트 저장에 특화된 데이터 저장소 사용 (e.g. eventstore.org)
  • NoSQL을 사용
  • 관계형 데이터베이스 사용

이벤트는 데이터베이스에 직렬화(serialize)해서 저장하고 역직렬화(deserialize)해서 사용한다. 각 저장하는 방식은 전략에 따라 다른데 관계형 테이터베이스를 사용한다면 이벤트명(주로 이벤트 타입명)과 데이터(주로 payload) 등으로 분리해서 저장한다. 이벤트의 정규 구조가 단순하기 때문에 단순히 위에서 언급한 데이터베이스가 아니더라도 용도에 맞게 선택할 수 있다.

스냅샷

스냅샷은 이벤트 저장에서 사용할 수 있는 전략이다. 기준에 따라서 이벤트가 많이 쌓이면 중간에 스냅샷을 만들고 그 스냅샷 이후의 이벤트만 가져와서 사용하는 방식이다. 특화된 저장소라면 시스템이 알아서 처리해주지만 그 외에는 이 문제를 고민해서 저장 방식을 설계해야 한다.

명령과 조회 책임 분리

필요한 데이터를 얻기 위해 모든 이벤트를 반복해서 재생하는 일은 많은 자원을 필요로 한다. 일련의 이벤트를 여러 장의 필름이라고 생각한다면 현재의 상태란 이 여러 필름을 한 위치에 투영(projecting)해서 나타난 그림이라고 볼 수 있겠다. 매번 모든 필름을 겹쳐 투영하는 대신 현재의 상태를 어딘가 저장하고 있다면 좀 더 쉽게 데이터를 사용할 수 있을 것이다.

현재 상품1의 재고량을 파악하기 위해 모든 이벤트를 투영하는 대신 재고 테이블에서 상품1의 재고량을 바로 찾아보는 것이 훨씬 쉽다. 다시 말하면 저장은 이벤트로 하지만 조회는 투영된 데이터, 구체화(materialised)된 데이터를 대상으로 수행하고 싶은 것이다. 이런 맥락에서 자연스럽게 명령과 조회의 책임을 분리하는 패턴(Command and Query Responsibility Segregation, CQRS)을 적용하게 된다. 명령으로 이벤트를 쌓고 리드 모델에서 조회하는 것이다. 명령과 조회에서의 책임 분리는 이 아키텍처의 장점을 끌어올릴 수 있다.

위에서 예시로 든 테이블의 경우는 id를 increment id와 같이 지정했지만 CQRS에서는 무작정 생성해도 충돌을 피할 수 있는 만큼 큰 id(예로 128bit GUID)를 생성해서 사용한다.

리드 모델

그렇다면 리드 모델은 어떻게 최신 데이터를 계속 유지할 수 있을까? 새 이벤트는 이벤트 버스(event bus)를 통해 전파되는데 리드 모델에 변화를 투영하는 경우에도 이벤트 리스너로서 발행되는 이벤트를 관찰하고 있다가 리드 모델을 갱신해야 하는 이벤트가 발생하는 순간에 갱신할 수 있다.

이벤트에 의해 갱신되는 리드 모델은 자료를 모두 유실하더라도 이벤트를 모두 저장하고 있다면 모든 이벤트를 이벤트 버스에 보내는 것으로 다시 복구할 수 있게 된다. 구체화된 데이터가 자료의 원형이 아니라 이벤트가 자료의 원형이기 때문에 필요에 따라서 언제든 비정규화된 조회를 새로 생성하고 삭제하는 것이 가능하다.

하지만 이 방식을 적용하면 리드 모델을 비동기적으로 처리하기 때문에 최종 일관성(eventual consistency)을 유지하게 된다. 리드 모델 갱신이 빠르다면 다시 조회를 수행했을 때 최신의 자료를 볼 수 있겠지만 연산이 많거나 부하가 커서 갱신이 느려진다면 이벤트는 생성되었지만 리드 모델은 갱신되지 않아 일시적으로 이전 데이터를 조회하게 될 수 있다. 분산 환경에서는 흔하게 나타나는 문제로 요구사항과 충돌한다면 이런 부분에 대한 대책을 세워야 한다.


간략하게 JavaScript로 이벤트소싱 패턴을 구현해 글로 작성했다.

더 읽기

시에라 이후로 Karabiner를 쓸 수 없게 되었는데 그나마 Karabiner-elements가 존재해서 그나마 다행이다. 맥에서는 언어 전환을 조합키로 사용하는 설정이 기본이다. 익숙해지면 그래도 괜찮긴 하지만 오늘따라 불편하게 더 느껴져서 어떻게 할 방법이 없나 찾아봤다.

키보드에 존재하지 않는 키를 언어 전환 키로 설정하면 우측 command 키를 해당 키로 설정하는 것으로 가능했다.

내 경우는 F16을 언어 전환키로 설정했다. 먼저 설정에서 Keyboard > Shortcuts > Input Sources > Select the previous input source 를 찾는다. 여기서 ^space 같이 키가 이미 지정되어 있을 것이다.

그리고 터미널을 열고 다음 명령을 실행한다.

sleep 5 ; osascript -e 'tell application "System Events"' \
          -e 'key code 106' \
          -e 'end tell'

키 코드 106 즉, F16 키를 입력하라는 내용의 애플 스크립트를 실행한다. 명령을 입력하면 바로 키를 누르기 때문에 5초 지연을 위해 sleep을 추가했다. 입력 후 5초 이내로 아까 열었던 키보드 설정에서 ^space를 더블 클릭하고 기다린다. 지연되었던 스크립트가 실행되면서 키보드에 존재하지 않는 F16이 입력이 된다.

이제 Karabiner-Elements를 실행한다. Simple Modifications에서 right_command를 f16으로 설정하면 끝난다.

처음엔 키보드에 나오는 F12로 설정해보려고 했는데 볼륨 조절 키로 연결이 되어서 생각처럼 되질 않았다.

Todd Motto의 글 Mastering Angular dependency injection with @Inject, @Injectable, tokens and providers를 번역했다. Angular 내에서 의존성 처리를 위해 어떤 과정을 거치는지 내부적인 구조를 이해하는데 도움이 되었다.


Angular의 의존성 주입 이해하기 – @Inject, @Injectable, 토큰과 프로바이더

Angular의 프로바이더는 애플리케이션을 개발하는데 있어 핵심적이며 의존성을 주입할 수 있는 다양한 방식을 제공한다. 이 포스트는 @Inject()@Injectable() 데코레이터 뒤에서 일어나는 일을 살펴보고 사용하는 방법을 확인하려고 한다. 그런 후에 토큰과 프로바이더를 이해하고 Angular가 실제로 어떻게 의존성을 찾고 생성하는지 살펴본다. 또한 Ahead-of-Time 소스 코드도 설명할 것이다.

프로바이더 주입하기

Angular는 의존성 주입(DI)를 사용할 때 수많은 마법 같은 일이 일어난다. Angular 1.x에서는 문자열 토큰을 사용해서 세세한 의존성을 처리하는 간단한 접근 방식을 사용했다. 이 방법은 이미 알고 있을 것이다.

function SomeController($scope) {
  // $scope를 사용한다
}
SomeController.$inject = ['$scope'];

DI 어노테이션 처리에 대해 더 자세히 알고 싶다면 예전 포스트를 참고하자.

좋은 접근 방식이지만 한계점이 있었다. 애플리케이션을 만들 때는 다양한 모듈을 만드는 것이 자연스러운 과정이다. 필요에 따라서 기능 모듈이나 라이브러리처럼 외부 모듈을 불러오기도 한다. (예를 들면 ui-router) 다른 모듈이라 하더라도 컨트롤러/서비스 등 동일한 이름을 사용할 수 없고 만약 동일한 이름이 있다면 컴파일 차례에서 충돌이 발생하게 될 것이다. (의존성이 같은 명칭을 갖고 있다면 서로를 덮어 쓰려고 하기 때문에 충돌이 발생한다.)

다행스럽게도 Angular의 새로운 의존성 주입은 완전히 새로 작성했으며 더 강력하고 유연한 방식으로 의존성 주입을 처리한다.

새로운 의존성 주입 방식

서비스(프로바이더)를 컴포넌트/서비스에 주입하려고 한다면 필요로 하는 프로바이더를 생성자에 _타입 정의_로 작성할 수 있다.

import { Component } from '@angular/core';
import { Http } from '@angular/http';

@Component({
  selector: 'example-component',
  template: '<div>I am a component</div>'
})
class ExampleComponent {
  constructor(private http: Http) {
    // `this.http` 로 Http 프로바이더에 접근 가능.
  }
}

여기서 사용한 타입 정의는 Http (H가 대문자)며 Angular는 자동으로 마법같이 이 서비스를 http에 배정했다.

여기까지만 봐도 상당히 마법같이 동작하는 것을 알 수 있다. 타입 정의는 타입스크립트에서만 사용할 수 있는 기능이다. 이론적으로는 컴파일된 자바스크립트 코드가 브라우저에서 실행되기 전까지는 http가 실제로 어떤 파라미터인지 알 방법이 없다.

tsconfig.json 파일을 살펴보면 emitDecoratorMetadata 값이 true로 설정되어 있다. 타입 파라미터를 컴파일된 자바스크립트 출력물에서 데코레이터로 사용할 수 있도록 메타데이터를 같이 내보내게 된다.

실제로 컴파일된 코드는 어떤 식인지 살펴본다. (읽기 편하도록 ES6의 모듈 불러오는 코드는 그대로 두었다.)

import { Component } from '@angular/core';
import { Http } from '@angular/http';

var ExampleComponent = (function () {
  function ExampleComponent(http) {
    this.http = http;
  }
  return ExampleComponent;
}());
ExampleComponent = __decorate([
  Component({
    selector: 'example-component',
    template: '<div>I am a component</div>'
  }),
  __metadata('design:paramtypes', [Http])
], ExampleComponent);

컴파일된 코드를 보면 @angular/http로 제공된 Http 서비스가 http와 동일하다는 점을 이해하고 있다. 데코레이터로 클래스에 다음처럼 메타데이터를 추가하게 된다.

__metadata('design:paramtypes', [Http])

기본적으로 @Component 데코레이터는 일반 ES5로 변경되었고 추가적인 metadata가 제공되며 __decorate를 통해 배정되고 있다는 점을 확인할 수 있다. 이런 과정으로 Angular는 Http 토큰을 확인한 후 컴포넌트 constructor의 첫 인자로 전달하고 this.http에 배정하게 된다.

function ExampleComponent(http) {
  this.http = http;
}

이 동작 방식은 $inject의 방식과 비슷하다. 하지만 _문자열_로 처리되던 토큰이 클래스 형태로 다뤄진다는 점에서 다르다. 명명에 의한 충돌이 없고 더 강력하다.

아마도 “토큰”에 대한 컨셉을 들은 적이 있을 것이다. (또는 OpaqueToken) Angular는 이 접근 방법으로 프로바이더를 저장하고 사용한다. 사용하려는 프로바이더를 참조할 때 토큰을 열쇠로 사용하는 것이다. (위 코드에서는 불러온 Http가 프로바이더다.) 하지만 기존 키와는 다르게 개체, 클래스, 문자열 등 어떤 것이든 올 수 있다는 점에서 다르다.

@Inject()

@Inject()는 어디서 사용할 수 있을까? 컴포넌트를 앞서 작성한 방식과는 다르게 다음처럼 작성할 수 있다.

import { Component, Inject } from '@angular/core';
import { Http } from '@angular/http';

@Component({
  selector: 'example-component',
  template: '<div>I am a component</div>'
})
class ExampleComponent {
  constructor(@Inject(Http) private http) {
    // 여기서 `this.http`를 Http 프로바이더로 사용할 수 있음
  }
}

이 코드에서 @Inject는 해당 토큰을 직접 찾아 배정하는 방법이다. 소문자 http 인자 뒤에 타입을 작성하는 방법과는 반대로 말이다.

이 방식은 컴포넌트나 서비스가 많은 의존성을 필요로 할 때 엉망이 될 수 있다. (그리고 엉망이 될 것이다.) Angular에서는 메타데이터를 추가해 의존성을 해결할 수 있기 때문에 대부분의 경우는 @Inject가 필요하지 않다.

OpaqueToken를 사용하는 경우가 유일하게 @Inject를 사용해야 하는 상황이다. 이 경우에는 의존성 주입 프로바이더에서 접근하기 위해 비어 있는 고유 토큰으로 사용할 수 있다.

@Inject를 이 경우에 사용해야 하는 이유는 OpaqueToken을 파라미터의 _타입_으로 사용할 수 없기 때문이다. 다음처럼 작성하게 되면 동작하지 않을 것이다.

const myToken = new OpaqueToken('myValue');

@Component(...)
class ExampleComponent {
  constructor(private token: myToken) {}
}

myToken은 타입이 아닌 값이다. 즉, 이 경우에는 타입스크립트가 컴파일을 할 수 없다. 하지만 OpaqueToken과 함께 사용할 수 있는 @Inject는 다음처럼 유용하게 사용할 수 있다.

const myToken = new OpaqueToken('myValue');

@Component(...)
class ExampleComponent {
  constructor(@Inject(myToken) private token) {
    // `token` 프로바이더를 사용할 수 있음
  }
}

여기서 OpaqueToken을 더 깊이 다루지는 않는다. 하지만 주입할 토큰을 수동 처리할 때 필요한 @Inject를 어떻게 사용할 수 있는지 살펴봤다. 어떤 것이든 이제 토큰으로 사용할 수 있을 것이다. 다시 말하면 타입스크립트에서 정의한 “타입”으로 제한하지 않고 사용한다는 뜻이다.

@Injectable()

@Injectable()은 컴포넌트나 서비스에 의존성 주입을 위해 필수적인 데코레이터라는 점은 흔하게 나타나는 잘못된 믿음이다. 실제로는 현재 이슈가 있어서 @Injectable()을 필수로 사용하는 것이지 이 사실은 아마도 변경될 예정이다. (이 변경은 상당히 최신이라 한동안 반영되지 않을 수도 있고 아예 반영되지 않을 수도 있다.)

Angular 데코레이터를 사용하면 꾸며진 클래스(decorated class)를 Angular가 읽을 수 있는 양식의 메타데이터로 보관하게 된다. 이 메타데이터에는 의존성을 어떻게 찾고 주입하는지에 대한 정보가 들어있다.

하지만 Angular 데코레이터를 사용하더라도 Angular가 어떤 방법으로도 필요한 의존성 정보를 찾을 수 없는 클래스가 있다고 생각해보자. 이런 경우에 @Injectable()을 사용해야 한다.

서비스에서 프로바이더를 주입한다면 반드시 @Injectable()을 사용해야 한다. 프로바이더는 추가적인 기능이 없기 때문에 Angular에 필요한 메타데이터가 저장될 수 있도록 지정해야 한다.

즉, 서비스를 다음처럼 작성했다고 가정하자.

export class UserService {
  isAuthenticated(): boolean {
    return true;
  }
}

예로 든 컴포넌트는 어떤 프로바이더도 주입하지 않고 있기 때문에 데코레이터를 사용할 필요가 없다.

하지만 서비스가 의존성(Http)을 포함하고 있다면 다음처럼 작성하게 된다.

import { Http } from '@angular/http';

export class UserService {
  constructor(private http: Http) {}
  isAuthenticated(): Observable<boolean> {
    return this.http.get('/api/user').map((res) => res.json());
  }
}

이 코드는 Http 프로바이더의 메타데이터가 저장되지 않아서 Angular가 제대로 의존성을 해결하지 못할 것이다.

이런 문제는 간단히 @Injectable()로 풀 수 있다.

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';

@Injectable()
export class UserService {
  constructor(private http: Http) {}
  isAuthenticated(): Observable<boolean> {
    return this.http.get('/api/user').map((res) => res.json());
  }
}

이제 Angular는 Http 토큰을 인식하고 http에 무엇을 주입해야 하는지 알게 되었다.

토큰과 의존성 주입

이제 어떻게 Angular가 주입하는지 알았으니 어떻게 의존성을 해결하고 인스턴스로 바꾸는지 배울 수 있다.

프로바이더 등록하기

NgModule 내에 어떻게 전형적인 서비스를 등록하는지 살펴보자.

import { NgModule } from '@angular/core';

import { AuthService } from './auth.service';

@NgModule({
  providers: [
    AuthService
  ]
})
class ExampleModule {}

위 코드는 짧은 문법으로 풀어서 작성하면 다음과 같다.

import { NgModule } from '@angular/core';

import { AuthService } from './auth.service';

@NgModule({
  providers: [
    {
      provide: AuthService,
      useClass: AuthService
    }
  ]
})
class ExampleModule {}

개체 내 provide 프로퍼티는 등록할 프로바이더를 위한 토큰이다. Angular가 AuthService 토큰에 어떤 내용이 있는지 useClass 값을 사용해서 살펴본다는 뜻이다.

이 방식은 장점이 여러가지 있다. 먼저 동일한 class명인 프로바이더가 있더라도 Angular에서 올바른 서비스를 참조하는데 전혀 문제가 없다. 둘째로 _토큰_이 동일하다면 기존에 존재하는 프로바이더를 다른 프로바이더로 덮어 쓸 수 있다는 점이다.

프로바이더 덮어쓰기

AuthService를 다음처럼 작성했다고 가정하자.

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';

@Injectable()
export class AuthService {

  constructor(private http: Http) {}

  authenticateUser(username: string, password: string): Observable<boolean> {
    // returns true or false
    return this.http.post('/api/auth', { username, password });
  }

  getUsername(): Observable<string> {
    return this.http.post('/api/user');
  }

}

이 서비스를 애플리케이션 전체에 사용하고 있다고 상상해보자. 사용자가 로그인 하기 위한 양식도 만들었다.

import { Component } from '@angular/core';
import { AuthService } from './auth.service';

@Component({
  selector: 'auth-login',
  template: `
    <button (click)="login()">
      Login
    </button>
  `
})
export class LoginComponent {

  constructor(private authService: AuthService) {}

  login() {
    this.authService
      .authenticateUser('toddmotto', 'straightouttacompton')
      .subscribe((status: boolean) => {
        // 사용자가 로그인 후 해야 할 내용 작성
      });
  }

}

이제 사용자명을 표시하기 위해 사용자 정보를 서비스에서 가져와 사용할 수 있다.

@Component({
  selector: 'user-info',
  template: `
    <div>
      You are {% raw %}{{ username }}{% endraw %}!
    </div>
  `
})
class UserInfoComponent implements OnInit {

  username: string;

  constructor(private authService: AuthService) {}

  ngOnInit() {
    this.authService
      .getUsername()
      .subscribe((username: string) => this.username = username);
  }

}

이 내용을 AuthModule라는 이름 아래에 모듈로 묶을 수 있다.

import { NgModule } from '@angular/core';

import { AuthService } from './auth.service';

import { LoginComponent } from './login.component';
import { UserInfoComponent } from './user-info.component';

@NgModule({
  declarations: [
    LoginComponent,
    UserInfoComponent
  ],
  providers: [
    AuthService
  ]
})
export class AuthModule {}

각각의 컴포넌트를 다 뒤져서 새 프로바이더를 참조하도록 변경해야 한다. 토큰을 사용했다면 직접 다 수정하는 대신에 AuthService 토큰에 FacebookAuthService을 대신 사용하도록 얹는 것이 가능하다.

import { NgModule } from '@angular/core';

// totally made up
import { FacebookAuthService } from '@facebook/angular';

import { AuthService } from './auth.service';

import { LoginComponent } from './login.component';
import { UserInfoComponent } from './user-info.component';

@NgModule({
  declarations: [
    LoginComponent,
    UserInfoComponent
  ],
  providers: [
    {
      provide: AuthService,
      useClass: FacebookAuthService
    }
  ]
})
export class AuthModule {}

위 프로바이더 설정을 보면 단순히 useClass 프로퍼티에 다른 값으로 바꾼 것을 볼 수 있다. 이 방법으로 어플리케이션 내에서 AuthService를 사용하는 곳이라면 변경된 프로바이더가 적용된다.

이런 이유로 Angular에 프로바이더를 찾기 위한 용도에서 AuthService를 토큰으로 사용한다. 새로운 클래스인 FacebookAuthService로 교체한 대로 모든 컴포넌트는 이 클래스를 사용하게 될 것이다.

인젝터(injector) 이해하기

여기까지 읽었다면 토큰과 Angular의 의존성 주입 시스템을 이해했을 것이다. 다음 장에서는 더 상세하게 이해하기 위해 Angular에서 컴파일된 AoT 코드 안을 들여다 보려고 한다.

사전 컴파일된 코드

컴파일된 코드를 살펴보기 전에 사전 컴파일된 버전의 코드를 보려고 한다. 사전 컴파일은 무엇일까? 그 코드는 Ahead-of-Time 컴파일 이전에 우리가 작성한 코드다. 기본적으로 우리가 작성한 모든 코드는 사전 컴파일에 해당하며 Angular는 JiT을 사용해 브라우저에서 컴파일을 하는 것이 가능하다. 또는 더 효과적인 접근 방식으로 오프라인에서 컴파일(AoT)을 수행할 수 있다.

이제 어플리케이션을 다 만들었다고 생각하고 아래 NgModule 코드를 확인해보자.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { Routes, RouterModule } from '@angular/router';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';

export const ROUTER_CONFIG: Routes = [
  { path: '', loadChildren: './home/home.module#HomeModule' },
  { path: 'about', loadChildren: './about/about.module#AboutModule' },
  { path: 'contact', loadChildren: './contact/contact.module#ContactModule' }
];

@NgModule({
  imports: [
    BrowserModule,
    HttpModule,
    RouterModule.forRoot(ROUTER_CONFIG),
  ],
  bootstrap: [
    AppComponent
  ],
  declarations: [
    AppComponent
  ]
})
export class AppModule {}

이 코드는 상당히 익숙하다. 루트 컴포넌트와 다른 모듈로 연결하는 몇 라우트가 포함되었다. 이 코드를 Angular가 컴파일 한다면 실제 코드는 어떤 모양일까?

Angular는 VM (가상 머신) 친화적 코드를 생성해서 가능한 한 성능 좋은 코드를 만들어 낸다. 환상적인 일이다. 컴파일된 코드를 열어보고 조금 더 상세하게 알아보자.

AppModuleInjector

Angular는 각 모듈을 위한 인젝터(주입자, injector)를 생성한다. 위 코드에서는 AppModule (데코레이트된 코드)를 사용해서 AppModuleInjector라는 이름의 인젝터를 생성한다.

생성된 AppModuleInjector를 확인해보자.

import { NgModuleInjector } from '@angular/core/src/linker/ng_module_factory';
import { CommonModule } from '@angular/common/src/common_module';
import { ApplicationModule, _localeFactory } from '@angular/core/src/application_module';
import { BrowserModule, errorHandler } from '@angular/platform-browser/src/browser';
import { RouterModule, ROUTER_FORROOT_GUARD } from '@angular/router/src/router_module';
import { NgLocaleLocalization, NgLocalization } from '@angular/common/src/localization';
import { ApplicationInitStatus, APP_INITIALIZER } from '@angular/core/src/application_init';
import { Testability, TestabilityRegistry } from '@angular/core/src/testability/testability';
import { HttpModule } from '@angular/http/src/http_module';
import { ApplicationRef, ApplicationRef_ } from '@angular/core/src/application_ref';
import { BrowserModule } from '@angular/platform-browser/src/browser';
import { Injector } from '@angular/core/src/di/injector';
import { LOCALE_ID } from '@angular/core/src/i18n/tokens';
import { RouterModule, provideForRootGuard } from '@angular/router/src/router_module';
import { Router } from '@angular/router/src/router';
import { NgZone } from '@angular/core/src/zone/ng_zone';
import { Console } from '@angular/core/src/console';
import { ROUTES } from '@angular/router/src/router_config_loader';
import { ErrorHandler } from '@angular/core/src/error_handler';

import { AppModule } from './app.module';
import { AppComponentNgFactory } from './app.component.ngfactory';

class AppModuleInjector extends NgModuleInjector<AppModule> {
  _CommonModule_0: CommonModule;
  _ApplicationModule_1: ApplicationModule;
  _BrowserModule_2: BrowserModule;
  _ROUTER_FORROOT_GUARD_3: any;
  _RouterModule_4: RouterModule;
  _HttpModule_5: HttpModule;
  _AppModule_6: AppModule;
  _ErrorHandler_7: any;
  _ApplicationInitStatus_8: ApplicationInitStatus;
  _Testability_9: Testability;
  _ApplicationRef__10: ApplicationRef_;
  __ApplicationRef_11: any;
  __ROUTES_12: any[];

  constructor(parent: Injector) {
    super(parent, [AppComponentNgFactory], [AppComponentNgFactory]);  
  }

  get _ApplicationRef_11(): any {
    if (this.__ApplicationRef_11 == null) { 
      this.__ApplicationRef_11 = this._ApplicationRef__10; 
    }
    return this.__ApplicationRef_11;
  }

  get _ROUTES_12(): any[] {
    if (this.__ROUTES_12 == null) { 
      this.__ROUTES_12 = [[
        {
          path: '', loadChildren: './home/home.module#HomeModule'
        },
        {
          path: 'about', loadChildren: './about/about.module#AboutModule'
        },
        {
          path: 'contact', loadChildren: './contact/contact.module#ContactModule'
        }
      ]]; 
    }
    return this.__ROUTES_12;
  }

  createInternal(): AppModule {
    this._CommonModule_0 = new CommonModule();
    this._ApplicationModule_1 = new ApplicationModule();
    this._BrowserModule_2 = new BrowserModule(this.parent.get(BrowserModule, (null as any)));
    this._ROUTER_FORROOT_GUARD_3 = provideForRootGuard(this.parent.get(Router, (null as any)));
    this._RouterModule_4 = new RouterModule(this._ROUTER_FORROOT_GUARD_3);
    this._HttpModule_5 = new HttpModule();
    this._AppModule_6 = new AppModule();
    this._ErrorHandler_7 = errorHandler();
    this._ApplicationInitStatus_8 = new ApplicationInitStatus(this.parent.get(APP_INITIALIZER, (null as any)));
    this._Testability_9 = new Testability(this.parent.get(NgZone));

    this._ApplicationRef__10 = new ApplicationRef_(
      this.parent.get(NgZone), 
      this.parent.get(Console), 
      this, 
      this._ErrorHandler_7, 
      this,
      this._ApplicationInitStatus_8,
      this.parent.get(TestabilityRegistry, (null as any)),
      this._Testability_9
    );
    return this._AppModule_6;
  }

  getInternal(token: any, notFoundResult: any): any {
    if (token === CommonModule) { return this._CommonModule_0; }
    if (token === ApplicationModule) { return this._ApplicationModule_1; }
    if (token === BrowserModule) { return this._BrowserModule_2; }
    if (token === ROUTER_FORROOT_GUARD) { return this._ROUTER_FORROOT_GUARD_3; }
    if (token === RouterModule) { return this._RouterModule_4; }
    if (token === HttpModule) { return this._HttpModule_5; }
    if (token === AppModule) { return this._AppModule_6; }
    if (token === ErrorHandler) { return this._ErrorHandler_7; }
    if (token === ApplicationInitStatus) { return this._ApplicationInitStatus_8; }
    if (token === Testability) { return this._Testability_9; }
    if (token === ApplicationRef_) { return this._ApplicationRef__10; }
    if (token === ApplicationRef) { return this._ApplicationRef_11; }
    if (token === ROUTES) { return this._ROUTES_12; }

    return notFoundResult;
  }

  destroyInternal(): void {
    this._ApplicationRef__10.ngOnDestroy();
  }
}

코드가 좀 이상하게 보일지 모르지만 (그리고 실제로 생성된 코드는 더 이상하게 보일 것이다) 이 코드에서 무슨 일이 실제로 일어나는지 확인하자.

실제 생성된 코드를 좀 더 읽기 쉽도록 불러오는(import) 부분을 고쳤다. 각 모듈에서는 와일드카드를 사용해서 동일 이름으로 충돌하는 일이 없도록 처리되어 있다.

HttpModule을 불러오는 부분을 보면 알 수 있을 것이다.

import * as import6 from '@angular/http/src/http_module';

이런 접근 방식으로 HttpModule을 직접 참조하는 대신 import6.HttpModule로 사용할 수 있다.

이 생성된 코드에서 살펴봐야 하는 부분은 세 가지다. 클래스에 있는 프로퍼티, 모듈이 어떻게 불려오는지, 그리고 의존성 주입 원리가 어떻게 동작하는지에 대해서다.

AppModuleInjector 프로퍼티

각 프로바이더와 의존성을 처리하기 위해서 AppModuleInjector프로퍼티가 생성되었다.

// ...
class AppModuleInjector extends NgModuleInjector<AppModule> {
  _CommonModule_0: CommonModule;
  _ApplicationModule_1: ApplicationModule;
  _BrowserModule_2: BrowserModule;
  // ...
}

위 코드는 컴파일된 출력의 일부다. 이 클래스에 선언된 세 가지 프로퍼티를 확인해보자.

  • CommonModule
  • ApplicationModule
  • BrowserModule

작성한 모듈에서는 BrowserModule만 사용했는데 CommonModuleApplicationModule은 어디서 온 것일까? 이 정보는 BrowserModule의해 추가된 부분으로 이 모듈을 사용하기 위해 직접 불러올 필요가 없도록 컴파일에 포함되었다.

또한 모듈의 모든 프로퍼티 끝에는 숫자가 붙어 있다. 와일드카드로 모듈을 불러온 것처럼 각 프로바이더 사이에서 나타날 수 있는 명칭으로 인한 잠재적 충돌을 피하기 위한 방법이다.

두 모듈이 동일한 이름의 서비스를 사용한다면 위 설명처럼 숫자를 붙이지 않고서는 같은 이름의 프로퍼티에 저장되어 잠재적인 오류의 원인이 될 것이다.

모듈 불러오기

컴파일을 수행하면 Angular는 각 프로바이더를 불러올 때 직접적인 경로를 사용하기 때문에 코드를 작성할 때는 다음처럼 작성해도 된다.

import { CommonModule } from '@angular/common';

AoT 컴파일된 버전은 다음과 같다.

import * as import5 from '@angular/common/src/common_module';

코드가 컴파일되고 번들로 묶이면 나무 흔들기를 할 수 있다는 장점이 있고 각 모듈에서 실제로 사용되는 부분만 포함하는 것이 가능하다.

의존성 주입

각 모듈은 각자의 의존성 주입을 처리하는데 만약 의존성을 찾지 못한다면 찾을 때까지 부모 모듈을 타고 올라간다. 계층을 다 탐색해도 찾지 못하면 그때 오류가 발생한다.

모든 의존성은 토큰을 통해 유일함을 확인하는데 이 접근 방식은 의존성을 등록할 때와 찾을 때 모두 사용된다.

의존성을 주입하는 방법은 createInternal를 사용하는 방법과 프로퍼티의 게터(getter)를 사용하는 방법 두 가지가 있다.

불려오는 모듈이나 밖으로 보내는 모듈은 createInternal과 함께 생성된다. 이 부분은 모듈이 인스턴스로 만들어지는 순간에 실행된다.

다음은 BrowserModuleHttpModule을 사용하는 경우다. 모듈을 사용하면 다음같은 코드가 생성된다.

class AppModuleInjector extends NgModuleInjector<AppModule> {
  _CommonModule_0: CommonModule;
  _ApplicationModule_1: ApplicationModule;
  _BrowserModule_2: BrowserModule;
  _HttpModule_5: HttpModule;
  _AppModule_6: AppModule;

  createInternal(): AppModule {
    this._CommonModule_0 = new CommonModule();
    this._ApplicationModule_1 = new ApplicationModule();
    this._BrowserModule_2 = new BrowserModule(this.parent.get(BrowserModule, (null as any)));
    this._HttpModule_5 = new HttpModule();
    this._AppModule_6 = new AppModule();
    // ...
    return this._AppModule_6;
  }
}

BrowserModule에는 두 외부 모듈인 CommonModuleApplicationModule이 생성되었고 불러온 모듈도 만들어졌다. 또한 실제 모듈인 AppModule도 생성되어 다른 모듈에서도 사용 가능하다.

다른 모든 프로바이더를 위해서는 생성을 한 후에 필요에 따라 클래스의 게터로 주입한다.

언제든 Angular에서 인젝터를 듣는다면 이 인젝터는 모듈에서 생성된 (컴파일된) 코드를 참조한다는 뜻이다.

Angular가 의존성을 확인할 때(예를 들면 constructor를 통해 주입할 때) 모듈 인젝터 속을 보고 찾고 찾지 못했다면 부모 모듈을 추적해서 계속 검색한다. 그래도 존재하지 않는다면 오류가 발생한다.

constructor에 타입 정의를 사용하면 Angular는 이 타입(여기서는 클래스)을 토큰으로 사용해 의존성을 찾는다. 그런 후에 이 토큰은 getInternal로 전달되는데 의존성이 존재한다면 해당 의존성의 인스턴스를 반환하게 된다. 소스 코드를 확인하자.

class AppModuleInjector extends NgModuleInjector<AppModule> {

  // new BrowserModule(this.parent.get(BrowserModule, (null as any)));
  _BrowserModule_2: BrowserModule;

  // new HttpModule()
  _HttpModule_5: HttpModule;

  // new AppModule()
  _AppModule_6: AppModule;

  getInternal(token: any, notFoundResult: any): any {
    if (token === BrowserModule) { return this._BrowserModule_2; }
    if (token === HttpModule) { return this._HttpModule_5; }
    if (token === AppModule) { return this._AppModule_6; }

    return notFoundResult;
  }
}

getInternal 메소드 안을 보면 Angular가 토큰을 단순한 if 문을 사용해서 확인하는 것을 볼 수 있다. 맞는 의존성을 찾는다면 프로바이더를 위해 연관된 프로퍼티를 반환하게 된다.

반면 notFoundResult를 반환하면 getInternal 메소드는 사용하지 않는다. Angular에서 필요한 의존성을 찾는 동안에 notFoundResultnull이 된다. 최상위 모듈까지 훑어도 의존성을 찾지 못했다면 오류가 발생한다.

정리하며

이 글이 @Inject@Injectable, 토큰과 프로바이더, 그리고 Angular가 AoT 컴파일을 통해 VM 친화적인 코드를 생성하는지에 대해 더 깊이 이해하는데 도움이 되었으면 좋겠다.

Todd Motto의 글 Angular constructor versus ngOnInit를 번역했다.


Angular의 constructor와 ngOnInit 차이점

Angular는 여러 생애주기 훅이 존재하지만 여전히 constructor도 있다. 이 글에서는 ngOnInit 생애주기 훅과 차이점을 확인한다. 이 차이는 Angular를 처음 시작할 때 혼란하게 만드는 근원이다.

constructor를 사용할 수 있는데도 생애주기 훅인 ngOnInit을 사용해야 할까?

차이점은 무엇인가

ES6의 constructor메소드 (여기서는 타입스크립트)는 Angular의 기능이 아니라 클래스 자체의 기능이다. constructor가 호출되는 시점은 Angular의 제어 바깥에 있다. 즉, Angular가 컴포넌트를 초기화 했는지 알기에는 적합하지 않은 위치다.

constructor를 살펴보자.

import { Component } from '@angular/core';

@Component({})
class ExampleComponent {
  // 이 부분은 Angular가 아닌
  // 자바스크립트에서 실행
  constructor() {
    console.log('Constructor initialised');
  }
}

// 생성자(constructor)를 내부적으로 호출
new ExampleComponent();

constructor를 호출하는 주체가 Angular가 아닌 자바스크립트 엔진이라는 점이 중요하다. 그런 이유로 ngOnInit(AngularJS에서는 $onInit) 생애주기 훅이 만들어졌다.

생애주기 훅을 추가하면서 Angular는 컴포넌트가 생성된 후에 설정을 마무리하기 위한 메소드를 한 차례 실행할 수 있게 되었다. 이름에서 알 수 있는 것처럼 컴포넌트의 _생애주기_를 다루는데 사용한다.

import { Component, OnInit } from '@angular/core';

@Component({})
class ExampleComponent implements OnInit {
  constructor() {}

  // Angular에서 필요에 맞게 호출
  ngOnInit() {
    console.log('ngOnInit fired');
  }
}

const instance = new ExampleComponent();

// 필요할 때 Angular에서 호출
instance.ngOnInit();

Constructor 용도

생애주기 훅을 사용해야 하는 경우도 있지만 constructor를 사용해야 적합한 시나리오도 있다. 이 생성자는 의존적인 코드를 컴포넌트에 전달하는 의존성 주입을 하기 위해서는 필수적으로 필요하다.

constructor는 자바스크립트 엔진에 의해 초기화 되는데 타입스크립트에서는 Angular에 의존성이 어느 프로퍼티에 적용되는지 직접 지정 안하고도 사용할 수 있다.

import { Component, ElementRef } from '@angular/core';
import { Router } from '@angular/router';

@Component({})
class ExampleComponent {
  constructor(
    private router: Router,
    private el: ElementRef
  ) {}
}

Angular의 의존성 주입은 여기서 더 읽을 수 있다.

위 코드는 Routerthis.router에 넣고 컴포넌트 클래스에서 접근할 수 있도록 한다.

ngOnInit

ngOnInit은 순수하게 Angular가 컴포넌트 초기화를 완료했다는 점을 전달하기 위해 존재한다.

이 단계는 컴포넌트에 프로퍼티를 지정하고 첫 변경 감지가 되는 범위까지 포함되어 있다. @Input() 데코레이터를 사용하는 경우를 예로 들 수 있다.

@Input() 프로퍼티는 ngOnInit 내에서 접근 가능하지만 constructor에서는 undefined를 반환하는 방식으로 디자인되어 있다.

import { Component, ElementRef, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';

@Component({})
class ExampleComponent implements OnInit {
  @Input()
  person: Person;

  constructor(
    private router: Router,
    private el: ElementRef
  ) {
    // undefined
    console.log(this.person);
  }

  ngOnInit() {
    this.el.nativeElement.style.display = 'none';
    // { name: 'Todd Motto', location: 'England, UK' }
    console.log(this.person);
  }
}

ngOnInit 생애주기 훅은 바인딩한 값을 읽을 수 있다고 보장할 수 있는 상황에서 호출된다.

Todd Motto의 글 Should you learn Angular 1.x or 2?를 번역했다.


Angular 1을 배워야 하나요 2를 배워야 하나요?

“Angular 1을 배워야 하나요 2를 배워야 하나요?”라는 질문은 정말 자주 받는다. 그 질문에 답하는 성격의 글로 도움과 안내가 될 수 있는 통찰을 줄 수 있었으면 한다. 이 질문은 누구도 쉽게 답할 수 없는데 바로 질문자에 따라 답이 다르기 때문이다. 내 생각을 정리해봤다.

흔한 질문

다음 같은 질문을 정말 자주 받는다.

  • “Angular를 새로 시작하는데 버전 1을 할까요 2를 할까요?”
  • “아직 Angular 2를 배우면 안되나요?”
  • “Angular 1과 2를 모두 배워야 하나요?”

먼저 기억해둬야 할 점은 이 질문에 “공식” 답변은 없다는 점이다. (물론 짧고 간결하게 답하면 어떤 도구든 가장 최신의 안정적인 버전을 선택하는 것이 바람직하다.) 하지만 어느 프레임워크를 배워야 할지, 어떤 프레임워크가 더 나은지 생각해본다면 몇 가지 큰 요인을 고려해야 할 것이다. 이 글에서는 그 질문에 대해 어떻게 스스로 답을 내릴지 고민할 수 있도록 몇 조언을 제공하려고 한다.

코드 기반과 팀

혼자서 일을 하든 팀으로 일을 하든 현재 일을 하고 있다면 질문에 답하기 위해 이런 생각을 해볼 수 있다.

먼저 AngularJS (1.x)는 프레임워크 세계에서 지배적인 포식자라고 할 수 있다. 그만큼 실무에서 가장 규모가 크고 현재 가장 많이 사용하는 프레임워크에 해당한다. 만약 AngularJS를 일에서 사용하고 있다면 전혀 문제가 아니다. 회사에서 고객을 위해 단일 프로덕트/어플리케이션을 개발한다면 고개를 숙여 프로젝트를 완성해 전달하는데 집중하고 프로젝트를 꾸준히 진행하길 바랄 것이다.

둘째로 코드 기반을 업그레이드하기 위해서 잠재적으로 여러 해의 수고가 들어간 코드를 다시 작성하고 싶은지 생각해봐야 한다. 이 질문에 답하기 위해서는 따져봐야 할 여러 요인이 있을 것이다. 2009년에 나온 AngularJS는 생산성을 극대화한 빠른 프레임워크긴 하지만 몇 가지 한계점이 존재하고 프로젝트의 생애를 잠재적으로 방해하는 요소가 될 수 있다. 말은 그렇지만 탈출 버튼을 누르고 당장에 Angular (v2+)로 넘어갈 만큼의 요소로 보기엔 어렵다.

어떻게 결정하든 상관 없이 이 고민은 “업그레이드”나 “마이그레이션”이 아니라 근본적으로 프레임워크를 교체하는 작업이다. 즉 완전히 다른 코드 기반으로 옮겨가게 된다는 것이다. (한번에 전부 옮기든, 점진적으로 옮기든 말이다.) CEO/CTO라면 견고하고 명확한 이유 없이는 사업에 영향을 주는 이런 결정을 쉽게 내리지 않을 것이다. 의사 결정권자는 고객에게 전달할 중점적 사안이 중요하지 버전 번호가 중요한 것이 아니다.

시나리오: 단일 상품 회사

GitHub에서 AngularJS를 사용한다고 가정해보자. 코드 기반은 아마 몇 년 정도 오래 되었을 것이다. 이제 코드 기반을 모바일에, 또는 데스크탑 클라이언트에도 배포하려고 한다. 이런 상황에서는 몇 가지 선택할 만한 경우가 있다. 단일 모바일 어플리케이션 (안드로이드, iOS)를 만드는 방법, 또한 네이티브 데스크탑 클라이언트를 만드는 방법이 있겠다. 이런 기술 차이는 더 큰 금전적 투자를 필요로 한다.

내 의견으로는 비지니스 목표를 달성하기 위해서 Angular로 옮겨 위에서 필요로 했던 모든 작업을 단일 프레임워크 내에서 수행할 수 있도록 권할 것이다. Angular를 사용하면 NativeScript를 사용해 네이티브 모바일 코드로 컴파일이 가능하며 모바일에 배포하기 위해 Ionic을 사용하거나 데스크탑 환경을 위해 Electron을 사용할 수 있을 것이다. 단일한 코드 기반에서 말이다.

하지만 한 걸음 물러나 다시 생각해봐야 한다. 무엇이 가장 중요한 웹 어플리케이션인가? 단일 페이지 앱(SPA)는 코드를 잘 나누고 작게 만들었다면, 제대로 된 성능 전략을 선택하고 사용자에게 컨텐츠를 전달하기 위한 가능한 가장 빠른 방법을 사용했다면 빠를 수도 있다. 하지만 더 빠르게도 가능하다. Angular는 Angular Universal을 사용해서 서버측 렌더링(SSR)이 가능하다. 이런 전략은 Angular 1.x에서 사용할 수 없다. 이 특징도 Angular를 배워야 할지 말아야 할지 결정할 때 참고할 중요한 부분이다.

시나리오: 다양한 프로덕트와 푸른 초원

내 경우에는 AngularJS로 작성된 단일 프로덕트 어플리케이션도 작업해봤고 그만큼 1.x를 사용하는 회사를 위해서 여러 프로젝트로 개발했었다. 그래서 두 경우 모두 경험해본 경력이 있다. 만약 대단한 클라이언트 10 곳과 10개의 Angular 1.x 앱이 있고 11번째 클라이언트가 푸른 초원에서 새로 시작하는 프로덕트를 제안했다고 하자. 무엇을 할 것인가?

이런 상황이라면 위에서 살펴봤던 이유들 때문에라도 미래를 보장받는 선택인 Angular를 고려할 것이다. Angular 2는 바닥부터 다시 작성되어 단일 방향 데이터 흐름과 컴포넌트 아키텍처와 같이 모범 사례를 적용하는데 집중했다. 이런 기능은 AngularJS에서도 사용할 수 있지만 가장 최신 버전에만 적용되어 있다. 즉, 기존에 존재하는 코드를 1.6+에서 사용하려면 코드 기반을 리팩토링하고 .component API를 사용해야 한다.

AngularJS가 언젠가 “중단(discontinued)”될 운명인건 알지만 그건 AngularJS 뿐만 아니라 모든 앱과 버전이 그러한 것 아닐까? 꼭 가장 최신에 가장 좋은 도구를 쓸 필요는 없지만 앞으로 3년 정도 어려운 시기 후에 Angular가 최종적으로 출시되면 투자할 가치가 있을 만큼 엄청난 힘이 있을 것이다.

만약 11번째 클라이언트가 당신에게 새로운 어플리케이션을 원한다면 시도해라. 하지만 그 전에 생명주기 훅, 상태 저장과 비저장(stateful and stateless) 컴포넌트와 단방향 데이터 흐름과 이벤트를 이해할 필요가 있다.

당신, 개인적으로

여기도 몇 가지 시나리오가 있으며 당신이 무엇을 하는지에 따라 다를 것이다. 하나의 답변으로 모든 상황에 딱 맞을 수는 없을 것이다.

시나리오: AngularJS를 사용해서 취업함

만약 AngularJS를 일에서 사용하고 있다면 아마도 이미 Angular를 둘러보고 문서를 살펴봤을 것이다. 그런 중에 이 괴물은 AngularJS나 기존에 알고 있는 MVC 패턴과는 사뭇 거리감이 있다는 것을 알게 되었을 것이다. 이 상황에서는 전적으로 본인에게 달렸다. Angular에 더 깊게 빠져들고 싶다면 도전해라. 그렇지 않아도 물론 괜찮다. 누군가에게 충고할 때 꼭 해라 하지 마라 하는건 별로 의미가 없다. 질문에 특별한 이유가 있는 경우가 아니고서는 말이다. (예를 들어, 서버에서 렌더링이 가능한가요? 아니면 이러이런 일을 할 수 있나요?) 이런 질문은 마치 “포르쉐를 사야 할까요 페라리를 사야 할까요?” 같고 답은 질문한 사람 머릿속에만 존재한다.

그렇다고 질문이 일을 벗어난 것은 아니다. Angular를 배우지 않고서는 사장에게 가서 Angular 사용하자고 할 수 없을 것이다. 자기 시간에 배워서 마음에 드는지 살펴보자. 그렇게 간단한 일이다.

시나리오: Angular를 처음 한다면

Angular를 전혀 본 적이 없다면 조금 어려운 질문이다. AngularJS가 갖고 있는 단일 시장 지배력과 Angular로 넘어가는 회사의 비율을 고려해보면 결국 둘 다 배워야 할 것이다. 만약 AngularJS를 .component() API로 배우고, 컴포넌트 기반 구조에서 “MVC 접근 방식으로” 어떻게 돌아가는지만 이해할 수 있다면 내일 당장이라도 AngularJS를 사용하는 회사에 취업할 수 있을 것이다.

“Angular만 하는” 직업을 찾고 있다면 위에서 언급했던 이유로 지금 당장은 조금 어려울 수 있다. 만약 Angular를 막 시작했다면 둘 다 배워야 할 것 같다. 하지만 앞서와 같이 이 결정은 자기 자신이 어떤 삶, 어떤 커리어를 선택하느냐에 기반하게 된다.

Angular가 급격하게 성장하고 있고 경이로운 성장 추이를 보여주고 있지만 새 이력서에 “Angular 2+”만 적어 놓고는 회사 문을 두드리기는 쉽지 않을 것이다. 대다수의 회사는 여전히, 앞으로 다년 간 AngularJS를 사용할 것이기 때문이다. 이런 경우에는 어떤 직업과 어떤 스킬을 원하는지, 어디에 취업하고 싶은지에 따라 결정할 필요가 있다. 이 “취업” 란에서는 최대한 일반적인 상황을 이야기하고 있다. 하지만 나처럼 자영업자를 하는 사람도 많을 것이다. 물론 그렇다고 이런 질문을 피할 수는 없다.

만약 자영업 엔지니어라면 더 땅을 파서 생계 유지에 집중하는 것이 당연하다. AngularJS에 대한 요청이 50회고 Angular 앱에 대한 요청은 한 번만 들어왔다면 어디에 더 시간을 집중해야 할까? AngularJS에 집중해야 할 것이다.

뒤집어서 새로운 일이든, 컨설팅이든, 무슨 일을 하든 50/50 비율로 요청을 받는 위치라면 둘 다 배워야 할 것이다. 개인적으로 아는 Angular 개발자는 대부분 AngularJS도 잘 알고 있었고 다 년 간 사용한 경험이 있었다.

시나리오: 다른 일로 취업함

아마도 React, Ember, Backbone, nockout 같은 프레임워크로 취업했지만 Angular를 고려하고 있는 경우일 것이다. 먼저 Angular 2가 무슨 이득이 있을 지 먼저 조사해볼 필요가 있다. Ahead-of-Time 컴파일은 브라우저에 배포하기 전 코드 크기를 극적으로 줄여서 앱을 전달 할 수 있는데 Angular를 살펴볼 때 주요하게 고려할 만한 부분이다.

마무리하며

빠르게 정리하자면 자신과 자신의 직업에 따라 답이 달라진다. Angular 직업은 많아질 것이고 AngularJS는 여전히 주변에 있을 거란 점에 의심하지 않는다. 사실 기업을 대상으로 한 AngularJS 지원은 더욱 높아질 것이다. (새 버전으로 마이그레이션 하기로 결정하기 전까지 말이다.)

요약하면 AngularJS를 사용하고 있다면 프로젝트 또는 회사의 미래 목표로 고려해보자. 만약 Angular를 처음 배우고 직업으로 삼고 싶다면 시장과 다니고 싶은 회사를 조사해보고 어떤 기술 스택을 요구하는지 살펴보자.

옳은 답은 없지만 이 글을 통해 조금이나마 고려에 도움이 되는 통찰이 생겼으면 좋겠다. 모두 잘 되길 바란다!

Marc Johannes Schmidt가 쓴 Bring High Performance Into Your PHP App (with ReactPHP)을 번역했다. 2014년 초 글이라서 아마 php7을 사용한다면 여기에 언급된 벤치마킹보다 더 나은 수치가 나오지 않을까 생각한다.


ReactPHP로 고성능 PHP 앱 만들기

이 글에서는 PHP 어플리케이션의 성능을 어떻게 최대화 하는지 살펴보려고 한다. 대부분 앱은 PHP의 성능을 완전히 사용하지 않는다. 대신 APC를 켜는 정도가 최선이라고 생각한다. 이 글을 읽어보면 아마 놀랄 것이다.

요약

대규모 심포니 앱에서 초당 130회 정도 요청을 처리할 수 있었는데 이 접근 방식으로 초당 2,000여 회 요청을 처리할 수 있다.

아키텍처

먼저 과거를 살펴보자.

근래 PHP를 사용하는 일반적인 방식은 Apache, Nginx, lighttpd와 같은 웹서버를 통해서 HTTP 프로토콜을 처리하고 동적 요청을 PHP로 전달하는 식으로 사용한다. Apache의 mod_rewrite와 같은 리라이트 엔진을 사용한다면 더 강력하게 사용할 수 있다.

웹서버에서 PHP를 구동하기 위해 설정하려면 다음과 같은 방법이 있다.

  • mod_php (apache 만)
  • f(ast)cgi
  • PHP-FPM

SuExec와 함께 FCGI를 설정하는 방식은 보안상 가장 많이 사용한다. 각 인터프리터 프로세스는 각 사이트 사용자 아래서 구동된다. 이렇게 분리된 환경은 VM 없이도 각각의 사용자에 대응해 대규모 호스팅 형태로 운영 가능하다. 이 접근 방식이 매우 일반적인 탓에 mod_php나 PHP-FPM을 로컬 개발 머신이나 단일 앱을 구동하는 웹서버(한 조직이 만든 앱인데 서버에서 이런 형식으로 웹서버에 올려 구동하는 경우)에서도 동일한 방식을 사용한다.

물론 이 접근 방식은 업계 전반에 걸쳐 상당히 일반적인 방식이다. 이 방식에서 가장 큰 손실은 “연산코드 캐시(opcode cache)”가 존재한다고 하더라도 클래스를 선언하고 객체를 초기화하며 캐시를 읽는 등의 작업을 매 요청마다 수행해야 한다는 점이다. 이 과정이 시간을 많이 소비하고 고성능의 완전한 환경과는 거리가 멀다는 점을 쉽게 상상할 수 있을 것이다.

틀을 깨고 생각하기

그럼 왜 이런 일을 하는 것일까? 왜 매 요청마다 사용하는 메모리를 정리하고 다시 생성하는 일을 반복해야 하는 것일까? 물론 PHP가 서버 자체로 디자인된 것이 아니라 템플릿 엔진, 도구 모음 정도로 만들었기 때문이다. 또한 PHP 자체가 비동기 형태로 디자인되지 않았기 때문에 대부분 함수는 “블로킹(blocking)”이 발생한다. 수년 동안 상황이 많이 달라졌다. PHP로 작성된 강력한 템플릿 엔진이 있다. 수 만 가지의 유용한 라이브러리를 Composer로 설치할 수 있는 커다란 생태계를 갖게 되었다. Java와 다른 언어에서 구현된, 아주 강력한 디자인 패턴도 PHP에서 구현되었다. (안녕, Symfony와 동료들!) 심지어 PHP의 비동기 웹서버를 위한 라이브러리도 존재한다.

잠깐, 뭐라고요?

잠깐, PHP를 위한 비동기 도구가 있다고? 그렇다. ReactPHP는 내가 가장 기대하는 라이브러리 중 하나다. 이 라이브러리는 이벤트 주도, 넌-블로킹 입출력 개념을 PHP로 가져왔다. (안녕, NodeJS!) 이 기술을 사용하면 HTTP 스택을 PHP에서 직접 작성할 수 있으며 각 요청마다 파괴할 필요 없이 메모리를 제어할 수 있게 된다.

매번 객체를 초기화 하거나 캐시를 읽는 것처럼 어플리케이션을 시작하기 위해 해야 하는 대부분의 작업이 응답 시간에 있어 많은 분량을 차지하고 있는데 이 시간을 줄여 성능을 올리는 것은 쉽게 이해가 되리라 믿는다. 이런 과정을 Java, NodeJs와 그 친구들처럼 없앨 수 있다면 성능이 향상될 것이란 얘기다. 만세!

어떻게 해야 하나

간단하다. ReactPHP는 http://reactphp.org에서 받을 수 있다. Composer를 사용해서도 설치할 수 있다.

$ composer require 'react/react=*'

server.php 파일을 다음처럼 생성한다.

<?php
require_once(__DIR__. '/vendor/autoload.php');

$i = 0;
$app = function ($request, $response) use ($i) {
    $response->writeHead(200, array('Content-Type' => 'text/plain'));
    $response->end("Hello World $i\n");
    $i++;
};

$loop = React\EventLoop\Factory::create();

$socket = new React\Socket\Server($loop);
$http = new React\Http\Server($socket, $loop);

$http->on('request', $app);
echo "Server running at http://127.0.0.1:1337\n";

$socket->listen(1337);
$loop->run();

이제 PHP 서버를 php server.php로 실행한다. 이제 http://127.0.0.1:1337로 접속하면 “Hello World” 문구를 볼 수 있을 것이다. $app은 “main” 함수로 서버에 들어오는 각 요청을 받는 엔트리 포인트 역할을 한다.

이게 전부다. 이렇게 쉽다. 끝.

벤치마크

아마 이런 생각이 들 것이다. “음, PHP는 하나의 cpu 코어/스레드를 사용하니깐 다중 코어 서버의 성능을 전부 사용하진 못할 거야.” 사실이긴 하지만 여러 대의 서버를 실행해서 프록시가 일감을 분배할 수 있다면 어떨까? 이제는 다중 코어를 지원하는 여러 워커를 실행할 수 있는 서버를 만들어야 한다. 이 작업을 위해 프로세스 매니저를 만들었는데 간단하게 Symfony를 위한 브릿지를 제공해서 핵 원자로 같이 강력하게 사용할 수 있게 되었다.

다음과 같은 형태로 구동된다.

테스트를 한 환경은 이렇다.

  • Intel(R) Xeon(R) CPU L5630, 6 Cores
  • 8GB RAM
  • PHP 5.4.4 with APC
  • Debian 7.1
  • nginx/1.2.1

각 PHP-FPM과 React 서버는 6개의 워커를 사용했다.

테스트는 Apache HTTP 서버 벤치마킹 도구인 ab를 사용했다.

테스트는 Symfony 2.4+로 작성된 웹앱을 사용했다. 이 프로젝트는 꽤 규모가 있는 CMS 번들을 사용했다. 이 번들에는 많은 서비스, 이벤트 리스너, 캐싱, 템플릿, 데이터베이스 엑세스 등 많은 기능이 들어있다. (이 번들은 jarves/jarves로 변경되었다.)

캐시는 기본적인 부분에만 적용되어 있으며, 이 벤치마크 결과에서 뷰는 캐싱되지 않았다.

react 서버를 실행할 때 다음 명령을 사용했다.

$ php ./bin/ppm start /path/to/symfony/ --bridge=symfony -vvv

hhvm은 다음처럼 구동했다.

$ hhvm ./bin/ppm start /path/to/symfony/ --bridge=symfony -vvv

결과는 이렇다.

초당 요청 횟수

메모리 사용량

  • fpm – nginx 서버 뒤에 구동한 일반적인 PHP-FPM 서버.
  • react – 내장 로드 벨런서로 실행한 react 서버. (위 구성 이미지에서 확인할 수 있음.)
  • react+nginx – nginx를 로드 밸런서로 사용한 react 서버. react 서버는 워커 프로세스만 사용했고 nginx는 이 워커에 직접 통신함.
  • hhvmreact와 동일하지만 hhvm으로 실행.
  • hhvm+nginxreact-nginx와 동일하지만 hhvm으로 실행.

hhvmstream_select 이벤트 루프를 사용하고 phplibevent를 사용한다. 그래서 hhvm의 내장 웹서버는 사용하지 않았다.

위 결과에서 확인할 수 있는 것처럼 nginx를 로드벨런서로 사용한 react 서버는 전통적인 PHP-FPM + APC 구성보다 15배나 빨랐다.

react+nginx (libevent PHP 모듈)에서는 거의 초당 2,000 여 회 요청을 처리했다.

        2,000 requests / second
    7,200,000 requests / hour
   86,400,000 requests / half day
  172,800,000 requests / day
2,678,400,000 requests / half month
5,356,800,000 requests / month

메모리 사용량은 동일하다. 결과 중 메모리가 꾸준히 증가하는 부분은 CmsBundle에 있는 메모리 유출이 문제일 것이다. 이 문제는 아직 코드가 이 환경에 최적화되지 않은 탓이다. 이 접근 방식에 관한 문제는 아래에서 더 언급했다.

이 의미는 Symfony 앱에 엄청난 성능 향상을 끌어낼 수 있다는 이야기다. 시도해볼 만한 가치가 있다. 더 설명할 필요 없이 위 결과가 수천 마디 말을 대신한다고 생각한다.

Nginx를 로드 벨런서로 사용하기

react+nginxhhvm+nginx를 사용할 때 다음 설정을 사용했다. 단순히 요청을 프록시로 넘겨줬을 뿐 특정 파일을 지정하지 않았다.

upstream backend  {
    server 127.0.0.1:5501;
    server 127.0.0.1:5502;
    server 127.0.0.1:5503;
    server 127.0.0.1:5504;
    server 127.0.0.1:5505;
    server 127.0.0.1:5506;
}

server {
    root /path/to/symfony/web/;
    server_name servername.com
    location / {
        if (!-f $request_filename) {
            proxy_pass http://backend;
            break;
        }
    }
}

이 접근 방식의 문제점

이 방식에서 나타날 수 있는 몇 가지 문제점이 있다.

첫째로는 메모리 유출을 막기 위해 메모리 처리를 잘 해야한다는 점이다. 최신 버전의 PHP는 상당히 잘 처리하고는 있지만 요청을 처리한 다음에 전체 어플리케이션의 메모리를 제거하는 이전 방식에서는 그다지 지적되지 않았던 부분이다. 그러므로 변수 내에 있는 자료에 대해 주의를 기울일 필요가 있다.

둘째로 파일이 변경되었을 때는 서버를 재시작해야 할 필요가 있다. PHP에서는 클래스나 함수를 재정의하는 것이 불가능하기 때문이다. php-pm에서는 새로운 워커를 실행하는 방식으로 해결하려고 계획하고 있다.

셋째로 예외 처리가 되지 않은 예외가 발생했을 때 서버를 재시작해야 할 필요가 있다. 이 문제도 php-pm에서 해결할 예정이다.

넷째로 ReactPHP가 비동기 코드를 작성할 수 있는 기능은 제공하지만 대부분의 라이브러리가 (Symfony 포함) 이런 방식으로 작성되지 않았다. 즉, 정말로 웹 어플리케이션의 성능을 극대화 하고 싶다면 앱을 비동기 형태로 다시 작성해야 할 것이다. 재작성이 불가능한 것은 아니며 일반적으로 빠르게 가능하지만 결국엔 콜백 지옥에 빠지게 될 수도 있다. Node.js 앱에서 이 문제를 어떻게 다루는지 살펴보고 이 접근 방식을 사용하는 것이 가치있는 일인지 고민해봐야 한다. 내 의견으로는 시도해볼 가치가 충분하다.

다섯째로 큰 규모의 프레임워크를 ReactPHP와 함께 작성할 예정이라면 요청에 사용할 수 있던 내부 데이터를 어떻게 분리해서 처리할지 고려해봐야 한다. Symfony는 Request/Response를 사용하는 프레임워크다. 이런 구조로 요청과 응답이 구분된 코드 환경이 아니라면 PHP 어플리케이션을 개발하는 방식 자체에 대해 다시 생각해볼 필요가 있다. 이 변화는 극적으로 크다. 기본적으로 $_POST, $_GET, $_SERVER와 같은 것을 절대 사용할 수 없게 된다는 뜻이다. 현재 프레임워크가 이런 차이점을 지원하지 않는다면 쉽지 않을 것이다. 만약 지원하지 않는다면 집어 던지고 Symfony를 사용하자. 🙂 대단히 가치있는 일이다.

마지막으로

이 접근 방식이 Apache/nginx/lighttpd를 대체하는 것은 아니다. 이 방식은 HTTP 서버를 ReactPHP로 실행하는 것으로 어플리케이션을 실행하도록 준비하는데 가장 비싼 부분들을 제거하는데 그 포인트가 있다. 추가적으로 이 접근 방식을 사용했을 때는 새로운 캐시 레이어를 생각할 수 있다. 바로 PHP 변수다. APC 사용자 캐시를 사용하기 전에 PHP 배열을 캐시처럼 사용했던 것을 생각해보자. 물론 이런 접근 방식은 유효하지 않은 캐시가 발생하지 않도록 어떻게 다룰 것인지 염두해야 한다.

ReactPHP는 100% CPU 문제가 있었는데 고쳐졌다.

Symfony의 Request/Response 객체를 React의 Request/Response로 변환하기 위해서 작성한 Symfony 브릿지는 아직 완벽하진 않다. React의 HTTP 서버를 리팩토링 하는 작업이 필요하다. 물론 Symfony와 동작하는가에 대해서 질문은 “언제 되느냐” 하나만 남았다. 기여는 언제나 환영이다. 🙂

색상을 바꿔요

눈에 편한 색상을 골라보세요 :)

Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.