Home

2019년 개발자 회고 (1)

2019-12-17

개요

2019년 한 해 동안, 나는 개발자로서 정말 빠르게 성장했다. 빠르게 성장할 수 있었던 이유는 내가 겪었던 모든 경험이 새로웠기 때문이다. 올해는 내가 개발을 업으로 삼아 온전히 보낸 첫 해였다. 학교에서 하던 개발이 필요로 하던 역량과 실제 판매되는 제품을 만들기 위한 개발이 필요로 하는 역량 사이에는 엄청난 갭이 있었다. 덕분에 나는 방금 세상에 나온 갓난아이가 세상의 지식을 빨아들이듯 프로 개발자로서 갖춰야 할 역량을 흡수할 수 있었다.

올해의 개발자 회고는 글이 길어져 두 편으로 나누었다. 첫 편에서는 이번 1년 동안 내가 어떤 성장을 이루었는지 되돌아보았다. 이어지는 편에서는 개발자로서 했던 활동, 아쉬웠던 점, 그리고 내년 목표를 정리하면서 성장에 대한 의지를 다지려고 한다.

계단식 성장

많은 개발 지식을 머리 속에 넣고 있다고 해서 반드시 개발을 잘하는 것은 아니다. 내가 생각하는 개발 실력은, 더 많은 선택의 순간을 포착하고 더 많은 선택지를 볼 수 있는 시야와, 그 상황에서의 최선의 선택지를 고를 수 있는 판단력이다. 경험과 고민이 빠진 단순한 지식은 어떤 선택지가 위험한지, 어떤 선택지가 더 좋은지 가치판단에 전혀 영향을 주지 못한다. 그 지식에 익숙해지고, 지식을 어떻게 활용하는지 알아야 비로소 지식은 실력이 된다.

보통 실력은 계단식으로 점프한다. 끊임없이 지식과 경험을 축적하다 보면, 어쩌다 우연히 결정적으로 부족했던 것을 깨닫는 순간이 온다. 그 깨달음의 순간에 그 사람은 한 계단 더 올라가고, 시야가 넓어지며, 실력이 비약적으로 증가한다. 물론 도약 이후에 그 깨달음을 온전히 자기 것으로 만들고 습관처럼 배어 나오게 하려는 노력은 필수적이다.

열심히 노력한 것과 운이 겹쳐, 나는 올해 총 4번의 도약을 할 수 있었다.

세상은 아주 더럽고, 소프트웨어는 유연해야 한다

세상이 회사에 거는 제약조건, 그리고 회사가 제품에 기대하는 요구사항은 종종 개발자가 제품을 개발하는 속도보다 훨씬 빠르게 변한다. 급격히 성장하는 스타트업이라면 더욱 그럴 가능성이 높다. 또한 타다 서비스는 여러모로 이슈가 끊이지 않은 서비스이기 때문에, 요구사항이 정말 빠르게 변했다. 상황이 이렇다 보니 개발 중간에 기획이 엎어지는 일이 비일비재했고, 그럴 때마다 나는 f**k을 외치며 지금까지 짠 코드를 버리고 설계를 다시 해야만 했다. 이미 배포한 시스템에 대해서도 새롭게 필요한 기능은 나날이 추가됐는데, 그중 지금의 설계로는 수용할 수 없는 기능도 있었다. 그러면 나는 어김없이 머리를 쥐어짜며 코드를 갈아엎어야 했다.

이런 상황은 내게 큰 스트레스였지만, 아이러니하게도 이런 압박감이 내가 성장할 수 있는 가장 큰 원동력이 되었다. 반년 가까이 기획이 갈아엎어지는 경험을 한 후에야 나는 요구사항과 기획이 언제나 바뀔 수 있는 것임을 이해하게 됐다. 이는 기획을 신뢰하지 않는 것과는 다르다. 현재 나온 기획은 지금의 요구사항을 해결하기 위한 최선이다. 다만, 나중에 요구사항이 변할 수 있고, 이에 따라 새로운 요구사항을 해결하기 위해 기획이 변경될 수 있다는 사실을 인지하고 있는 것이다.

그래서 소프트웨어는 기획의 변경에 쉽게 대응할 수 있도록 유연하게 만들어야 한다. 개발자는 기획이 어떤 식으로 변경될 수 있을지 어느 정도 고려해야 한다. “어느 정도”라는 표현을 사용한 이유는 너무 과하게 예측하고 일반화를 하면 나중에 예측한 것과는 다른 방향으로 기획이 변경되었을 때 시스템을 변경하기 힘들어지기 때문이다. <클린 아키텍처>에서 나온 표현을 빌리자면, 지금 안 해도 되는 선택을 나중으로 미루어서 최대한 선택지를 많이 남겨 놓도록 설계해야 한다.

그렇기 때문에 문제를 해결할 수 있는 가장 좋은 방법은 개발을 하지 않고 해결하는 방법이다. 그다음으로 좋은 것은 최소한의 개발만으로 해결하는 방법이다. 개발하지 않으면 모든 선택지가 남아있는 것과 같다. 또한, 앞서 말했듯이 개발자가 제품을 만드는 속도보다 제품에 대한 요구사항이 변하는 속도가 훨씬 빠르기 때문에, 모든 문제를 개발로 해결하는 것은 현실적으로 불가능하다. 따라서 개발자가 어떤 문제를 당면했을 때 가장 먼저 해야 하는 일은, 개발 없이 문제를 해결하는 방법을 고민하는 것이다. 그게 안 된다면 개발을 최대한 조금 해야 한다. 이미 만들어진 코드를 바꾸는 것은 새로 구현하는 것보다 훨씬 힘들다.

이런 맥락에서, 개발자가 기획에 참여하는 것은 대단히 중요하다. 기획자는 개발을 잘 모르기 때문에 개발자가 무엇을 할 수 있을지 역시 잘 모른다. 그래서 기획자는 해결하고자 하는 문제는 명확하게 정의할 수 있지만, 종종 그 문제를 해결하기 위해 제시한 기획은 개발자가 생각하는 방법과 동떨어져 있다. 개발자가 아예 할 수 없는 일을 요구하기도 하고, 더 쉬운 방법이 있는데도 굳이 돌아가는 방법을 제시할 때도 있다. 이는 어쩔 수 없는 일이다. 따라서 좋은 기획을 하기 위해, 개발자는 기획 단계에서부터 내가 무엇을 할 수 있고 무엇을 할 수 없는지를 기획자에게 잘 어필하고, 기획자가 제시한 문제를 풀기 위한 개발자 입장에서의 최선을 적극적으로 제시해야 한다.

좋은 코드에 대한 가치관

좋은 코드를 작성하는 것은 아주 중요하다. 좋은 코드는 다른 사람의 생산성을 올려주고, 개발의 비용을 비약적으로 낮춘다. 좋은 코드를 짤 수 있는 개발자 1명의 아웃풋은 개발 팀원의 숫자만큼 스케일 아웃 한다.

올해 로버트 마틴의 여러 책을 읽으면서 나는 좋은 코드에 대한 나만의 기준을 갖춰나가기 시작했다. 내가 생각하는 좋은 코드는 1. 다른 사람이 2. 읽기 편하고 3. 고치기 쉬운 코드이다. 클린 아키텍처에서 하는 이야기와 거의 동일하고, 표현만 내 방식대로 바꾼 것이다.

코드는 다른 사람을 위해 작성해야 한다. 내가 영원히 이 회사에 다닐 것이라는 보장은 없다. 설사 이 회사에 죽을 때까지 다닌다고 해도, 내가 휴가를 가서 부재중에 내가 짠 코드에서 버그가 터질 수도 있다. 혹은 내가 너무 바빠서 내가 하던 작업의 일부분을 다른 사람에게 넘겨줘야 하는 상황도 있다. 그렇기 때문에 코드는 정상적으로 돌아가는 것도 중요하지만, 무엇보다 다른 사람들을 위해 작성되는 것이 중요하다.

읽기 쉬운 코드를 짜기 위한 첫걸음은 명확한 변수와 함수, 클래스 이름을 사용하는 것이다. 이름은 곧 그 변수/함수/클래스가 가지는 의미, 혹은 책임을 나타낸다. 함수/클래스가 너무 비대해지면 너무 많은 책임을 가지므로 이름을 짓기 어려워진다. 따라서 함수/클래스를 한 가지의 명확한 책임만 가지도록 적절히 쪼개야 한다. 또한, 같은 함수 안에서는 추상화 수준이 비슷한 함수를 호출해야 한다. 추상화 수준을 맞춰서 함수를 작성하면 함수를 점점 쪼개게 된다. 함수 개수는 많아지지만, 함수 하나하나는 마치 소설책을 읽듯이 부드럽게 읽히는 함수가 된다. 다른 사람은 그 사람이 알아야 하는 추상화 수준까지의 함수만 타고 들어가서 읽으면 된다.

코드를 고치기 쉬우려면 고칠 부분을 명확히 파악할 수 있어야 한다. 이는 함수와 클래스의 책임을 명확히 분리하고, 인터페이스를 통해 구현을 은닉함으로써 달성할 수 있다. 또한, 코드를 고칠 때는 “이 부분을 고치면 연쇄적으로 영향을 받는 부분이 있지 않을까?”와 같은 불안한 마음이 들면 안 된다. 이를 위해서는 각 컴포넌트가 정제된, 최소한의 의존성을 가지게 해 변경에 영향을 받는 부분을 최소화해야 한다. 한편, 실제로 영향을 받은 부분이 없다는 사실을 보장해주는 것은 잘 짜여진 테스트이다. 꼼꼼히 테스팅 되는 코드는 변경에 대한 두려움을 없애준다.

이러한 좋은 코드에 대한 가치관은 코드를 작성할 때 마주치는 수많은 선택의 순간에서 가치판단의 기준이 된다. 1년 전의 나는 이런 가치관이 거의 없었다. 그래서 함수를 쪼개냐 마냐, 클래스를 새로 만드냐 마냐와 같이 사소해 보이면서도 중요한 선택지를 만났을 때, 고민은 너무 과하게 하고 선택은 명확한 이유 없이 해야만 했다. 지금은 비록 잘못된 선택일지라도 적당히 고민하고, 명확한 이유와 함께 확신을 가지고 선택한다.

자신이 짠 코드에 대한 이유와 확신이 필요한 이유는, 좋은 코드에 대한 더 좋은 가치관을 형성해나가기 위한 시작점이 되기 때문이다. 코드를 작성한 본인만의 이유를 가지고 있어야 타인의 가치관을 선택적으로 수용함으로써 점점 좋은 가치관을 만들어나갈 수 있다. 다른 사람이 내 코드에 대한 리뷰를 달았을 때, “아 그런가 보다”가 아니라 “나는 이런 이유로 이렇게 짰는데, 저분이 제시하신 코드가 더 좋은 것 같네”가 되어야 다음에 더 좋은 코드를 작성할 수 있을 것이다.

모델 설계에 대한 감각

서버팀 팀장님께서 항상 하시는 말씀 중 하나는, DB는 논리적인 상태를 표현하는 것이고, 쉽게 바뀔 수 있는 로직은 코드로 처리해야 한다는 것이다. 올해 나는 정말 운이 좋게도 수많은 설계 작업을 해볼 수 있었고, 그 과정에서 위 말의 의미를 점차 깨닫게 됐다.

컴퓨터는 튜링 머신이다. 즉, 어플리케이션의 모든 로직은 결국 “A라는 상태일 때 Z를 하시오”의 집합으로 표현할 수 있다. 이때 현재 상태가 A인지 B인지는 어떻게 판단할 수 있을까? 바로 DB를 통해서다. 우리가 설계한 모델의 상태는 DB에 적혀있는 데이터가 결정한다. 그리고 이렇게 결정된 모델의 상태를 바탕으로 어플리케이션의 로직이 실행된다. 즉, DB에 저장된 데이터에 의해 어플리케이션 로직이 좌우되는 것이다.

그래서 DB는 아주 중요한 리소스이고, 그런 DB를 어떻게 사용할지를 결정하는 모델과 DB 스키마 설계는 더욱 중요하다. 모델과 DB 스키마를 잘못 설계하면 기획을 올바르게 커버할 수 없다. 모델이 표현할 수 있는 상태와 기획서가 필요로 하는 상태가 다르다면 아무리 코드를 잘 짜더라도 누락되는 시나리오가 존재할 수밖에 없다. 따라서 기획에서 필요한 모든 상태를 표현할 수 있도록 모델과 DB 스키마를 설계는 것은 당연한 일이다.

필요한 상태를 모두 표현할 수 있는지보다 더욱 중요한 것은, 모델이 얼마나 “자연스럽게” 상태를 표현할 수 있냐는 것이다. 단순하게 생각하면, boolean field가 10개만 있으면 우리는 1024개의 서로 다른 상태를 표현할 수 있다. 1024개의 다른 시나리오를 커버할 수 있는 모델을 만들 수 있는 것이다. 하지만 그런 모델은 아무도 설계하지 않는다. 왜냐하면 코드를 보는 사람이 각 상태가 어떤 의미를 가지는지 전혀 알 수 없기 때문이다. 모델은 각 필드의 의미가 분명하고 필드의 조합이 만들어내는 상태가 자연스럽게 기획을 녹여낼 수 있도록 설계되어야 한다.

또 하나 중요한 것은, 기획의 변경에 취약하지 않도록 모델과 DB 스키마를 설계하는 것이다. 어플리케이션의 모든 것은 DB에 대해 의존성을 가지고, 반면 DB는 어떤 것에도 의존하지 않는다. 즉, DB 스키마가 바뀌면 관련된 모든 코드가 영향을 받는다. 따라서 DB는 한 번 배포하면 가장 변경하기 어려운, 사실상 변경이 불가능한 컴포넌트이다. 그래서 필요한 경우(성능 등)가 아닌 이상 상대적으로 쉽게 바뀌는 로직, 혹은 “정책”을 DB로 처리하려고 해서는 안 된다. 모델은 반드시 정책이 어떻게 변하더라도 관련 도메인이 남아있는 한 항상 존재할 상태들을 표현해야 한다.

기획을 바탕으로 모델과 DB 스키마를 설계한 이후에는 모델의 상태를 MECE(Mutually Exclusive Collectively Exhaustive)하게 구분해보는 것이 큰 도움이 된다. 모델의 상태를 MECE하게 나눠보면 모델이 어떤 상태로 존재할 수 있고 어떤 상태에 존재할 수 없는지, 각 상태에서 다른 상태로 언제 전이하는지를 쉽게 정리할 수 있다. 이를 한데 모으면 모델에 대한 state machine이 된다. 이러한 state machine은 생각의 정리를 돕는 것은 물론이고, 모델이 기획을 커버하는지 쉽게 확인할 수 있도록 도와준다.

모델의 state machine을 그려보면, 모델이 기획을 잘 녹여내는지 검증하는 것 외에도 많은 이득을 얻을 수 있다. 우선 기획을 바탕으로 모델을 설계한 것의 역으로, state machine 상 가능하지만 기획에서 고려되지 않은 상태 및 전이를 쉽게 발견할 수 있다. 이는 기획을 더욱 탄탄하게 만드는 데에 무척 큰 도움이 된다. 로직을 짤 때는 미처 처리하지 않은 케이스(혹은 상태)가 있는지 검토할 수 있다. 디버깅을 할 때는 해당 버그가 발생할 수 있는 상태를 구분하고, 그 상태에 진입하기 위해 가능한 코드 플로우를 찾아내는 데 큰 도움을 준다.

서버 개발자는 운영을 해봐야 한다

어디서인가(아마 페이스북일 것이다) 이런 뉘앙스의 글을 본 적이 있다. “운영 안 해본 서버 개발자는 믿지 않는다.” 나는 올해 이 문장의 의미를 제대로 느꼈다.

어플리케이션 개발과 서버 운영은 서로 밀접한 연관을 가지고 있다. 예를 들어, 마이크로서비스 아키텍처는 DevOps 팀의 노력만으로는 절대 이루어질 수 없다. 로직 역시 마이크로서비스 아키텍처에 적합하게 짜여야 한다. 이벤트 큐를 활용한 비동기식 eventual consistency를 이루려면 이벤트를 받아 처리하는 로직이 멱등적으로 짜여야 한다. 응답 속도가 중요한 API의 경우 이벤트 큐를 사용하는 대신 서비스 간 gRPC 통신을 해야 할 것이고, 코드를 이에 맞춰 작성해야 할 것이다.

타다 서버에서 운영적인 아키텍처가 서버 로직에 영향을 주는 예시를 들어보겠다. 우리는 k8s 클러스터 위에 서버를 띄우고 있고, 배포할 때는 helm을 사용한다. 이때 helm은 rollout deployment를 하기 때문에, 서버를 띄울 때 기존 버전의 서버 pod과 새 버전의 서버 pod이 동시에 공존하는 시간이 존재한다. 이때 기존 버전의 서버와 새 버전의 서버가 데이터를 적재하는 로직이 다르면 데이터가 심각하게 꼬일 것이다. 따라서 새 버전의 서버는 DB 조작에 있어서 하위호환성을 반드시 지켜야 한다.

또 다른 예시는 클라이언트와 gRPC 연결을 맺고 있는 서버를 분리한 것이다. 우리 서버는 클라이언트와 gRPC 연결을 맺고 있다. 유저가 차량을 호출하면 근처의 차량과 매칭시켜주는데, 이때 매칭되었다는 사실을 gRPC 연결을 통해 클라이언트에 전파한다. 이런 상황에서 발생했던 문제 중 하나는, 클라이언트와 gRPC 연결을 맺고 있는 서버가 배포되면 서버가 새로 뜨면서 모든 gRPC 연결이 끊어졌다 다시 맺어진다는 점이었다. 이러면 클라이언트에서는 잠깐 차량이 매칭되었다는 정보를 전달받지 못하게 된다. 이 문제를 해결하기 위해 우리는 클라이언트에 내려줄 데이터를 serialize 해서 서빙하는 서버와, 클라이언트와 gRPC 연결을 맺고 이미 serialize 된 데이터를 내려주기만 하는 서버를 분리했다. 아키텍처를 이렇게 설계하면 내려줄 데이터가 변경되더라도 gRPC 연결을 맺는 서버는 배포하지 않아도 되고, 클라이언트와의 gRPC 연결이 끊기지 않는다. 이런 아키텍처 변경에 대응하여 우리는 꽤 많은 서버 코드를 고쳐야 했다.

이렇듯, 운영은 어플리케이션 로직에 적지 않은 영향을 미치며, 때로는 어플리케이션에 존재하는 문제를 해결할 수 있는 키포인트가 되기도 한다. 따라서 어플리케이션 개발자는 운영에 대한 지속적인 관심을 기울여야 한다. 가장 좋은 것은 직접 개발과 운영을 같이 해보는 것이다. 하지만 이런 경험을 쌓기는 정말 작은 스타트업이 아닌 이상 쉽지 않다. 나는 시니어의 리뷰를 통해 운영과 이를 간접적으로 배워나갔다. 운영이 서버 로직에 주는 영향을 간과하고 짠 코드를 리뷰를 통해 수차례 지적받은 후에야 운영과 서버 로직이 밀접한 연관이 있다는 것을 깨달았다.

리뷰할 때 보는 요소

내가 “올해 정말 빠르게 성장했구나” 하고 가장 크게 느낀 순간은, 얼마 전 새로 들어온 신입 분들의 코드를 리뷰할 때였다. 그분들은 놀랍도록 스마트하고, 내 1년 전과 비교도 안 되게 좋은 코드를 작성했다. 그렇지만 그들의 코드는 아직 유저들이 돈을 내고 쓰는 제품에 적합한 코드는 아니었다. 학생 때와 지금 개발의 가장 큰 차이점은 만들어야 하는 제품의 완성도이다. 완성도를 보장하기 위해 우리는 로직 개발 외에 수많은 중요한 요소들을 고려해야 한다. 내가 1년 동안 느낀, 완성도를 위해 필요한 요소에는 아래와 같은 것들이 있다.

  • 적절한 테스트가 짜여 있는가
  • 테스트를 잘 짜기 위한 코드 구조가 갖춰져 있는가
  • 테스트 서버에서 QA를 어떻게 할 것인가
  • 쿼리를 얼마나 효율적으로 날리는가(index를 타는지)
  • 트랜잭션의 isolation level이 적합하게 설정되어 있는가
  • Hibernate랑 같이 사용할 때 발생하는 여러 가지 문제를 방지하는가(lost update, LazyInitializationException, N+1 쿼리 문제 등)
  • Spring Reactor를 사용하는 경우, 쓰레드가 바뀌는 것과 관련하여 문제가 없는가(각종 context 전파, transaction / entityManager 범위, 쓰레드 점유 등)
  • 에러 처리가 적절히 되어있는가
  • 함수명/변수명이 적절한가
  • 코드가 적절한 추상화 수준으로 나뉘어 있는가 & 코드가 적절한 책임을 가진 컴포넌트 있는가
  • 함수의 시그니처가 적절한가
  • 기존 코드를 변경한 경우 로직의 semantic이 변경되는 부분이 없는가
  • 기존 클라이언트를 위해 API의 하위호환성이 유지되는가
  • 이 PR 버전의 서버와 기존 서버가 함께 떠 있을 때 문제가 없는가(rollout deployment)
  • 지금의 기획을 모두 올바르게 커버하는가
  • 앞으로 기획이 변경될 것 같은 부분에 대해 유연하게 설계했는가
  • 기획이 놓치고 있는 부분은 없는가

내가 이 모든 사항을 완벽하게 다루고 있다는 말은 아니다. 몇몇 요소는 종종 까먹기도 하고, 몇몇 요소는 충분히 인지하고 있지만 어떻게 해결해야 할지 모르는 상황이 비일비재하다. 하지만 이런 사항을 아예 모르고 작성한 코드와, 이런 요소에 대해 고민하면서 작성한 코드의 퀄리티는 차원이 다르다. 이런 요소에 대해 고민을 계속하다 보면, 결국 언젠가는 숨 쉬듯 자연스럽게 위 요소들을 고려하게 될 것이다.

(다음 글에서 계속)