Simple Design

드라이퍼스 모델에 따르면, 전문가는 직관으로 일하지만 초보자는 규칙이 있어야 성과를 낼 수 있다. 나는 내 경력의 대부분의 기간 동안 전문성이 높은 시니어 프로그래머들과 함께 일해왔기 때문에, 코드 품질에 대해서도 각자의 주관을 충분히 발휘할 수 있게 하되, 충돌이 날 때는 서로 토론해서 맞춰가는 식으로 해결해왔고, 이 방식은 꽤 잘 동작했다. 하지만, 프리랜서를 다수 고용해야 하는 외주 프로젝트들, 그리고 큰 팀을 빌딩해야 하는 상황 등을 거치면서 다양한 전문성 레벨의 팀원들과 함께 일할 때도 코드 품질을 잘 다룰 수 있는 방법이 필요하다고 느꼈다. 그래서 XP 커뮤니티의 코드 품질에 대한 글들, 내 생각, 몇 가지 책들을 조합해서 원칙을 정리해보기로 했다.

코드 품질에 대한 느낌적 느낌

다양한 전문성 레벨의 프로그래머들과 일하면서 느낀 점 중 하나는, 전문가들은 직관으로 코딩해도 좋은 코드가 나오는 경우가 많고 직관이 나쁜 냄새를 잘 짚어내지만 그럼에도 불구하고 자신의 설계를 끊임 없이 의심하고 논거를 찾으려 하고 더 나은 방법을 생각해본다는 것이다. 반대로 초보자들은 직관이 아직 부족한데도 오히려 명확한 논거 없이 자신의 주관으로 설계를 한다. 그래서 코드 품질을 개선했다고 주장하는데 왜 이렇게 했냐고 물어보면 객관적인 근거를 이야기하기보다 그냥 이게 더 좋은 구조 같아서, 이게 내 멘탈 모델에 더 잘 맞아서, 더 깔끔해보여서, 가독성이 좋은 것 같아서 같은 느낌적 느낌을 이야기한다. 근데, 이게 또 느낌이다보니 사람마다 달라서 다른 사람의 느낌과 충돌하는 경우도 흔하다.

코드 품질을 표현하는 말 중에 가독성이라는 표현이 해로운 방향으로 동작하는 경우도 많았다. 그냥 자기 눈에 잘 안 들어오면 코드 읽기 어렵다라고 표현하는 경우가 많았다. 재미있는 점은 다른 사람의 코드를 읽기 어렵다는 불평을 많이 하는 사람들의 코드가 대체로 그런 불평의 대상이 되는 경우도 많았다는 것. 아무래도 가독성이라는 말, 코드가 읽기 쉽다 어렵다는 말 자체가 코드를 주관적으로 평가하게 만들고 또 주관적으로 코드를 작성하게 하다보니 사람마다 그 느낌이 일치하지 않는 문제가 생길 수 밖에 없는 것 같다.

클린 코드도 좀 비슷하다. 다만, 이건 책이 있다보니 어느 정도 고유 명사처럼 기능하기도 하면서 기준을 제시하기도 하는데, 나는 클린 코드 책의 기준들은 절반 정도만 동의하는 편이라 이 기준을 가져다 쓰고 싶지는 않았다. 마찬가지로 코드를 "깔끔하다", "지저분하다" 등으로 표현하는 것도 코드 품질을 높이는데 오히려 방해가 되는 표현들이라고 본다.

이런 표현들의 또 다른 문제 중 하나는 본질적 복잡성의 문제를 간과하게 만든다는 것이다. 프로그래밍의 복잡성은 두 가지, 본질적 복잡성과 우발적 복잡성이 있다. 우발적 복잡성은 개발 도구, 프로그래밍 언어, 코드 품질 등으로 해결할 수 있는 것이지만, 본질적 복잡성은 소프트웨어로 해결하려는 문제 자체에 담긴 복잡성이라 코드로 해결할 수가 없다. 그런데, "가독성" 관점으로 이런 코드를 바라보게 되면 본질적 복잡성을 우발적 복잡성으로 오해해서 잘못된 해결책을 시도하게 된다.

주관을 배제한 코드 품질 기준

그래서, 나는 코드 품질에 대해서 주관을 배제해보면 어떨까 하는 생각이 들었다. 다만 한 가지 우려가 있었다. 초보자들은 규칙에 따라 일하면 성과를 낼 수 있지만, 전문가들은 그 규칙을 넘나들면서 직관에 따라 일하기 때문에 지나치게 객관적인 규칙을 제시하면 전문가들의 생산성을 깎아먹지 않을까 하는 것이다. 나는 전문가들이 초보자의 몇 배 생산성을 내기 때문에 팀이 초보자보다는 전문가에 맞춰져서 굴러가야 한다고 보는 편이라 더욱 걱정이 컸다. 그래서, 어떤 규칙을 적용할 경우 전문가의 생산성을 유지하면서 초보자의 생산성을 업그레이드할 수 있을지 시뮬레이션을 해보았다.

여러 가지 시행착오를 거치면서 내가 확인한 것은, 애자일 & XP 커뮤니티에서 이야기하는 좋은 코드의 기준들은 대개 전문가의 생산성을 떨어드리지 않는다는 것이었다. 그런 기준들은 어떤 코드에서 냄새가 나는지는 모호함 없이 판단 가능하게 해주지만, 어느 정도 냄새가 심할 때 개선해야 할지, 또 어떤 식으로 개선할지 등에 대해서는 다양한 여지를 열어두기 때문에, 초보자에게 지침이 되어주면서도 전문가를 속박하지 않는 좋은 기준이 될 수 있다.

하지만, 이런 기준들이 또 너무 많으면 곤란하다. 실용주의 프로그래머의 다양한 원칙들, 리팩토링의 나쁜 냄새 목록 등은 모두 유용하지만, 이걸 다 머리 속에 담고 코드를 볼 수는 없는 노릇이다. 코드 품질 뿐 아니라, 팀을 운영하는 원칙도 너무 개수가 많으면 작동하기 어렵다. 그래서, 좀더 단순하면서도 부작용 없이 목적을 달성할 수 있는 원칙을 찾고자 했다. 코드의 문제는 곧 복잡성의 문제인데, 그 복잡성을 해결하기 위한 원칙이 복잡하다면 그것도 문제 아니겠는가. 단순한 원칙이 단순한 코드를 만든다.

Simple Design

이런 고민 끝에 찾아낸 규칙이 켄트 벡의 Simple Design이다. 네 가지에 불과해서 기억하기도 쉽고, 그러면서 코드 품질 문제의 핵심을 잘 정의하고 있다. 켄트 벡이 처음 표현한 문구와 마틴 파울러가 재해석한 문구 두 버전이 있는데, 둘 모두 가치가 있어서 적당히 조합해서 표현해본다.

근데, 이것도 실무적으로는 더 단순화할 수 있다. 첫번째 규칙은 설명이 필요 없는 규칙이라 대부분의 팀에서는 거론할 필요도 없을 것이다. 그리고, 세번째 규칙은 이 중에 가장 주관적인 규칙이라 역시 전문가들에게는 유용한 규칙일 수 있지만 초보자들에게는 또다른 부작용을 유발할 수 있는 규칙이며, 빼도 타격이 적다. 그래서, Simple Design을 다음과 같이 요약해서 정의할 수 있다.

중복이 없으면서 최소한의 문법 요소로 구성된 코드

이 정도면 기억하기도 쉽고, 실무에서 적용하기도 쉽고, 사람마다 다르게 해석할 여지도 적다. 중복이 뭔지는 굳이 해설이 필요 없을 텐데, 켄트 벡이 명시한 것처럼 숨어 있는 중복을 찾아내려는 노력은 좀 필요하다. 최소한의 문법 요소라는 게 좀 해설이 필요할 수 있는데, 켄트 벡은 원래 클래스와 메서드라고 표현했었던 것을 마틴 파울러는 최소한의 요소라고 좀더 일반화했고, 나는 이걸 다시 좀더 좁혀서 문법요소라고 표현했다. 클래스, 메서드 뿐 아니라 상속 계층, 콜 스택, 멤버 변수, 로컬 변수, 데코레이터, if 분기, 루프 등등이 다 문법 요소다. Cyclomatic Complexity가 늘어나는 것도 다 문법요소의 증가라고 볼 수 있다. 이런 게 최소한이 되는 설계를 찾아야 한다는 것이다. 여기에 더해 TLoC(전체 코드 분량)까지 최소화하면 더 좋다.

물론 중복을 해결하면서 문법 요소가 늘어나지 않을 수는 없다. 간단하게 extract method만 해도 메서드가 늘어난다. 하지만, 똑같이 중복을 해결하면서도 상속 계층이 몇 개씩 추가되거나, 클래스가 추가되기도 하는데, 메서드 하나만 늘어나면 더 유리한 것이다.

중복을 너무 일찍 제거하면 안된다고?

최근 Don't DRY Your Code Prematurely라는 글이 유행하면서 중복을 내버려둬야 할 때도 있다 이런 주장이 나도는데, 나도 중복을 내버려둬야 할 때도 있다고 보지만, 저 글은 좀 문제가 많다. 특히 premature라는 표현을 쓴 것이 premature optimization과 비슷한 문제인 것으로 오해하게 만들 수 있다는 점도 꺼려지는 점이다.

말 꺼낸 김에 저 글에 대해 좀더 구체적으로 반론하자면, DRY를 하지 않는 이유로 다음 이야기를 하고 있는데, 이거야말로 같은 글에서 언급한 YAGNI다. 앞으로 일어날지 아닐지 모를 일을 근거로 들면서 YAGNI를 이야기하면 곤란하다.

However, tasks and payments represent distinct concepts with potentially diverging logic

그리고, 중복을 해결하는 방식도 최소한의 문법요소로 해결했다고 할 수 없다. 나라면 이런 방법을 생각했을 것이다.

def assert_future(deadline):
    if deadline <= datetime.now():
       raise ValueError('blah')
    return deadline

set_task_deadline(assert_future(datetime(2024, 3, 12)))

YAGNI도 그렇지만 좋은 코드는 현재에 충실하게 Simple Design을 하는 것이지 미래에 일어날 그 어떤 변화를 미리 예상해서 유연하게 준비해두는 것이 아니다. 변화를 예상해서 DRY를 하지 않는다는 것은 좋은 생각이 아니다.

밥 먹기 전에 설거지하기

저 글은 다분히 잘못된 방향으로 독자들을 이끌고 있지만, 나 역시도 중복을 내버려 둬야 하는 경우는 존재한다고 생각하고, 또 그 방향 역시 YAGNI라는 관점이긴 하다. 근데 그 이유는 저 글처럼 모순된 이유가 아니라 바로 비용이다. 코드 품질을 개선하더라도 그 코드를 다시 만질 일이 없거나, 그 코드가 영향을 미치는 범위가 좁다면 코드를 개선해도 이득이 없다. 그러니까 코드를 개선하는 비용이 허공으로 사라지는 것이다.

그래서, 나는 밥 먹기 전에 설거지하는 전략을 섞어서 쓴다. 보통은 밥 먹고 나서 설거지하는 것이 좋은 생활 습관이지만, 설거지한 이득은 다음 밥 먹을 때가 되야 발생한다. 그렇다면 밥 먹고 설거지를 쌓아두었다가 밥 먹을 때가 되었을 때 설거지하는 전략을 쓸 수 있다. 아마 많은 자취생들의 전략일 것이다. 집을 청결하게 유지한다는 목적에서는 나쁜 전략이지만, 자취생들은 에너지를 절약하는 것도 중요하다. 다음에 언제 또 집에서 밥 먹을지도 모르는데 지금 설거지하는 것은 심리적 저항이 크다. 하지만 다음에 밥을 먹을 때가 되면 설거지 안하고는 밥을 못 먹기 때문에 설거지를 하게 되서 저항이 작아진다. 심리적 저항도 실무에서는 시간이라는 비용으로 현실화되기 때문에 우습게 보면 안된다.

그러니까, 코딩할 때도 필연적으로 발생하는 중복들을 바로 제거하는 것이 아니라 그 중복이 악영향을 미치기 시작할 때(설거지 하지 않으면 밥을 못 먹을 때처럼) 제거하는 것이다. 근데, 이게 좀 작은 단위로 일어나야 한다. 예를 들어, 주어진 작업을 완료하기 위해 A, B, C, D 네 가지 모듈을 작성해야 한다고 가정해보자. A는 B에서 사용되고 B는 C, D에서, C는 D에서 사용된다. D는 그 자체로 최종 목적이며 다른 곳에서 더 사용되진 않는다. 이런 기능을 개발할 때 A를 개발 완료하고 나면 이게 다시 B에서 쓰이니까 A에는 나쁜 냄새가 남아 있으면 안된다. 그래서 A는 충분히 리팩토링을 해두고 넘어간다. B, C도 마찬가지. 근데 D는 다른 곳에 영향을 안 준다면 굳이 리팩토링하지 않고 내버려 둔다. 다음에 다른 곳과 연관이 되기 시작할 때 리팩토링한다.

이런 식으로 비용과 타협하면서 코드 품질을 높여가자는 것이고, 이걸 한 마디로 표현한 것이 YAGNI라고 할 수 있다.

가끔은 극단적으로

근데 밥 먹고 설거지하는 전략은 초보자에게 추천할 만한 전략은 아니다. 코드 개선에 드는 비용이 얼마나 될지 추정하기 어렵고, 개발 단계에 따라서 다를 수도 있기 때문이다. 당장 기능 개발을 빨리 해내는 것이 회사의 운명을 좌우할 정도로 급하지 않다면 때때로 극단적인 코드 품질을 추구해보는 것이 좋다. 모든 회사가 스타트업인 것도 아니고, 유지보수가 중요한 단계로 넘어간 회사들이 더 많다. 그래서, 나는 상황에 따라 중복을 하나도 살려두지 말고 다 잡으라는 조언을 하는 경우가 많다.

또, 생각보다 많은 스타트업이 코드 품질 때문에 장벽에 가로막힌다. 몇 달 개발하지도 않았는데 코드가 지저분해서 더 못하겠다면서 갈아 엎자고 하는 사람이 나오는 팀을 숱하게 봤다. 이미 돌고 있는 비즈니스가 있고 핵심 경쟁력이 될 기능 하나를 추가해야 하는데 코드 품질 문제로 버그가 양산되면서 사고가 터지는 경우도 많다. 스타트업이라고 해도 상황에 따라 코드 품질을 높여야 하는 경우는 많다.

회사 뿐 아니라 개인에게도 이런 관점이 도움이 될 때가 많다. 극단적으로 코드 품질을 추구하는 경험을 해보면 다양한 방법들을 탐색해보게 되고 이 과정에서 좋은 학습이 일어난다.

또 하나 생각해야 하는 것은, 코드 품질을 떨어뜨리면서 개발 속도를 높이는 게 생각보다 어렵다는 것이다. 중복 코드도 종류에 따라 팀 전체의 생산성을 떨어뜨리는 것도 있고 별 영향을 안 주는 것도 있다. 어떤 종류의 나쁜 냄새는 내버려두면 하루이틀 짜리 개발 작업도 늦어지게 만든다. 이런 걸 판단하는 게 곧 실력이기도 하다.

코드 품질을 높이려는 이유 자체가 더 빨리 개발하기 위해서다. 간혹 코드 품질을 높이려면 프로젝트 기간이 더 필요하다는 주장을 접하는데, 프로젝트를 더 늦게 완료할 거면 뭐하러 코드 품질을 높이나? 주객이 전도된 것이다. 더 빨리 개발할 수 있게 해주는 코드가 아니라면 품질이 높은 코드가 아닌 거다.

코드 품질을 희생해서 개발 속도를 높인다는 건 컨텍스트가 머리 속에 다 들어 있을 때만 가능한 일이다. 머리 속에 담은 양을 벗어나서 작업하게 되면 결국 자신이 작성한 코드를 다시 읽어야 하고 사용해야 하는데, 그 코드들의 품질이 낮다면 손해를 보기 시작하는 것이다. 근데 프로그래머의 머리 속에 들어갈 수 있는 컨텍스트의 분량은 얼마나 되나. 보통은 30분 안팎일 것이다. 좀 큰 사람은 한두 시간, 천재들은 며칠 정도 되겠지. 근데 그 한계를 벗어난 크기의 일을 하게 되면 이미 코드 품질로 인해 생산성 하락을 겪기 시작하는 것이다. 한 달 정도 규모의 일을 나쁜 코드 품질로 빨리 완료할 수 있는 사람은 존재하지 않는다.

그래서, 나는 다소 극단적으로 코드 품질 - 중복이 없으면서 최소한의 문법 요소로 구성된 코드 - 을 추구해보라고 이야기하고 싶다. 비용과 타협해가면서 리팩토링을 저울질 하는 것은 자신이 코드 품질 전문가가 되었다고 생각할 때 시작해도 늦지 않다.

리팩토링의 나쁜 냄새는 어떤가?

내가 코드 품질에 대해 이야기할 때 실무적으로 가장 많이 활용하는 것은 리팩토링에서 말하는 나쁜 냄새(Code Smell)다. 이것도 매우 잘 정리되어 있고, 코드에서 바로 이건 이 냄새니까 이렇게 리팩토링하는 게 좋겠다고 설명하기 좋다. 켄트 벡의 Simple Design만으로는 충분히 구체적이지 않다고 느낄 때 나쁜 냄새 목록에서 찾아보는 것은 추천할 만하다. 목록이 많아서 Simple Design처럼 머리에 담아두고 즉시 활용하기는 어렵지만, 코드에서 나쁜 냄새를 맡았을 때 이게 무슨 냄새인지 찾아보고 생각해보는 용도로는 아주 훌륭하다.

본질적인 복잡성에도 도전하자

앞서서 본질적인 복잡성과 우발적인 복잡성을 언급했고, 코드 품질 개선의 대상은 우발적인 복잡성이라고 이야기했다. 하지만 여기서 한 발 더 나아갈 수 있다. 본질적인 복잡성으로 인해 복잡해진 코드를 보면 코드를 개선하자고 나서는 대신, 비즈니스를 건드려야 한다. 개발자가 이해하기도 어려울 정도로 복잡한 코드로 표현되는 비즈니스 로직이라면 비즈니스를 운영하는 실무자들도 이미 어려움을 겪고 있을 것이다. 그 복잡성이 정말 피할 수 없는 복잡성인지 고민하고 개선 방법을 찾아야 한다.

내가 이전에 경험한 팀에서도 환불과 관련된 규칙이 무려 30개의 조항에 달했었다. 처음에 담당자들과 미팅했을 때는 8개 정도로 정리가 되었었는데, 알고보니 담당자들도 인지하지 못하는 규칙들이 세세하게 많았고, 개발이 진행되면서 계속 새로운 규칙이 튀어나왔다. 그러니 담당자들도 어떻게 계산해야 할지 모르는 상황이 빈번하게 나오고 개발하기도 어려웠다. 근데 그 규칙들을 세세하게 뜯어보면 굳이 필요한가 싶은 규칙들이 정말 많았다. 그냥 우리가 월 수백원 손해보면 되는 것, 수천원 손해보면 되는 것, 혹은 아무도 손해보지 않는 것 등등 없애기 쉬운 규칙들이 많았고, 그런 것들을 하나하나 협의해서 없애나가서 나중에는 10여개의 규칙으로 정리할 수 있었다.

비즈니스에서 나오는 요구사항은 대개 담당자마다 다른 생각에서 나오고 통일된 기본 원칙 하에서 나오는 것이 아니기 때문에, 이런 걸 다 수용하다보면 본질적인 복잡성이 끝도 없이 커질 수 있다. 이럴 때 전체 규칙을 다 볼 수 있는 개발자가 비즈니스 규칙도 같이 다듬어 가면 비즈니스 전체에 이익이 될 수 있다. 나쁜 코드가 나오게 만드는 요구사항은 비즈니스적으로도 비용을 크게 증가시키는 경우가 많다. 나쁜 냄새의 기원이 본질적인 것이든 우발적인 것이든 일단 냄새를 맡았으면 개선하려는 시도를 해보자.

코드 품질과 비즈니스 성과

최근 좋은 코드, 돈 버는 코드 등의 이야기, Premature abstraction 등의 이야기가 나오면서 코드 품질을 꼭 추구할 필요는 없다는 주장이 종종 나온다. 나 역시도 QuickAndDirty의 중요성을 강조하는 글을 쓴 적이 있다. 실제로 코드 품질이 좋다고 비즈니스가 성공하는 것도 아니다. 하지만, 코드 품질이 나빠서 실패한 프로젝트는 수없이 많다. 코드 품질이 나쁨에도 불구하고 성공한 비즈니스들이 있기 때문에 종종 간과되는 사실이지만, 정말 많은 회사들이 이미 만든 서비스에 약간의 기능 추가하는데 실패해서 성장이 정체되고 있다. 

전에 애자일 방법론에 관한 글에서도 이야기한 적 있지만, 코드 품질을 개선하는 활동도 일종의 방법론이고, 과학적 방법론과 비슷하게 볼 필요가 있다. 과학적 방법론을 사용한다고 해서 과학적인 연구 성과가 보장되는 것은 결코 아니다. 또한, 과학적이지 않은 방법으로 여러 가지 직관과 우연이 겹쳐서 나온 과학적 발견도 무수히 많다. 그렇다고 해서 현대의 과학자가 과학적 방법론을 사용하지 않는다면 그게 말이 될까? 방법론이란 그런 거다. 절대 성과를 보장해주지 않지만, 성과를 낼 확률을 높여주기 때문에 하는 것이다.

그런 만큼 코드 품질을 높이느라 프로젝트 기간이 늘어난다면 코드 품질이란 걸 잘못 알고 있는 것이다. 더 빨리 개발하기 위해 코드 품질을 개선하는 것이라는 점을 잊지 말아야 한다.


블로그 | 소프트웨어 개발