C. Titus Brown의 글 A framework for thinking about Open Source Sustainability?을 번역했다. 공유지의 비극과 그 해결책을 오픈소스 프로젝트에 적용해보는 글인데 오픈소스 거버넌스에서 기성 연구를 적용하는 글이라 더 흥미롭다.

오픈소스 지속가능성 체계적으로 생각해보기

공유자원 문제를 온라인 오픈 프로젝트에 적용할 수 있을까?

Nadia Eghbal의 아름다운 글인 “공유지의 비극”을 다시 읽었습니다. Elinor Ostrom의 공유자원 연구에 기반한 대안적 결과를 읽으며 여러 맥락에서 다양한 생각이 들었고 그 생각을 공유하려고 합니다.

Nadia는 오픈소스의 지속가능성 문제에 대해 탐구하고 있습니다. 중요한 오픈소스 소프트웨어가 상대적으로 적은 인원으로 관리되고 있으며 금전적 보장 방법이 많지 않은 상황이라 중대한 문제로 볼 수 있습니다. 이 문제의 규모나 범위를 따지면 누구와 대화하고 있냐에 따라 달라집니다. Python의 과학적 컴퓨팅 환경에 관한 충격적인 그림을 보면 numpy는 겨우 6명의 메인테이너가 유지하고 있는 점을 확인할 수 있습니다. Python에서 과학적 컴퓨팅 환경을 생각해보면 numpy에 상당히 의존적이기 때문에 메인테이너의 수가 적다는 이야기는 쉽지 않은 도전처럼 들립니다. 소프트웨어 개발에 관한 일반적인 도전 과제를 심층적으로 살펴보고 싶다면 “The Astropy problem”, Muna et al., 2016를 보도록 합니다.

(이후 논의에서는 과학적 소프트웨어에 치중되어 있지만 더 넓은 범위에도 적용할 수 있다고 생각합니다.)

저는 생물정보학(bioinformatics) 분야에서 일하고 있습니다. 이 분야는 다양한 조합의 소프트웨어 패키지를 사용하며 이런 패키지도 대학원생이나 포스트닥터, 학부에 계신 분이나 직원 등 다양한 사람이 관리하고 있습니다. 제 랩에서도 소프트웨어를 개발하고 있습니다. (주로 khmersourmash를 개발하며 그 외에도 여럿 있습니다.) 그리고 지난 해에는 지속적으로 소프트웨어를 유지하기 위한 방법도 개발했는데 주로 테스트와 지속적 통합을 중심으로 동작합니다. 하지만 항상 무언가 제대로 동작하지 않는 부분이 있습니다. 자동화된 관리에서 정말 사소한 상수인데도 그렇습니다. 운이 좋다고 해야할지 Jupyter와 같은 프로젝트와는 다르게 저희 소프트웨어를 사용하는 사람은 그리 많지 않아서 버그 리포트와 관리 문제가 주체 못할 정도로 쏟아지진 않습니다.

이미 언급했듯 오픈소스 소프트웨어를 관리하는 상수는 꽤 노력이 필요합니다. 매주 소프트웨어의 어떤 부분이든 작업하다보면 버그와 관련되어 있지 않는 경우가 드뭅니다. 아니면 새로운 기능을 추가하기 위해 뒤집어 엎어야 하는 부분이 많은 경우도 있습니다. 물론 랩에서 저나 다른 사람이 갑자기 연구나 코딩에 영감이 온다면 한동안 그런 문제를 마주할 일이 없긴 할겁니다.

학습에 있어서도 동일하게 노력이 필요합니다. 매년 2주짜리 과정분석 워크샵을 진행해 새로운 접근법, 소프트웨어, 자료 타입 등 발전에 필요한 학습을 합니다. 2010년에 배운 지식도 여전히 동작하긴 하지만 대부분 지독하게 썩었을 것이라 예상합니다.

다년간 여러 패키지를 관찰해본 제 경험에서는 활발하게 관리되지 않는 오픈 온라인 프로젝트(소프트웨어와 학습 자료를 포함)는 빠르게 부식한다는 확고한 결론에 도달했습니다. 또한 사람들이 많이 사용하는 프로젝트는 고쳐야 할 많은 버그와 문제를 항상 찾게 됩니다. 이 관리된다는 말에는 다른 사람들이 당신의 온라인 프로젝트를 _향상_하는데 실제로 도움을 주고 싶어서 GitHub에 리뷰 받아 머지되거나 거절되야 할 풀 리퀘스트를 제출하는 것을 포함하지 않았습니다. 또는 (정말 성공적이라면) 어떤 회사에서 프로젝트에 참여해 그들의 코드를 넣고 싶어할지도 모르겠지만 그런 회사도 포함하지 않았습니다.

프로젝트가 활발하게 관리되려면 지속적인 관심이 필요합니다. 이 관심에는 멋진 과학적 소프트웨어 패키지의 제멋대로 자란 생태계, 실제 메인테이너로 구성되어 있지만 상대적으로 작은 커뮤니티도 포함됩니다. 이 모든 것이 하나로 섞이면 과학계에서 마주하는 오픈소스 지속 가능성 문제로 이어집니다. 영웅적인 노력 없이는 이 모든 프로젝트를 유지할 수 있는 인력이 없습니다. 과학계에서의 소프트웨어 유지라는 명확한 커리어 패스가 존재하지 않는다면 오픈소스 유지보수를 위해 더 많은 사람을 찾는 일은 도덕적이지도, 지속 가능하지도 않다는 점이 명확합니다.

최근에 동료와 다른 오픈소스 프로젝트에 대해 브레인스토밍을 한 적이 있습니다. (뒤에 더 설명합니다.) 이 과정에서 이 문제를 공유자원 문제라는 틀 안에서 생각해보려고 했습니다. 프로젝트의 성공이란 지속가능성 문제와 맡닿아 있으며 공유 자원 프레임워크를 사용하면 그 지속가능성에 대해 판단할 수 있을 거라 생각하고 이 틀을 찾기로 했습니다.

공유 자원, 공유지의 비극, 지속가능성을 위한 디자인 원리

제가 알고 있는 공유자원 프레임워크는 Elinor Ostrom의 책, 공유지의 비극을 넘어 (원제: Governing the Commons)에서 왔습니다. 이 책에서는 아주 일반적인 공유자원 문제를 설명하고 공동체가 이런 문제를 어떤 방식으로 해결했는지 논의하고 있습니다.

개괄적으로, 60, 70년대 Elinor Ostrom과 동료는 “공유지의 비극”이라는 유명한 개념을 설명했습니다. 공유지의 비극은 공유자원이 이기적인 이유로 과도하게 이용되는 상황을 의미합니다. 이 문제는 피할 수 있습니다. 꼭 정부의 규제나 기업화만 이런 문제를 관리하는 방법이 아닙니다. 실제로 공동체 대다수는 공유자원을 지역적으로 관리하는 방식을 통해 문제를 해결했습니다. Ostrom과 다른 사람의 사례 연구에서 Ostrom은 공유 자원의 지속 가능성을 위한 8가지 “디자인 원칙”을 추출했습니다.

Nadia의 블로그 포스트에서 잘 설명하고 있고, 또한 읽기 좋게 정리되어 있는 위키피디아에 있는 8가지 디자인 원칙 링크를 남겨둡니다.

Ostrom은 이 연구에 대한 업적으로 2009년 노벨상을 수상했습니다.

오픈 온라인 프로젝트로 돌아가기

저와 동료들은 이 프레임워크를 디지털 자원인 오픈 온라인 프로젝트에 어떤 방식으로 적용할 수 있을까 고민했습니다. 디지털 자원은 물리적 자원과 달리 소비할 수 없고 누가 프로젝트의 소스 코드를 소비한다고 해서 다른 사람이 사용하지 못하는 것이 아니기 때문입니다.

대화 속에서 답을 찾았고 그 답은 노력(effort)이었습니다. 오픈 온라인 프로젝트의 공유 자원은 노력입니다.

기여자가 프로젝트에 새로운 기능을 추가하려면 무슨 일을 하게 되나요? 노력을 들입니다. 기여자가 버그를 보고할 때는? 역시 노력을 들입니다. 좋은 버그 리포트를 남길 때는? 노력을 들입니다. 문서를 작성할 때는? 기능을 테스트할 때는? 새 기능을 추천할 때는? 모든 과정에서 노력이 필요합니다.

하지만 이보다 더 깊은 주제가 있습니다. 새로운 기여자를 프로젝트로 끌어들이면 노력의 크기를 성장시킬 수 있습니다. 오픈 소스 프로젝트에 금전적 지원을 줄 새 투자자와 만나게 될 때, 프로젝트에 얼마나 많은 헌신적 노력이 있는가에 따라 그 지원의 규모가 증가하기도 합니다.

물론 전에 썼던 것과 같이 모든 기여가 노력으로 가치 있는 것은 아닙니다. 어떤 기여는 그 기여가 주는 가치보다 프로젝트에 더 많은 _비용 부담_을 주기도 합니다. 예를 들면 새로운 기능 제안, 나쁜 버그 리포트가 있습니다. 기여가 없는 커다란 기능 추가는 단순히 리뷰하고 거절하는 일에서도 핵심 프로젝트 메인테이너에게 엄청난 비용적 부담을 줄 수 있습니다. 이런 일은 유지보수 필요성이 낮은 일인데 유지보수가 프로젝트의 노력을 죄다 끌어가야만 하는 상황으로 뒤바뀔 수도 있습니다.

Fernando Perez가 #GCCBOSC에서 비슷한 점을 언급한 적이 있습니다. 바로 오픈소스 프로젝트에서 사람을 채용해가는 방식에 대한 지적입니다. 많은 회사가 오픈소스 커뮤니티에서 사람을 채용합니다. 단순히 생각해봐도 오픈소스 커뮤니티에서 이들을 교육하는데 들인 노력은 안중에 없이 발굴해가는 것은 문제입니다.

공유 자원의 지속가능성을 위한 8가지 디자인 원칙을 보고 “노력”을 공유 자원으로 정의한다면 오픈소스 프로젝트에 다음처럼 적용할 수 있습니다.

  1. 오픈소스 프로젝트에 누가 기여자인지 명확하게 정의합니다.
  2. 오픈 온라인 프로젝트에서의 노력은 프로젝트의 필요에 따라 지역적으로 적용됩니다.
  3. 오픈 소스 프로젝트는 기여하는 사람이 디자인 결정에 참여한다는 규칙을 따릅니다.
  4. 크게 기여하는 사람을 공식적으로 프로젝트에 참여하도록 해서 결정에 더 큰 역할을 맡을 수 있도록 합니다.
  5. 커뮤니티의 규칙을 위반한 기여자는 제재할 수 있는 범위를 둡니다.
  6. 충돌 대부분은 법률적으로 해결하기보다 프로젝트 내부적으로 해결합니다.
  7. 충돌 대부분은 가벼운 방법과 논의로 다룹니다.
  8. 오픈소스 기여자 대부분은 여러 프로젝트에 기여합니다. 예를 들면 Python 생태계에서는 같은 사람이 많은 프로젝트에 참여하고 있습니다. 이런 맥락에서 Python 생태계를 큰 규모의 공유 자원으로 여기고 많은 지역적 공유 자원으로 구성되어 있다는 점을 고려해야 합니다. 예를 들어 “핵심 CPython 개발”, “수치 연산/numpy 라이브러리” 등의 지역적 공유 자원이 존재합니다.

제 생각에는 많은 종류의 열린 공동체에 일반적으로 적용할 수 있는 항목이라고 생각합니다.

그렇다면 이게 무슨 의미일까요?

저와 동료가 이런 방식으로 생각하기 시작한 후로 이 관점에서 오픈소스 프로젝트와 온라인 공동체 자료를 보기 시작했습니다. 이 틀은 오픈소스의 지속 가능성을 생각하는데 아주 간단하고 좋은 프레임워크가 될 수 있습니다. “얼마나 열리면 지나치게 열린 것인가” 포스트는 정확히 이 생각에서 온 글입니다. 또한 당신의 프로젝트에 사람들이 더 모집되는 것을 긍적적으로 봐야하는지 설명합니다. 프로젝트에 노력을 쓸 수 있는 양이 늘어난다면 프로젝트 필요에 더 많은 노력을 사용할 수 있습니다. 이렇게 참여를 늘린다는 점에서 행동 강령과 기여자 가이드라인이 더 중요해집니다. 장기적으로 참여할 사람을 찾고 노력할 사람을 유지하는게 가능하게 됩니다.

이 관점 자체로는 어떤 문제도 해결하지 않습니다. 하지만 이 원칙은 정말 좋은 사례 연구와 함께 묶여 있으며 공유 자원에 대한 연구로 어떻게 공동체 자원을 지속 가능하게 관리하는지 심도있게 다루고 있습니다.

좀 더 세부적으로 다루자면 일반적인 오픈 온라인 프로젝트를 한다는 맥락에서 몇 가지 고려해야 할 점을 제안하고 있습니다.

먼저 오픈소스 프로젝트에 사용할 노력이 잠식되지 않도록 지켜내야 합니다. 프로젝트가 성공하려면 기여 잠재력이 가용 노력에 얼마나 영향을 주는지 평가해야 합니다. 이런 평가가 기술적 기여에는 이미 포함되어 있겠지만 (“기술적인 빚”이란 관점에서), 버그 리포트와 기술 제안에서도 고려되어야 합니다. (물론 프로젝트 대다수는 이런 점을 이미 고려하고 있습니다.)

둘째, 지속적으로 유지보수 할 필요가 있는 부분(코드, 문서, 설치 등)에 들어가는 비용도 노력이 들어가는 부분으로 계산해야 합니다. 새 기능을 기여하면서 이런 유지보수가 필요한 부분을 간과하는 경우가 있으므로 세심히 살펴야 합니다. 새 기여자가 계속 도움을 주려고 하나요? 유지보수하기 위한 노력을 들여 헌신하려고 하나요? 그렇지 않다면 이런 유지보수에 노력을 더하게 되는 기여는 파트너십 등을 통해서 미뤄야 합니다.

셋째로, 새 기여자를 교육하고 육성하는 방식은 장기적인 관점에서 가용 노력을 늘린다는 점을 염두해야 합니다. 하지만 기여자의 심리는 까다롭고 교육하고 육성한다고 해서 계속 이 프로젝트에 남아 있으리라는 예측은 그렇게 간단하지 않습니다. Python 코어 멘토 프로그램처럼 어떤 프로젝트는 뛰어난 인큐베이터를 갖고 있습니다. 이런 곳은 자신들의 노력을 새로운 기여자를 찾는 일에 사용하는데 관심있는 사람들이 운영합니다. 제가 생각하기에는 친근한 환경을 만들고 기여하고 싶은 사람들에게 갖는 기대감이 어떤 방향인지 잘 알려줄 수 있다면 새로운 기여자에게 도움이 될거라고 봅니다. 우리 유지보수 노력에 기여하고 싶은 사람에게 어떻게 하면 새로운 기능도 추가하고 버그도 개선할 수 있는지 기쁘게 알려줄 수 있다는 그런 마음가짐 말이죠. 장기적으로 보면 건강한 공동체가 즉, 건강한 프로젝트라고 할 수 있습니다.

넷째로, 모든 또는 대다수의 자원 제공자가 의사 결정에 참여하도록 허용하는 흥미로운 구조의 영향을 받습니다. 이 부분은 좀 더 세세하게 살펴봐야 하긴 합니다. 하지만 대략적으로 설명하면 프로젝트에서 어느 정도 수준의 투자와 기여가 어떤 실효적 수준에서 정책이나 원칙 수립 단계의 보상을 받게 되는지 정할 필요가 있습니다.

다섯째로, 자금 제공자의 기대치를 설정하고 투자를 얻는 과정에서 프로젝트의 성숙도를 측정하는 기준을 정의하는 일이 도움이 될 수 있습니다. 제 경험에 따르면 대부분의 자금 제공자는 _프로젝트 지속 가능성_를 도식화 하는 일을 가장 우선시 합니다. 위에서 얘기한 디자인 원칙(그리고 공유 자원에서 나온 사례 연구)을 프로젝트 성숙도의 기반으로 삼는다면 지속 가능성을 판단할 수 있게 됩니다. 프로젝트의 자금 지원 제안을 작성한다고 가정하면 어느 디자인 원칙과 연계된 부분을 향상하려고 하며 어떻게 지속 가능 프레임워크와 연결되는지 적용할 수 있습니다. 예를 들어 “지금 당장 새로운 기여자가 참여하는데 있어 우리 역량을 걱정하고 있습니다. 또한 회사로부터 큰 규모의 기여를 하겠다는 제안을 받고 있습니다. 저희는 프로젝트 관리 방식을 구축하고 기여자를 위한 안내를 향상해 새 기여자와 투자자에게 어떤 수준의 투자와 기여를 기대하고 있는지 명확하게 설명하려고 합니다.” 식으로 설명한다면 자금 제공자는 이런 설명에 만족할겁니다.

“노력”이라는 공유 자원 프레임워크가 오픈소스 프로젝트와 오픈 온라인 프로젝트에 일반적으로 정말 잘 맞나요?

좋은 질문입니다. 저는 공유 자원에 대해 해박하지 않고 앞으로도 읽어야 할 것이 정말 많습니다. 공유 자원 프레임워크와 부합하지 않는 부분도 보여서 좀 더 다듬어야 하는게 사실입니다. 하지만 오픈소스 프로젝트가 동작하는 방식과 잘 맞아 떨어진다는 게 제 직감입니다. 공유 자원과 같은 개념적인 틀은 제 관점을 다시 생각하게 하고 더 나은 그림에 맞춰볼 수 있게 하며 문제를 해결하는데 다른 결론에 닿게 할 지도 모르겠습니다. 이 예제는 앞서 언급했던 “얼마나 열리면 지나치게 열린 것인가” 포스트에서 찾아볼 수 있습니다.

제가 정말로 시도하고 싶은 일은 오픈 소스 프로젝트의 사례 연구에 참여하여 오픈소스 프로젝트가 실제 삶에서 어떻게 작동하는지 연구하며 이 프레임워크에 부합하는지, 혹은 부합하지 않는지 확인하는 일입니다. 몇 년 내로 안식년을 가지게 되는데 그 기회가 될지 모르겠습니다.

이 프레임워크에서 가장 좋아하는 점 하나는 오픈소스 프로젝트에서 자유 오픈소스 소프트웨어에서 큰 가치로 여겼던, 흐릿해진 목표와 분리해서 생각할 수 있게 되었다는 점입니다. “우리는 행복한 대가족이고 모든 부분이 함께 동작합니다!” 라는 구호는 메인테이너의 삶이 열린 기여에 의해 망가지는 상황을 마주하면서 희미해졌습니다. 오픈소스 프로젝트는 오늘날 세계를 구성하는 커다란 부분을 실제로 운영하고 있으며 지속 가능성을 포함한 어떤 관점에서도 공식적이지 않은 방식으로 운영했던 과거의 접근 방식으로는 더 이상 지속할 수 없다고 한 Fernado Perez의 이야기와 맥락이 닿습니다. 우리는 지속 가능한 오픈 프로젝트를 목표로 더 책임감을 갖고 더 현실적이고 냉철한 프레임워크를 만든다면 돈이 있는 투자자(그 도구를 사용하는 기술 회사나 학계 공동체)가 지속 가능성을 만드는 일을 도울 수 있을 겁니다. 이런 지속성 문제를 어떻게 해결할지 고민하는 것은 (예를 들어) 구글이 해야 할 일이 아니라 우리가 고민해서 그들에게 어떻게 우리를 도와야 하는지 이야기하고 그들이 우리와 함께 할 때 일을 해쳐나가는 것이 우리가 해야 할 일입니다. 그러나 지금 당장 중요한 프로젝트 대부분은 이런 접근이 어렵습니다. 그리고 어떤 경우도 간단하게 해결할 수 있는 문제가 아닙니다. 하지만 공유 자원이 이런 문제를 해결할 수 있는 접근법이 될 수 있습니다. 함께 생각해봅시다!

개인적으로 제가 참여했던 여러 오픈소스 프로젝트에서 어떤 노력을 했는지 다시 살펴보게 되어 흥미로웠습니다. 그 노력 중 하나도 지속적이지도, 지속되지도 않았습니다. 🙂 또한 그렇게 참여했던 프로젝트에서 Ostrom의 디자인 원칙에 맞지 않은 부분도 볼 수 있었습니다. 이 이야기는 다른 포스트에서 이어가도록 하겠습니다.

— 타이투스

특별히 Cameron Neylon과 Michael Nielsen에게 감사합니다. 이들은 Elinor Ostrom의 업적을 몇년 전에 알려줬습니다. Nadia Eghbal의 설명에도 감사를 전합니다. 주제에 대한 설명이 이 글의 재료가 되었고 앞으로도 같이 고민하며 전진했으면 합니다.

이 글을 쓰며 많은 이름을 언급했는데 Luiz Irber, Katy Huff, Katie Mack, Cory Doctorow, Jake VanderPlas, Tracy Teal, Fernando Perez, Michael Crusoe와 Matthew Turk에게 감사를 전합니다. #scifoo18과 #gccbosc에서도 같은 주제로 대화를 할 수 있었기에 SciFoo와 BOSC에도 감사합니다.

Buck Shlegeris의 My advice on studying algorithms를 번역했다.


알고리즘 학습에 대한 조언

소프트웨어공학 면접에서는 화이트보드 알고리즘 질문을 종종 냅니다. 이런 질문을 어떻게 공부해야 하는지 조언을 하려고 합니다. (저는 구글과 애플을 포함한 수많은 화이트보드 면접을 통과했습니다. 그리고 프로그래머가 이런 알고리즘 면접을 준비하도록 돕는 일이 제 직업의 일부입니다. 게다가 다양한 분야의 개발자를 대상으로 200회 이상의 기술 면접을 치뤘습니다.)

이 글은 Triplebyte이 아닌 제 자신으로서 쓰는 글입니다.

알고리즘 외에도 면접에 관한 여러 주제가 있습니다. 이런 주제는 Triplebyte의 포스트에서 잘 다루고 있습니다. 이 글에서 중요하게 다루려고 하는 내용은 Triplebyte의 포스트에서 2번 항목에 해당합니다.

배경: 왜 회사는 알고리즘 문제를 낼까요?

실생활에서 프로그래머가 이진트리 검색이나 그래프 탐색 알고리즘을 구현하는 시간은 거의 존재하지 않습니다. 그런데 왜 회사는 알고리즘에 대해 많은 질문을 낼까요?

이 질문을 존 왓슨과 코난 도일의 관점으로 해석할 수 있습니다. “회사가 알고리즘 문제를 내는 것이 왜 유용한가?”, 그리고 “어떤 회사가 알고리즘 질문을 하는 실제 일반적 원리가 무엇인가?”가 그 관점입니다.

먼저 알고리즘을 물어보는 이유를 설명하려고 합니다. 그리고 더 나아가 이 유행에 대한 냉소적인 입장에서 설명합니다.

먼저 직업 프로그래머 대다수가 아주 기초적인 일을 수행하지 못합니다. 예를 들어 고객 객체 목록이 있고 각 고객 객체에 구입 객체 배열이 존재합니다. 지난 주에 가장 많이 구입한 고객 다섯 명의 이름을 찾으려고 합니다. 제 예상에는 직업 프로그래머의 50%가 이 문제를 30분 이내에 풀지 못합니다. 이런 사람들을 실수로라도 채용하고 싶지 않을겁니다.

조금 덜 비관적으로 가정해봅시다. 프로그래밍 일을 위해 누군가 면접을 볼 때 어렵고 혼란스러운 문제를 잘 풀어낼 수 있는지 알아내려고 할겁니다. 모든 세세한 내용을 머리에 담고 있어야 풀 수 있는 것을 말이죠. 실생활에서 혼란스럽고 복잡한 문제가 존재하는 이유는 그 프로젝트를 몇 주 동안 봐야 할 만큼 큰 규모고 소프트웨어의 여러 부분을 동시에 고려해야 하기 때문입니다. 하지만 면접은 일반적으로 그렇게 깊은 프로그래밍 문제를 다룰 만큼 시간이 넉넉하지 않습니다. 그래서 규모가 크기 때문에 복잡한 문제를 물어보는 것보다 짧고 복잡한 질문을 물어보는 것입니다.

그럼 어떻게 복잡하지만 쉽게 설명할 수 있는 짧은 코딩 문제를 낼 수 있을까요? 그런 관점으로 생각해보면 제 생각에 여기서는 알고리즘이 좋은 선택입니다. 알고리즘은 컴퓨터 과학에서 대부분의 소프트웨어 엔지니어가 알고 있는 복잡한 분야며 쉽게 설명할 수 있고 구현하기 힘든 문제가 많이 존재합니다.

여기 조금 냉소적인 설명이 따라옵니다.

면접 프로세스는 이상할 정도로 끈적합니다. 엔지니어링팀은 팀의 기술 면접을 통과한 사람으로만 구성되어 있습니다. 그래서 모두가 면접이 옳은 방식이라고 믿게 되고 면접 과정이 소프트웨어 엔지니어링 능력을 측정하는데 매우 정확하다고 생각하게 됩니다. 그래서 회사에 알고리즘 면접 문화가 생기고 나면 그 이후로 바꾸기가 어려워집니다.

또한 모두가 알듯 구글은 10년 전에 놀라운 팀이 있었습니다. (지금은 그 당시보다 적습니다.) 그 당시에 구글은 알고리즘 면접 질문을 했습니다. 대부분의 회사가 자신들이 구글이 아니라는 점에 조금 불안했는지 (알다시피 최고의 지원자는 구글에 다 잃었으니까요), 이 회사도 구글 면접 과정을 따라하기 시작했습니다.

최악의 경우로 보면 알고리즘 면접은 기괴하고 못살게 구는 절차로 바뀌어 버립니다. 가끔 회사에서 무작위 난제를 내는 것이 위대한 지원자를 찾는 비밀 병기라고 단단히 착각해버려서 그런 생각을 바꾸는 일이 불가능한 경우가 있습니다.

종합해서 말하자면, 저는 이런 전통적이고 어려운 알고리즘 문제를 면접에서 안냈으면 합니다. 최악으로는 알고리즘 문제가 극단적으로 나쁜 면접 질문이 될겁니다. 제가 특히 싫어하는 질문은 이런 상황을 위해 따로 적어뒀습니다. 알고리즘 질문은 난제나 여러 통찰을 요구하는 경우에 특히 나쁜 질문입니다. (만약 알고리즘 면접 과정을 만들고 싶다면 언제든지 이메일을 보내시기 바랍니다. 이런 문제가 없는 질문을 하는 방법에 대해 더 자세한 의견을 드릴 수 있습니다.)

어떻게 공부하나요?

추가: Haseeb Qureshi의 블로그 포스트를 읽었는데 이 글에 동의합니다. 그리고 이 글이 좀 더 상세하다고 생각합니다. “일반 학습 전략”과 “프로그래밍 면접 공부 가이드” 부분을 읽으세요.

저는 알고리즘 문제에 답하려면 두 가지 다른 기술이 필요로 하다고 생각합니다. 첫째로 모든 대표적 알고리즘과 자료 구조 문제를 알아야 합니다. 둘째로는 부담되는 상황에서 알고리즘 논리를 화이트보드에 빠르게 풀어나갈 수 있어야 합니다. 이 두 주제를 나눠서 얘기해보려고 합니다.

표준 알고리즘 자료

회사의 시험을 준비하는데 있어 습득해야 하는 거의 필수적인 핵심 알고리즘 모음이 있습니다. 회사에서는 이런 목록에 들어있지 않은 질문은 하지 않으려고 합니다. 좋은 프로그래머 다수가 이 목록에 없는 질문에 대해서는 답을 모르기 때문이며 그래서 목록 외 질문을 냈다가 회사는 좋은 사람을 뽑는데 실패하게 됩니다.

알아야 할 자료구조는 다음과 같습니다.

  • list 구조: 배열, 동적 배열, 링크드 리스트(linked list)
  • set과 map 구조: 해시맵, 이진 검색 트리, 힙

여기서 언급한 자료구조는 필수 메소드가 어떻게 구현되어 있는지, 런타임은 어떻게 동작하는지 알아야 합니다. (list의 필수 메소드는 set, get, pushAtEnd, popAtEnd, insertByIndex, removeByIndex, set의 필수 메소드는 insert, remove, contains? 입니다.) 자료구조 구현을 어떻게 사용하는지 알아야 합니다. 예를 들면 getNearestElementTo(x) 메소드를 구현할 수 있어야 합니다. 이 메소드 즉, x와 가장 가까운 값을 찾는 구현을 하려면 이진 검색트리를 알아야 합니다.

이 문제를 해결하는데 이런 내용을 알아야 합니다.

  • 이진 검색트리 구현에 균형을 맞추는 코드가 필요하다는 점을 알아야 하지만 세부 내용은 몰라도 괜찮습니다. (선택 자료: 자기 균형 BST을 어떻게 구현하는지 빠르게 배우고 싶다면 이 트립을 참조하세요. 어떻게 레드블랙 트리가 동작하는지 이해하고 싶다면 좌편향 레드블랙 트리 또는 2-3-4 트리를 배우세요.)
  • 큐를 스택 두 개로 구현할 수 있다는 점을 알아야 합니다.

다음 알고리즘은 어떻게 구현하는지 알아야 합니다.

  • 그래프 알고리즘: 너비 우선 탐색(breadth first search), 깊이 우선 탐색(depth first search), 다익스트라 알고리즘 (dikstra’s algorithm)
  • 빠른 정렬 알고리즘 하나. 병합 정렬(mergesort) 또는 퀵 정렬(quicksort)
  • 배열에서 수행하는 이진 검색. 이 알고리즘은 제대로 작성하기 매우 까다롭고 대략적으로 알고리즘을 이해하고 있더라도 코드로 작성해볼 가치가 있습니다.

그리고 Big O 표기법도 대충이라도 편하게 사용할 수 있어야 합니다.

이 모든 내용을 어떻게 배워야 하나요? 제가 가장 좋아하는 자료는 Skiena의 Algorithm Design Manual입니다. 위에서 언급한 모든 내용을 챕터 26에서 다룹니다. 이 책을 좋아하는 이유는 저술 방식이 참여를 유도하고 각 부분에서 중요한 자료에 잘 초점을 맞추고 다루는데 이런 방식은 중요하다고 생각합니다. 이 책은 인터넷에서 무료로 찾을 수 있습니다. 이 책의 단점은 예제가 C로 작성되었다는 점인데 C를 읽지 못하는 개발자라면 접근성이 좋지 않습니다. 저는 챕터 16, 12는 꼭 읽어야 한다고 생각합니다. 이 부분은 인터뷰에서 나올 가능성이 극히 낮지만 필요 없다고 생각하는 부분이 진정 핵심적인 부분을 잘 보강한다고 생각하기 때문입니다.

이런 부분에 대략적인 설명을 보고 싶다면 Craking the Coding InterviewInterviewCake.com의 설명이 좋습니다.

저는 Skiena의 책이 극단적일 정도로 건조하고 딱딱한 유명 CLRS 교재보다 낫다고 생각합니다.

그래프 알고리즘에 대한 글을 쓴 적이 있는데 참고가 되었으면 좋겠습니다.

표준 알고리즘 기술

여기까지 인터뷰에 핵심적으로 필요한 부분을 확인했습니다. 이제 다른 종류의 프로그래밍 기술로 무엇을 테스트하는지 확인하고 제가 선호하는 학습 자료도 함께 확인합니다.

이런 기술에 있어서는 Cracking the Coding Interview(이하 CtCI) 책이 가장 유용합니다. 이 책에 대해서 작성한 글입니다.

알고리즘 면접 문제 중 가장 일반적이고 중점적으로 다뤄지는 요소는 다음과 같습니다.

  • 동적 프로그래밍: Skiena 책의 챕터 8 또는 CtCI에서 이 주제의 챕터에서 학습합니다.
  • 재귀: CtCI에 이 주제에 대한 멋진 챕터가 있습니다.
  • 유명 자료 구조를 반복(iterating)하는 문제: CtCI에서 각각의 자료 구조를 다룰 때 이 문제도 함께 다룹니다. 예를 들어 BST에서는 CtCI 트리 챕터를 참고할 수 있습니다.
  • 문제 해결을 위해 빠른 자료 구조를 조합하기: 이런 문제에 대한 예제는 이 글에서 확인할 수 있습니다.

CtCI에서 살펴볼 수 있는 많은 문제를 살펴보는 방법이 제가 드리는 가장 주요한 조언입니다. 이 문제에서 가장 중요하다고 생각하는 부분은 위 목록과 같습니다.

이런 부류의 문제를 어떻게 학습하는지에 대해 일반적인 생각은 이렇습니다. 제 생각엔 답안을 “훔쳐보는” 일은 그래도 괜찮다고 생각합니다. 면접 문제 푸는 일을 내던지고 아예 포기하는 것보다는 문제 풀다가 막히면 해결책을 보는 방법이 차라리 나은 접근이기 때문입니다.

알고리즘 면접에서 성공하기 위한 비기술적 측면

이런 질문은 실제로 부담되는 환경에서 답하는 연습을 해야 합니다. 진짜 사람이 질문하는 상황에서 말이죠. 이 부분에 대해서는 Triplebyte의 블로그 포스트에서 다루고 있고 2, 3, 7번을 읽어보기 바랍니다.

알고리즘과 자료구조에 대해 더 배우기

취업 목적 학습을 넘어서 본인을 위해 즐겁게 알고리즘과 자료구조를 배우고 싶다고 가정해봅시다. 어떻게 더 배워야 할까요?

가장 쉬운 방법은 위에서 필수로 배워야 한다고 한 핵심 자료 구조에 포함되지 않는 자료 구조 중 상대적으로 간단한 자료 구조를 학습하는 방법입니다. 트립, 스킵 리스트, 증강 이진검색트리, 서로소 집합 자료구조가 그 예로 모두 쉽게 이해할 수 있는 편이며 모두 멋진 알고리즘입니다.

자료구조 주제 중 이해하기 어렵지만 노력해서 이해하면 좋은 주제도 있습니다. 예를 들어 이 슬라이드에서는 이진트리와 2-3-4 트리를 설명합니다.

흥미로운 자료 구조를 배울 수 있는, 제가 좋아하는 자료는 다음과 같습니다.

  • Skiena의 챕터 12와 이후 챕터
  • 스탠포드의 멋진 강의인 CS166. 이 강의의 슬라이드는 멋지고 읽기 좋은 편입니다. 저는 여기서 다룬 문제가 즐거웠습니다. 자료구조와 더 놀고 싶다면 이 프로젝트 아이디어 핸드아웃을 추천합니다.
  • 저는 이런 작업처럼 그다지 어렵지 않은 자료 구조 문제를 아마추어 활동으로 재미삼아 한다는 점이 자랑스럽습니다. 이 문제를 풀기 위한 해결책으로 고급 자료구조에서 얻은 몇 아이디어를 적용했다는 점이 멋지지 않나 생각합니다.

자료

lazlojuly의 글 Node.js module.exports vs. exports을 번역했다.


node.js의 module.exports와 exports

(노트: 이 글은 Node.js 6.1.0 릴리즈 이후에 작성되었습니다.)

요약

  • module.exportsrequire() 함수를 사용했을 때 반환 받는 변수라고 생각해봅시다. 비어 있는 객체가 기본값이며 어떤 것으로도 자유롭게 변경할 수 있습니다.
  • exports 자체는 절대 반환되지 않습니다! exports는 단순히 module.exports를 참조하고 있습니다. 이 편리한 변수를 사용하면 모듈을 작성할 때 더 적은 양의 코드로 작성할 수 있습니다. 이 변수의 프로퍼티를 사용하는 방법도 안전하고 추천하는 방법입니다.
exports.method = function () { /* ... */ }
// vs.
module.exports.method = function () { /* ... */ }

간단한 모듈 예제

먼저 예제로 사용할 코드가 필요합니다. 간단한 계산기로 시작합니다.

// calculator.js

module.exports.add = (a, b) => a + b

다음처럼 사용할 수 있습니다.

// app-use-calculator.js

const calculator = require('./calculator.js')
console.log(calculator.add(2, 2) // 출력: 4

모듈 감싸기

Node.js는 require()로 모듈을 불러올 때 함수 래퍼(wrapper) 형태를 사용해 내부적으로 감싸서 호출합니다.

(function (exports, require, module, __filename, __dirname) {
  // calculator.js의 내용은 여기에 추가해 실행합니다.
  module.exports.add = (a, b) => a + b
});

module 객체

변수 **“module”**은 객체로 현재 모듈을 나타냅니다. 이 변수는 각 모듈에 지역적이며 비공개(private) 입니다. (모듈 코드에서만 접근할 수 있습니다.)

// calcualtor-printed.js

module.exports.add = (a, b) => a + b
console.log(module)

// 이 모듈을 require('./calculator-printed.js')로 호출하면 다음과 같은 결과를 볼 수 있습니다.
//
// Module {
//   id: '/Users/laz/repos/module-exports/calculator-printed.js',
//   exports: { add: [Function] },
//   parent: 
//     Module { ... }
//   filename: '/Users/laz/repos/module-exports/calculator-printed.js',
//   loaded: false,
//   children: [],
//   paths: [ ... ]
// }

module.exports

  • 객체 참조로 require() 호출을 하면 받는 값입니다.
  • Node.js에 의해 자동으로 생성됩니다.
  • 일반 JavaScript 객체를 참조합니다.
  • 또한 기본값은 비어 있습니다. (앞서 코드에서는 add() 메소드를 추가했습니다.)

다음 두 방법으로 module.exports를 사용합니다.

  1. 공개(public) 메소드를 붙여 사용합니다. (앞서 작성한 예제가 이 방법입니다.)
  2. 직접 작성한 객체나 함수로 대체하는 방식을 사용합니다.

왜 대체하는 방식으로 사용할까요? 대체하는 방식으로 사용하면 다른 클래스를 임의적으로 수정해 반환하는 것도 가능합니다. 다음 ES2015 예제를 보겠습니다.

// calculator-base.js

module.exports = class Calculator {
  add(a, b) {
    return a + b
  }
  substract(a, b) {
    return a - b
  }
}

이 calculator-base 예제에서는 클래스를 내보냈습니다. 다음 예제에서는 “Calcaulator” 클래스를 확장한 후, 클래스의 개체를 내보냅니다.

// calculator-advanced.js

const Calculator = require('./calculator-base.js')

class AdvancedCalculator extends Calculator {
  multiply(a, b) {
    return a * b
  }
  devide(a, b) {
    return a / b
  }
}

module.exports = new AdvancedCalculator()
// app-use-advanced-calculator.js

const calculator = require('calculator-advanced.js')

console.log(calculator.add(2, 2))      // 출력: 4
console.log(calculator.multiply(3, 3)) // 출력: 9

exports 별칭(alias)

  • exports는 편의 변수로 모듈 작성자가 코드를 덜 작성하도록 돕습니다.
  • 이 변수의 프로퍼티를 사용하는 방식은 안전하고 추천하는 방법입니다. (예를 들면 exports.add = function () { /* ... */ } 식으로)
  • exportsrequire() 함수에서 반환되지 않습니다. (module.exports는 반환하지만요!)

좋은 예제와 나쁜 예제를 확인합니다.

// calculator-exports-exmaples.js

// 좋음
module.exports = {
  add(a, b) { return a + b }
}

// 좋음
module.exports.subtract = (a, b) => a - b

// 가능함
exports = module.exports

// 위에서 작성한 코드보다 간단하고 짧은 코드
exports.multiply = (a, b) => a * b

// 나쁨, exports는 바깥으로 전달되지 않음
exports = {
  divide(a, b) { return a / b }
}

노트: module.exports를 함수나 객체로 대체하는 경우는 일반적인 접근법입니다. 이렇게 대체하면서도 여전히 exports 축약을 사용하고 싶다면 exports를 새로 대체할 객체를 가리키도록 설정해야 합니다. (위에서도 이런 방법을 사용헀습니다.)

exports = module.exports = {}
exports.method = function () { /* ... */ }

결론

변수명은 exports지만 실제로 내보내지 않는다는 사실은 좀 혼란스러울 수 있습니다. 막 node.js를 시작한 사람이라면 특히 그럴겁니다. 공식 문서에서도 이 부분이 조금 이상합니다.

exports와 module.exports의 관계가 마술처럼 느껴진다면 exports는 무시하고 module.exports만 사용하도록 지침을 드립니다.

제 경우엔 이런 코드가 마술이라고 생각하지 않습니다. 개발자라면 사용하는 플랫폼과 언어가 어떻게 동작하는지 깊게 이해하려는 노력을 항상 해야합니다. 이처럼 깊게 이해하는 과정에서 프로그래머는 값진 자신감과 지식을 얻으며 코드 품질, 시스템 구조와 생산성에 긍정적인 영향을 주게 됩니다.

제 글을 읽어주셔서 감사합니다. 의견이나 생각은 언제나 환영하니 덧글로 남겨주세요.

호주 생활을 정리하게 되었다. 얼마 지낸 것 같지 않은데 날을 세보면 벌써 만 6년이 넘었다. 처음 어떤 마음으로, 어떤 생각으로 왔는지 예전 글을 읽어보면 참 멀리까지도 잘도 왔다는 기분이 든다. 도움도 응원도 많이 받았고, 많은 기도 덕분에도 잘 정착하고 지냈다. 다만 내가 받은 만큼 주변에 도움을 많이 주지 못한 것 같아 미안한 맘이 든다.

요즘 지내면서 호주 생활에서 얻은 점과 잃은 점을 생각해봤지만 여러 감정이 있어서 명료하게 정리하기 어려웠다. 처음 왔을 때 환율이 1달러에 1200원을 넘고 있었는데 마트에서 손을 부들부들 떨며 사먹었던 일도 기억난다. 5시면 동네 대부분 가게를 닫는 것 보고 놀라기도 했다. 네팔 아저씨네 살면서 매일 카레만 먹던 일이라든지, 멜버른의 들쑥날쑥한 날씨에 감기를 달고 살았던 일, 비 펑펑 오는 날에 아내와 끝 없는 그레이트오션로드를 운전했던 일, 가족과 함께 한 멜버른 여행, 저스틴님 댁에서 지내며 함께 한 수많은 바베큐도 생각난다. 동네 공원도 많아서 언제든 산책할 수 있고 어디 가든 맛있는 커피를 마실 수 있는 이 멜버른이 많이 그리울거다.

크고 작은 일 많았던 첫 회사, 대학이라 특별했던 두 번째 회사, 열정적인 사람이 많았던 컨퍼런스, 밋업에 가서 에너지도 많이 받았고 Korean Developers Meetup에서 한국어로 부담 없이 기술 이야기를 하기도 했다. 아는 분들과 함께 사이드 프로젝트를 하기도 했고 혼자서 이것저것 만들기도 했다. 한국에서는 내 커리어에 대한 큰 그림을 그리지 못했는데 여기 와서는 기술적으로 어떤 깊이를 가져야 하는지 생각을 많이 하게 됐다.

생각해보면 이상한모임도 호주 오고나서 생긴 일이다. 멀리 있어 참여하지 못하는 아쉬움이 있기도 하고 작년부터 제대로 참여하지 못해서 미안한 맘도 크다. 지금 사는 곳으로 이사오고 나서는 이상한모임서 온 분들과도 밤샘 코딩을 하기도 했었고, 이모콘도 참여하고 진행하기도 했었다. 한국서 참여하지 못했지만 대신 멜버른으로 오시는 분들과는 짧게라도 함께 시간을 보낼 수 있던 점에도 감사했다. 생활이 좀 정돈되면 각오를 다시하고 부지런히 하고 싶다. 다들 코알라로 나를 불렀는데 호주 나가도 그 닉네임 계속 가져갈 수 있을까, 다음 코알라님이 얼른 출현했음 좋겠다.

이 글도 이렇게 길게 쓸 생각이 아니었는데 또 줄줄 쓰게 되었다. 그동안 코드도 손에 잡히지 않고, 글도 뜸했던 이유가 이런 변화를 앞두고 있어서 그런거 아니었나, 핑계 대본다. 문득 내가 블로그에 올린 글을 보고 호주에 올거라고 여러가지 물어봤던 분들도 생각나고 괜스레 미안한 마음도 든다. 리로케이션 하는 과정이 모두 끝난게 아니라서 어디로 가는지는 간 이후에 정리할 수 있을 것 같다. 월말에 떠나게 될텐데 그 전까지는 도움받은 분들과 함께 식사도 나누고 시간을 보내고 싶다.


호주 생활을 정리한 과정도 기록삼아 적는다.

  • 렌트 정리하기
  • 유틸리티 해지하기
  • 인터넷 해지하기
  • 가구/집기 정리하기
  • 각종 주소지 변경하기
  • 짐 보내기

그동안 플랫을 렌트해서 지내고 있었다. 이사 갈 때는 노티스를 보내야 하는데 주마다 샘플 양식이 있다. 간단히 양식을 찾아 내용을 작성하면 된다. 별다른 하자가 없다면 28일을 줘야 한다. 14일만 주면 되는 줄 알고 있다가 뒤늦게 노티스를 줘서 키를 반납한 후에도 돈을 조금 더 내게 되었다. 노티스를 주면 빌려준 곳에서 인스펙션을 한 차례 하게 되는데 이 과정에서 집주인이 집을 유지보수할 곳이 있는지 확인한다.

그리고 노티스를 주면 Confirmation of vacating 을 우편으로 보내준다. 노티스를 언제 보냈는지, 얼마간 렌트했는지, 디파짓은 얼마인지, 본드(Bond)는 어떻게 지불되는지 등 내용과 함께 여러 조항이 적혀있고 본인 이름과 주소, 연락처와 서명을 해서 제출한다. 키 반납은 약속한 날에 프로퍼티를 관리하는 부동산에 가져다주면 된다. 반납이 늦으면 렌트비가 계속 나간다.

전기와 가스는 Origin energy와 계약해서 사용하고 있었다. 이 회사는 웹사이트에서 간편하게 해지할 수 있었다. 해지 비용으로 $40 정도 나갔고 마지막 인보이스와 함께 지불한다.

인터넷은 Engin을 사용하고 있었다. 여기는 웹페이지에서 해지할 수 없어서 상담시간에 맞춰 전화했다. 생각보다 별 문제 없이 해지할 수 있었다. 다만 원하는 날짜 대신에 결제일에 맞춰 해지 가능했다. 그래서 집에 인터넷이 예정보다 조금 일찍 끊길 예정이다.

책은 한국어 도서가 많아서 간단하게 페이지를 하나 만들어 주변 분들에게 공유했다. 직접 가져가야만 하는 책이라서 복잡하지 않게 만들어 올렸다.

가구와 집기는 페이스북 마켓플레이스를 통해 정리했다. 내 경험으로는 검트리보다 훨씬 좋았다. 검트리는 연락 오는 사람이 어떤 사람인지 확인도 불가능하고 구입하러 온다고 하고는 오지 않는 경우도 꽤 있었다. 반면 마켓플레이스는 페이스북의 프로필이 그대로 공개되기 때문에 구매자를 적당히 스크리닝 할 수 있었다. 또한 페이스북과 연동이 되어 있어서 집 주변에 물건이 올라오면 노티피케이션이 가게 되어 있다. 덕분에 가까운 곳에서 당일날 바로 와서 가져가는 경우도 많았다. 생각보다 빨리 정리할 수 있어 좋았다.

주소지 변경이 필요한 곳은 은행과 메디케어다. 그동안 커먼웰스 뱅크를 주거래로 하고 있었다. 해외 주소로 변경하려면 방문해야 한다. 그래서 동네 지점에 방문했다. 별다른 증명 없이도 해외 주소로 변경할 수 있는데 사진이 있는 ID가 필요하다. 해외 주소는 호주와 입력하는 방식이 달라서 문제가 될 수 있다. 특히 우편번호 란이 별도로 없기 때문에 우편번호가 중요하다면 주소란에 같이 입력해달라고 해야 한다. 변경한 후에는 account information letter를 뽑아달라고 해서 변경된 주소가 원하는 방식으로 줄바꿈 되어 표기되는지 확인하는게 좋다. 그리고 모든 statement는 이메일로 전환했다. 해외에서도 우편으로 받을 수 있지만 그 비용은 계좌에서 차감된다.

나는 그다지 우편을 별로 받지 않아서 문제가 없는데 만약 우편을 계속 받아야 한다면 mail forwarding 이나 mailbox 서비스를 신청하면 된다. 가입하면 주소를 주는데 그쪽으로 바꾸면 모아서 보내주거나 스캔해서 이미지로 받아볼 수 있다. 가격도 천차만별이라 잘 읽어봐야 한다.

메디케어는 국외 주소지 지정이 안되는데 거주자 대상인걸 생각하면 당연하다. 우편물을 받을 수 있는 주소가 있다면 그쪽으로 변경하면 된다. 아니면 smarttraveller.gov.au 에 들어가서 여행자로 등록하는 방법이 있다. 나는 여행자로 등록하는 방식으로 처리했다. 호주 정부서 제공하는 페이지인 myGov에 메디케어 계정을 연결했다. MyGov에 서비스를 연결해두면 모든 안내 메시지가 myGov inbox로 전달해줘서 유용하다.

짐을 보내려고 이삿짐 업체를 알아봤는데 크게 해로, 육로에 따라 가격이 달라지며 업체마다 천차만별이다. 따져보면 거의 비슷한 가격이긴 한데 유독 저렴한 곳이라면 기본 보험을 제공하지 않는 경우일 수 있으니 확인해야 한다. 내 경우도 저렴하다고 계산했더니 실제로 보험을 더하면 다른 곳보다 가격이 더 비싸졌다. 조급한 마음에 골라서 결정했던 점이 좀 실수였지 않나 생각한다. 짐이 적다면 fedex, dhl을 이용하는 쪽이 저렴하다. 나는 아이맥도 보내야 했었고 짐을 많이 줄였어도 책 두 박스, 옷과 잡화 세 박스가 나왔다. 직접 포장해서 full cover 보험은 안되고 restricted 보험을 들었는데 배송에 보험까지 해서 650불 가량이 나왔다. 이렇게 가져가도 택배는 내가 간 다음에 도착하게 되어 있는데 너무 빨리 가져가면 스토리지에 보관하는 비용을 추가로 낸다고 한다. 짐을 찾고 나서야 얼마나 썼는지 정확하게 나올 것 같다.


이외에도 환전이라든지 생각해야 할 부분이 좀 있다. 아직 환전은 명확하게 생각하지 않았는데 가서 지낼 동안 돈은 바꿔가고 나머지는 transferwise라든지 hifx 같은 곳으로 보낼까 생각중이다.

지금의 마이크로소프트는 이전의 MS가 보여줬던 모습과는 확연히 다르다. 오픈소스로 공개하는 많은 결과물과 다양하면서도 견고한 서비스를 제공하는 클라우드, 유기적으로 통합된 프로덕트는 커다랗고 정적인 회사라는 내 생각의 틀을 바꿨다. MS의 CEO로 사티아 나델라가 선임된 날을 기억한다. 그 날 이후 달라진 MS를 보면서 단순히 CEO의 차이로 이렇게 체질을 바꿀 수 있는 것일까 하고 그동안 생각했었다. 사티아 나델라의 책 <히트 리프레시>를 읽으면서 지금의 MS이 되기까지의 과정을 엿볼 수 있었다.

조직이 어떻게 생각하고 행동할지를 문화가 결정한다고 하지만 문화의 틀을 빚는 주체는 개인이다.

책은 사티아 나델라의 이야기로 시작해 CEO가 되기까지 어떤 삶을 살았고 어떤 역할을 맡았는지 설명한다. 그리고 미래에는 어떤 기술이 주목받고 그 변화의 중심이 되려면 어떤 비전을 갖고 준비해야 하는지 풀어간다. 사티아의 독특한 이력도 흥미로웠고, CEO가 되기 전에는 클라우드 부문에서 Azure를 만드는 과정, CEO가 된 이후에 체질을 바꾸기 위한 과정도 인상적이었다. 책 전체에서 강조된 “공감”이라는 키워드를 조직에 녹이기 위해 어떤 일을 했는지 잘 설명하고 있다.

어떻게 해야 우리 기술로 우리 정체성에 말을 걸고 우리 사용자에게 유일무이한 가치를 부여할 수 있을까?

지금까지 내가 해왔던 일을 생각해보면 기술의 끝단에 서서 기술을 비지니스에 녹이고 사용자에게 제공하는 역할을 해왔다. 이 책에서 언급한 변화 앞에서는 어떤 역할을 하는 사람이 되어야 하나 고민된다. 일반 사용자를 대상으로 하는 도구를 만드는 일도 즐거운 일이지만 기술적 깊이가 있는 일은 아직 안해봐서 그런지 해보고 싶은 마음이 크다. 나는 어떤 역할로 어떤 위치에서 무슨 일을 해야 하는가 끊임없이 되물어보게 한다.

사람이든 조직이든 사회든 스스로 새로고침을 해야 하는 순간이 찾아온다. 그 순간이 오면 다시 열정을 불러일으키고 새로운 마음으로 목표를 재설정하고 치열하게 고민해야 한다.

무슨 일을 해야하나 생각을 오래 해온 탓에 그저 관성적인 고민이 아닌가 싶기도 했다. 치열하게 고민한다는 말이 울렸다. 새로고침을 해야 하는 순간이 왔다.

사람들이 변화에 저항하는 근본적인 이유는 미지에 대한 두려움 때문이다. 정말 중요하지만 확실한 답이 없는 질문은 사람들을 두렵게 한다.

앞으로 무슨 일을 해야 하는가, 이 질문은 늘 두렵다. 본문에서는 이 질문을 마이크로소프트에 던졌다. 서버 중심의 환경에서 클라우드로 나아가기 위해 두려운 질문을 마다하지 않았다. 또한 책에서는 미래를 대비하기 위한 기술로 인공지능, 증강현실 그리고 양자 컴퓨팅을 꼽았다. 이런 기술을 사용한 서비스가 하루가 다르게 나오고 있고 삶을 변화하고 있다. 나는 이런 질문에 어떻게 답할 것인가.

책을 읽은 후에도 여러 번 뒤적이게 된다. 고민이 많은, 지금의 나에게 답을 바로 주는 것은 아니지만 고민을 어떤 방향성과 동력을 갖고 해야하는지 알려주고 있다. 남은 한 해도 여러가지 변화가 있을 예정인데 고민 속에서도 하나씩 잘 해결해가고 싶다.

언제쯤이면 스크린에 뜬 키보드를 사용하는데 더 편하다고 생각하게 될까. 자판 세대를 살면서 쓰기 좋은 키보드를 찾기 위한 검색을 반복하는 것은 어쩔 수 없는 일 같다. 이전부터 눈여겨 봤던 마이크로소프트의 유니버셜 폴더블 키보드를 구입했다.

그동안 아이패드 스마트 키보드를 사용해왔다. 하지만 9.7인치 스크린의 세로폭에 맞춘 키보드라서 키도 작고 손도 모아서 쳐야 했다. 매일 사용하면 어색함이 없지만 다른 키보드를 잠시라도 만지고 오면 바로 이질적인 느낌이 들 수 밖에 없다. 그런 키보드에도 아이패드를 많이 썼던 이유는 그동안 아이폰 5s를 계속 사용해왔고 작은 크기에서 글쓰는데 늘 답답함이 컸던 탓이다.

하지만 아이폰 X를 구입하고 나서는 달라졌다. 더 많은 내용을 볼 수 있어서 그런지 글을 쓰고 다시 보는데도 큰 어려움이 없다. 스마트 키보드를 아이폰X에서 사용할 수 있다면 조금 더 편했을지 모르겠다.

그래서 블루투스 키보드를 대신 들고 다니기도 했다. 애플 키보드도, 싱크패드 키보드도 들고 다녀봤다. 애플 키보드는 모서리가 날카로워서 케이스에 넣지 않으면 가방 안에 모든 것에 흔적을 남겼다. 싱크패드 키보드는 내가 들고 다니는 작은 가방엔 맞질 않아서 백팩을 매고 다녀야 했다. 무슨 키보드든 들고 다니면서 혹시 다른 키보드는 없나 매번 생각했다. 가방에 넣고도 그냥 잊고 지내다가 필요할 때 불쑥 꺼내서 쓸 수 있는 그런 키보드는 없을까.

그러던 중 마이크로소프트의 유니버셜 폴더블 키보드를 다시 보게 되었다. 예전에도 구입할까 고민하긴 했었는데 비싼 가격에 구입하지 않았었다. 이번에는 Ebay에서 AUD$79에 판매하고 있길래 구입했다.


포장은 여느 마이크로소프트 제품과 같다. 점점 덕지덕지함을 벗어나는 패키징이 마음에 든다. 키보드 소재는 부드럽고 무게도 무겁지 않다.

2대의 기기까지 페어링을 지원하고 운영체제를 변경할 수 있는 키도 붙어 있다. USB 케이블도 동봉되어 있고 한 번 충전으로 꽤 오래 사용할 수 있다고 한다. 충전이 얼마나 되었는지 표시가 없는건 아쉽지만 충전하면서도 사용하는데는 전혀 문제 없었다.

Microsoft Universal Foldable Keyboard

타이핑하는 느낌은 애플 블루투스 키보드를 눌렀을 때와 가장 비슷하며 좀 더 정숙하다. 키 피치는 높지 않아서 스마트 키보드 보다는 나은 경험이지만 일반적인 블루투스 키보드보다는 얕은 느낌이 든다. 서피스 키보드와도 유사하게 느껴진다.

Microsoft Universal Foldable Keyboard

키보드 레이아웃도 전혀 작다고 느껴지지 않고 키도 마음에 든다. 다만 오른쪽에는 키를 좀 더 오밀하게 배치한 탓에 특수 문자를 입력할 때 잘못 누르는 경우가 좀 있다. 더불어 방향키도 작은 편이다. 탐색을 터치 스크린으로 한다면 큰 문제는 아니다. 그리고 B키가 왼쪽에 위치하고 있다. 나는 B는 왼손으로 누르는데 ㅠ는 오른손으로 누르고 있었다. 익숙해질 때까지 좀 시간이 걸렸는데 쉽게 고칠 수 없다면 이 키보드에서 가장 큰 단점이지 않을까 싶다.

Microsoft Universal Foldable Keyboard

장점은 역시 접을 수 있다는 점이다. 평소 들고 다니는 수첩과 큰 차이가 없다. 재질도 부드러워서 가방 안 다른 물건에 손상을 주는 일도 없다. 휴대성에 있어서는 최고다.

Microsoft Universal Foldable Keyboard

가끔 뭔가 잘 안되면 지름으로 해결하려는 경향이 있는데 아마 글이 잘 안쓰여지니 또 키보드를 질러서 풀어보려는 모양이다. 그래도 생각했던 용도대로 가방에 수첩과 함께 넣어두고 지내다가 필요할 때 언제든 꺼내서 쓸 수 있는 좋은 키보드다. 부지런히 사용해서 본전 찾아야겠다.

오늘이 마지막 출근이다.

1년 조금 넘는 기간을 다닌 이번 회사에서는 이전 다녔던 곳과는 확연히 다른 경험을 했다. 규모도 달랐고 프로세스도 갖춰져 있었다. 다른 부서와 함께 일하는 경험은 처음이었다. PM과 BA와 함께 일하고, 아키텍트에게 리뷰도 받고, 수많은 단계를 거쳐 배포와 retrospective까지 가는 모든 과정이 배움의 연속이었다. 모두 친절하고 많이 도움 받았다.

회사 다니는 동안 크고 작은 다양한 업무를 했고 프로젝트는 대외적으로 인정도 받아 즐거웠다. 업무 프로세스가 많고 내 영어가 부족해서 지치는 면도 좀 있었지만 좋은 사람들과 함께 재미있는 프로젝트를 할 수 있었다. 프로젝트에서 대학에서만 볼 수 있는 그런 도메인 지식을 배운 것도 즐거운 경험이었다.

또한 성평등과 문화 다양성이 로드맵에 들어있을 정도로 신경을 많이 쓰는 회사라서 그런지 지난 회사에서 겪었던 작은 편견조차 여기서는 볼 수 없었다. 덕분에 회사가 차별에 맞서기 위해서는 어떤 역할을 해야 하는지, 어떤 장치를 둬야 하는지도 업무 외적으로도 많이 배웠다.

다음은 어느 회사로 갈지 정해지지 않았다. 다음 회사에서는 다른 기술을 사용해서 일하고 싶다는 막연한 생각만 있다. 오랜만에 어디에도 소속되지 않은 상태로 시간을 보낼 예정이다. 그동안 만나지 못했던 사람들도 보고, 책도 읽고, 여유도 가질 참이다. 물론 나날이 들어오던 돈이 더 이상 없다는 상상은 좀 걱정이 된다. 이런 면에서 보면 월급 중독이 심한 것 아닌가 싶기도 하고.

매년 무언가 배우면서 미래를 대비하려고 했지만 조급한 마음만 앞서서 그런지 1, 2년이면 닳아버리는 지식만 반복해서 접했던 것 같다. 급함에 너무 좁은 시각으로 살지 않았나 생각이 많이 들었다. 그래서 이번에는 기간을 두고 집중해서 볼 수 있는 지구력도 좀 만들고, 무엇을 오래 보고 배울지 부지런히 찾아야지 싶다. 무엇이 미래에 정말 필요할까, 나는 어떤 역할로 그 기류 속에 서 있을 수 있을까.

무엇을 준비하고 어떻게 대비하는지 고민과 함께 정말 하고 싶은 것은 무엇인지도 생각하게 된다. 하고싶은 일이 기술적 깊이가 있는 엔지니어링인지, 아니면 비즈니스와 더 맞닿아 있는 기술 컨설팅인지, 아니면 좀 더 큰 그림을 그리는 아키텍트가 되고 싶은 건지. 이 전환 기간동안 좋은 결정을 내릴 수 있는 토대를 잘 가꿔서 결정 하고싶다.

<p style=" margin:8px 0 0 0; padding:0 4px;">
  <a href="https://www.instagram.com/p/BinbuBlBUUS/" style=" color:#000; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none; word-wrap:break-word;" target="_blank">Today is my last day at Swinburne. I’ve met so many great individuals in here and I really enjoyed all collaborated work with co-workers for awesome projects. I feel grateful for all my experience!</a>
</p>

<p style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin-bottom:0; margin-top:8px; overflow:hidden; padding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;">
  A post shared by <a href="https://www.instagram.com/edwardykim/" style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px;" target="_blank"> 용균</a> (@edwardykim) on <time style=" font-family:Arial,sans-serif; font-size:14px; line-height:17px;" datetime="2018-05-10T23:56:14+00:00">May 10, 2018 at 4:56pm PDT</time>
</p>

벌써 멜번에서 만 6년의 시간을 보냈다. Stop and smell the roses. 고민도 많고 고생도 했던 기간이지만 몸 건강히 잘 지내는게 얼마나 다행인지 생각한다. 지금까지 지내온 시간과 받은 도움에 감사하는 시간을 보내고 싶다.

Stephan BoyerWhat are covariance and contravariance?을 번역한 글이다.


공변성과 반공변성은 무엇인가?

서브타이핑은 프로그래밍 언어 이론에서 까다로운 주제다. 공변성과 반공변성은 오해하기 쉬운 주제이기 때문에 까다롭다. 이 글에서는 이 용어를 설명하려고 한다.

이 글에서는 다음과 같은 표기법을 사용한다.

  • A <: BAB의 서브타입이라는 뜻이다.
  • A -> B는 함수 타입으로 함수의 인자 타입은 A며 반환 타입은 B라는 의미다.

동기부여 질문

다음과 같은 세 타입이 있다고 가정하자.

Greyhound <: Dog <: Animal

GreyhoundDog의 서브타입이고 DogAnimal의 서브타입이다. 서브타입은 일반적으로 추이적 관계(transitive)를 갖는다. 그래서 GreyhoundAnimal의 서브타입이라 할 수 있다.

여기서 질문이다. 다음 중 Dog -> Dog의 서브타입이 될 수 있는 경우는 어느 것일까?

  1. Greyhound -> Greyhound
  2. Greyhound -> Animal
  3. Animal -> Animal
  4. Animal -> Greyhound

이 질문에 어떻게 답할 수 있을까? Dog -> Dog 함수를 인자로 받는 f 함수를 살펴보자. 반환 타입에 대해서는 크게 생각하지 않는다. 구체적으로 적어보면 다음과 같다. f : (Dog -> Dog) -> String.

이제 f를 다른 함수인 g와 함께 호출해보자. g에는 위에서 나열했던 각각의 함수를 넣어서 어떤 일이 나타나는지 관찰한다.

g : Greyhound -> Greyhound로 가정하면 f(g)는 타입 안전(type safe)한가?

아니다. 함수 f는 인자 g를 사용하면서 Dog의 다른 서브타입, 예를 들면 GermanShepherd를 사용해서 호출할 수도 있기 때문이다.

g : Greyhound -> Animal로 가정하면 f(g)는 타입 안전한가?

아니다. 1과 동일한 이유다.

g : Animal -> Animal로 가정하면 f(g)는 타입 안전한가?

아니다. f에서 인자 g를 호출하면서 개가 어떻게 짖는지 그 반환 값을 얻으려고 할 수 있다. 하지만 모든 Animal이 짖는 것은 아니다.

g : Animal -> Greyhound로 가정하면 f(g)는 타입 안전한가?

그렇다. 이 경우는 안전하다. f 함수는 인자인 g를 호출할 때 어떤 종류의 Dog든 사용할 수 있다. 모든 DogAnimal이기 때문이다. 또한 반환값은 Dog로 가정할 수 있는데 모든 GreyhoundDog이기 때문이다.

무슨 일이 일어나고 있나요?

결과적으로 다음 경우에 안전하다.

(Animal -> Greyhound) <: (Dog -> Dog)

반환 타입은 간단하다. GreyhoundDog의 서브타입이다. 하지만 인자 타입은 반대다. AnimalDog의 수퍼타입이다!

이 독특한 동작 방식을 적당한 용어를 사용해서 설명한다. 함수 타입에서 반환 타입은 공변적(covariant) 이고, 인자 타입은 반공변적(contravariant) 이다. 반환 타입의 공변성은 A <: B(T -> A) <: (T -> B)로 적용된다는 뜻이다. (A<:의 좌측에, B는 우측에 남아 있다.) 인자 타입의 반공변성은 A <: B(B -> T) <: (A -> T)로 적용된다는 의미다. (AB가 위치를 바꾸게 된다.)

재미있는 사실 타입스크립트에서는 인자 타입이 이변적(bivariant), 즉 공변성과 반공변성을 동시에 지닌다. 뭔가 말이 안되는거 같겠지만 말이다. (TypeScript 2.6부터 --strictFunctionTypes 또는 --strict를 사용하면 이 문제를 해결할 수 있다.) Eiffel은 인자 타입을 반공변적이 아닌 공변적으로 잘못 구현했다.

다른 타입은?

또 다른 질문이다. List<Dog>List<Animal>의 서브타입이 될 수 있을까?

답변하기 좀 미묘하다. 만약 목록이 불변이면 맞다고 답할 수 있다. 하지만 가변적이라면 당연히 안전하지 않다!

이유를 생각해보자. 나는 List<Animal>이 필요한데 List<Dog>를 넘겨줬다고 해보자. 나는 당연히 List<Animal>을 갖고 있다고 생각하고 Cat을 집어넣었다. 이제 List<Dog>Cat이 들어있게 된다! 타입 시스템은 이런 동작을 허용하지 않을 것이다.

불변 목록의 타입이라면 타입 파라미터가 공변적일 수 있지만 가변 목록의 타입은 반드시 공변적이지도, 반공변적이지도 않아야(invariant) 한다.

재미있는 사실 자바에서의 배열은 가변적이면서도 공변적이다. 물론 부적절하다.


추가로, 원문의 덧글 중에 시각적으로 잘 설명하는 자료가 있었다.

요즘 사무실에서 비는 시간이 좀 많이 있어서 책을 가져다두고 읽었다. 가볍게 읽으려고 읽었던 책을 가져가야지 했는데 지금 회사에서는 C#을 전혀 쓰지 않고 있으니 리마인드도 할 겸 읽게 되었다. 베타리딩을 포함해서 3번째 읽는데 그래도 또 배우는 게 많은 건 전에 열심히 안 읽어서 그런 걸까. 이번에는 읽고 기억하고 싶은 키워드라도 적어놔야지 싶어 표시해둔 부분을 여기에 옮겼다.

이 책은 어떤 방식으로 구조를 짜야 좋은 적응력을 가진 코드를 작성할 수 있는지 설명한다. 크게 세 부분으로 볼 수 있는데 가장 먼저 애자일 방법론과 적응형 코드를 작성하기 위한 배경적인 지식을 쌓는다. 그리고 SOLID 패턴에 대해 여러 예시를 들어 설명한 후, 마지막으로 실무에서 적용하는 예시로 마무리한다.

1부에서는 어떤 방식으로 작성하면 코드 의존성이 강해지는지, 어떤 계층적인 접근을 해야 유연한 구조를 구성할 수 있는지 풀어간다. 이런 흐름에서 왜 인터페이스를 도입해야 하고 디자인패턴이 어떻게 코드에 유연함을 더하는지 확인한다. 이런 구조의 코드를 어떻게 테스트하고 리팩토링하는 과정을 통해 개선하는 부분까지 다뤘다.

C#을 기준으로 설명하고 있어서 어셈블리를 어떻게 구성해야 한다거나 nuget을 구성하는 방법이라든가 하는 부분은 좀 거리감 있게 느낄 수도 있는데 요즘은 대다수 언어가 어떤 방식으로든 이런 부분을 지원하고 있으니 그렇게 맥락이 멀게 느껴지지 않았다.

의존성 관리(p. 66)에서 어떤 게 코드 스멜인지 알려주고 그 대안을 잘 설명하고 있다. C#에 매우 한정된 부분이긴 하지만 CLR의 어셈블리 해석 과정도 흥미로웠다.

여기서는 계층화를 점진적으로 진행하는 과정(p. 96)이 특히 좋았다. 인류 발달 과정 설명하듯 하나씩 짚어가며 어떤 이유에서 분리했는지 설명하고 있어서 최종 단계만 보면 막막할 수 있는 계층을 이해하는데 도움이 되었다.

2부에서는 SOLID 패턴을 하나씩 실질적인 예시와 함께 설명했다. 리스코프 치환 원칙을 설명하는 부분(p. 253~)이 재미있었다. 계약 규칙에서는 사전 조건과 사후 조건을 작성하는 방법과 불변성을 어떻게 유지해야 하는지 설명했다. System.Diagnostics.Contracts를 사용해서 각 조건을 기술하는 방식도 참 깔끔하다. 그리고 가변성 규칙에서 공변성과 반공변성을 짚고 넘어갔는데 제네릭이 어떤 식으로 동작하는지 이해하는데 많이 도움 됐다.

인터페이스 분리도 유익했는데 질의/명령을 인터페이스로 분리하는 부분도 좋았다. 의존성 주입은 이미 부지런히 쓰고 있었지만, 생명주기(p. 346)를 설명하는 부분은 다소 모호하게 생각했던 IDispose를 다시 살펴볼 수 있었고 이 인터페이스를 어떤 방식으로 생명주기 관리에 적용하는지 배울 수 있었다. 이 부분은 실무에서 좀 더 많은 사례를 접해보고 싶다.

예전에도 좋아서 추천 많이 했는데 또 읽어도 좋아서 추천하고 싶다. C#도 부지런히 해서 실무에서 다시 쓸 기회가 왔으면 좋겠다.

지금 있는 회사에서도 정말 오래된 php 페이지가 발굴되어 작업해야 하는 경우가 간혹 있다. 예전에는 그냥 MAMP 같은 패키지를 사용해도 큰 문제가 없었다. 이 회사에서는 기본적으로 포함되어 있지 않은 익스텐션을 사용하는 경우가 많아서 (ldap이라든지) 여기 온 이후로는 docker를 많이 사용하고 있다. 물론 배포 환경은 그대로라서 로컬에서만 주로 사용하고 있다. 배포까지 일괄적으로 사용하지 못하는게 좀 아쉽다.

라라벨과 같은 프레임워크를 사용하고 있으면 이미 공개된 docker도 많고 튜토리얼도 찾기 쉽다. 대신 예전 방식으로 작성된 코드를 기준으로 설명하는건 별로 못본 것 같아서 간단하게 정리하려고 한다. 이 글에서는 용어 없이 슥슥 넘어가는 부분이 많다. 또한 실제로 배포 환경까지 사용하지 않고 로컬에서만 사용하더라도 편리한 점이 많다. 그래서 단순히 로컬에서 php 프로젝트를 돌린다는 것 자체에 한정했다. 이 글에서는 docker-compose로 php-apache, mysql 스택을 빠르게 구성하는 방법 을 살펴본다.

도커에 대해 자세히 알고 싶다면 다음 글을 보자.

이 글에서는 다음 이미지를 사용하고 있다.

여기서 사용한 모든 코드는 haruair/docker-simple-php 리포지터리에서 확인할 수 있다.

docker 설치하기

먼저 docker를 받아 설치한다.

환경을 파일로 작성하기

이제 구축할 환경을 파일로 먼저 만든다. 다음 내용에 따라서 docker-compose.ymlDockerfile을 작성한다.

docker-compose.yml 작성하기

docker-compose.yml은 스마트폰을 예로 들면 어떤 앱을 설치하고 앱을 어떻게 설정할지 정리한 파일이다. 1 docker compose라는 도구가 이 파일을 읽어서 앱을 설치하게 된다.

먼저 다음과 같이 작성한다.

version: '3'

services:

services: 아래로는 설치할 이미지를 작성한다.

#...

services:
  db:
    image: mariadb:5.5
    volumes:
      - "./data:/var/lib/mysql:rw"
    environment:
      - "MYSQL_DATABASE=hello"
      - "MYSQL_USER=hello"
      - "MYSQL_PASSWORD=hello"
      - "MYSQL_ROOT_PASSWORD=root"
    ports:
      - "3306:3306"

먼저 db를 추가했다. image를 보면 mariadb의 공식 이미지를 사용했음을 알 수 있다. 이런 이미지는 docker hub를 통해 공유되는데 앱스토어 정도로 생각하면 되겠다. volumes를 사용해서 앱의 경로에 현재 폴더의 ./data를 연결했다. mysql에 생성되는 데이터베이스는 이 폴더에 저장된다.

대부분의 도커 앱은 environment를 통해서 설정을 할 수 있는데 여기서는 데이터베이스명과 사용자, 비밀번호와 루트 비밀번호를 설정한 것을 볼 수 있다. ports는 현재 컴퓨터의 포트와 해당 앱의 포트를 연결하는 설정이다. 즉, 로컬 호스트의 3306 포트로 접속하면 해당 앱의 3306 포트로 접속하는 것과 동일하게 된다.

#...
services:
  #...
  phpmyadmin:
    image: phpmyadmin/phpmyadmin:4.7
    depends_on:
      - db
    ports:
      - "8000:80"
    environment:
      - "PMA_HOST=db"
      - "PMA_USER=root"
      - "PMA_PASSWORD=root"

다음으로 phpmyadmin을 추가했다. phpmyadmin을 사용하지 않는다면 이 부분은 건너뛴다. mysqlworkbench 등으로 접속해도 사용해서 된다. 나는 웹에서 쓸 수 있는 도구를 선호하는 편이라서 phpmyadmin을 개발 환경에 넣어놓는 편이다.

앞서 db와 크게 차이는 없다. depends_on으로 앞에서 추가한 db에 의존하고 있다고 명시했다. 환경변수에 PWA_HOST를 db라고 설정했는데 docker의 앱은 서로 지정된 이름으로 호스트를 참조할 수 있기 때문이다. 사용자명과 비밀번호는 위에서 설정한 대로 설정해서 접근할 수 있도록 했다.

#...
services:
  #...
  web:
    build:
      context: .
      dockerfile: ./Dockerfile
    volumes:
      - ".:/var/www/html"
    depends_on:
      - db
    ports:
      - "80:80"

마지막으로 web을 추가했다. 앞서 추가했던 이미지와 다르게 build가 포함되어 있다. buildDockerfile을 참고해서 이미지를 생성한다. 대부분 도커 앱은 환경변수로도 제어할 수 있지만 앱 안에 확장 기능을 설치하거나 하는 동작도 가능하도록 Dockerfile을 사용할 수 있다. 자세한 내용은 아래서 다룬다. context는 이미지 생성에서 사용할 기본 경로를 지정하는데 사용하며 다음 내용에서 설명한다. Dockerfile의 명칭이 다르거나 경로가 다르면 dockerfile을 통해 지정할 수 있다. volumes에서 현재 프로젝트의 경로를 앱 내 웹서버의 기본 경로에 연결한다. ports에 보면 앱의 80 포트를 로컬의 80포트와 연결했다. http://localhost/를 입력하면 앱에 있는 서버로 연결되게 된다.

작성을 마친 docker-compose.yml 파일은 다음과 같다. php앱을 구동하기 위해서 필요한 환경을 한 위치에 모두 작성했다.

version: '3'

services:
  db:
    image: mariadb:5.5
    volumes:
      - "./data:/var/lib/mysql:rw"
    environment:
      - "MYSQL_DATABASE=hello"
      - "MYSQL_USER=hello"
      - "MYSQL_PASSWORD=hello"
      - "MYSQL_ROOT_PASSWORD=root"
    ports:
      - "3306:3306"
  phpmyadmin:
    image: phpmyadmin/phpmyadmin:4.7
    depends_on:
      - db
    ports:
      - "8000:80"
    environment:
      - "PMA_HOST=db"
      - "PMA_USER=root"
      - "PMA_PASSWORD=root"
  web:
    build:
      context: .
      dockerfile: ./Dockerfile
    volumes:
      - ".:/var/www/html"
    depends_on:
      - db
    ports:
      - "80:80"

Dockerfile 작성하기

앞서 설정에서 작성했던 내용대로 web에 사용하기로 한 Dockerfile을 작성해야 한다. 여기서는 php의 공식 이미지를 기반으로 사용한다. 파일에 다음 내용을 추가하자.

FROM php:5.6-apache

php:5.6-apache를 기반 이미지로 사용했다. Docker hub에 php:5.6-apache로 지정되어 있는 Dockerfile의 내용을 그대로 상속하게 된다. 이제 필요한 패키지와 확장을 설치한다. Dockerfile로 이미지를 생성하면 매 명령마다 중간 이미지를 생성하기 때문에 필수적인 패키지와 확장을 우선순위로 두면 나중에 비슷한 이미지를 생성할 때 더 빠르게 동작한다.

# ...
RUN apt-get update
RUN apt-get install -y git zip

RUN apt-get install -y libpng12-dev libjpeg-dev
RUN apt-get install -y mysql-client
RUN docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr \
&& docker-php-ext-install gd

RUN docker-php-ext-install mbstring
RUN docker-php-ext-install mysqli
RUN docker-php-ext-install pdo
RUN docker-php-ext-install pdo_mysql
RUN docker-php-ext-install opcache

RUN apt-get install -y libssl-dev openssl
RUN docker-php-ext-install phar

RUN apt-get clean \
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

이제 apache의 모드를 활성화하고 웹서버에 반영하는 부분을 추가한다.

# ...
RUN a2enmod rewrite
RUN a2enmod headers
RUN apache2ctl -k graceful

여기서 작성한 Dockerfile의 전체 내용은 다음과 같다.

FROM php:5.6-apache

RUN apt-get update
RUN apt-get install -y git zip

RUN apt-get install -y libpng12-dev libjpeg-dev
RUN apt-get install -y mysql-client
RUN docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr \
&& docker-php-ext-install gd

RUN docker-php-ext-install mbstring
RUN docker-php-ext-install mysqli
RUN docker-php-ext-install pdo
RUN docker-php-ext-install pdo_mysql
RUN docker-php-ext-install opcache

RUN apt-get install -y libssl-dev openssl
RUN docker-php-ext-install phar

RUN apt-get clean \
  && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

RUN a2enmod rewrite
RUN a2enmod headers
RUN apache2ctl -k graceful

docker 실행하기 및 종료하기

docker-compose up을 실행한다. 처음 실행하면 이미지를 받고 빌드를 시작하는 것을 확인할 수 있다.

$ docker-compose up
Creating network "dockerhello_default" with the default driver
Pulling db (mariadb:5.5)...
5.5: Pulling from library/mariadb
4269eaa217cc: Downloading [=========>      ]  14.12MB/38.11MB
b5d5817a79f8: Download complete
5a270f0327f3: Download complete
911f94a14d77: Download complete
114588764b3b: Downloading [=============>  ]   5.12MB/5.994MB
d1dcaee5ec4a: Download complete

모든 빌드가 완료되면 현재 폴더에 있는 내용을 http://localhost/를 통해 접속할 수 있다.

앞서 phpmyadmin도 포함했고 포트에 연결했었다. 이제 http://localhost:8000/로 접속하면 phpmyadmin도 잘 실행되고 있는 것을 확인할 수 있다.

docker-compose down으로 종료한다.

docker 컨테이너 다루기

docker-compose로 실행한 후에 실행된 컨테이너를 보기 위해서는 docker ps 명령을 사용할 수 있다.

$ docker ps
CONTAINER ID        IMAGE                       COMMAND                  CREATED             STATUS              PORTS                            NAMES
bacdc4de6660        dockerhello_web             "docker-php-entrypoi…"   6 minutes ago       Up 5 minutes        0.0.0.0:80->80/tcp               dockerhello_web_1
408909957ec4        phpmyadmin/phpmyadmin:4.7   "/run.sh phpmyadmin"     6 minutes ago       Up 5 minutes        9000/tcp, 0.0.0.0:8000->80/tcp   dockerhello_phpmyadmin_1
4949e0a2d10f        mariadb:5.5                 "docker-entrypoint.s…"   6 minutes ago       Up 5 minutes        0.0.0.0:3306->3306/tcp           dockerhello_db_1

그 외에도 다양한 docker 명령어를 직접 사용해서 컨테이너를 제어할 수 있다. 다음 명령으로 컨테이너에 쉘을 실행하고 접속할 수 있다.

$ docker exec -it dockerhello_web_1 bash
root@bacdc4de6660:/var/www/html# 

다만 이렇게 접속해서 설정을 변경한다면 지금 당장은 문제가 없겠지만 다시 이미지를 생성했을 때는 그 변경 부분이 새 이미지에서는 적용되지 않는다. 도커파일을 사용하면 그런 반복되는 과정을 없에고 파일에 명시적으로 남기는 것으로 매번 동일한 환경을 구성할 수 있다. 설정을 추가하거나 변경할 필요가 있다면 그 명령어는 Dockerfile에 정의하자.


docker-compose를 처음 사용하고 난 후에 docker로 환경을 구성한다는 것이 더 와닿아서 일반적인 설명과는 조금 다른 순서로 적어봤다. 시작부터 대뜸 복잡하게 느껴졌다면 (죄송하지만) 위에 추천한 글을 읽어보자.

여기서는 웹서버가 포함된 php를 사용했지만 fpm을 사용하는 php로 분리하고 별도의 웹서버 컨테이너를 사용하는 것도 가능하다. 반대로 한 이미지에 lamp 스택을 모두 담고 있는 linode/lamp와 같은 경우도 존재한다. 개인적으로는 역할별로 컨테이너를 분리하기를 선호하는데 상황에 맞게 전략을 짜야겠다.

  • 이런 역할을 오케스트레이션(orchestration)이라고 한다. 
  • 웹사이트 설정

    웹페이지 색상을 선택하세요

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