많은 개발자들이 클린 코드를 지향하고, 클린 코드를 작성하려 합니다. 잘 관리되지 않는 레거시 코드와 기술 부채는 굉장히 나쁘게 바라보고요.
하지만 클린 코드에만 집중하고 레거시를 정리하려고만 하다 보면, 클린 코드의 본질을 놓치고 그저 코드를 예술적으로 만드는 것만 신경쓰게 될 수도 있습니다. 이번 글에서는 제 철학을 바탕으로, 클린 코드를 어떻게 바라봐야 할지 작성해 보려 합니다.
먼저 요즘 일반적인 의미에서 개발자는 코드를 통해 어떠한 목적을 가진 소프트웨어를 만들어내는 사람입니다. 소프트웨어에서 중요한 걸 생각해 보면 일단 성능이 좋아야겠고, 버그가 없어야겠고, 그리고 개발 속도가 빨라야겠죠?
따라서 코드에 대한 개발자의 개발 역량은 아래와 같다고 볼 수 있겠습니다.
그리고 클린 코드의 정의 역시 여기에서 출발합니다. 버그를 양산할 여지가 없어야 하고, 기능을 추가하고 싶을 때 편하게 추가할 수 있어야 하는 거죠. 그러기 위해서는 네이밍이 깔끔해야 하고, 사용하지 않는 값이 불필요하게 정의되어 있지 않아야 하고, 로직이 책임과 역할이 확실하게 구분되어 명료하게 정의되어 있어야 하는 등등, 우리가 클린 코드
라고 하면 생각하게 되는 그런 특징들을 가져야 할 거예요. 코드가 클린하면 좋다는 건 당연하고, 이 부분에 대해서는 반박의 여지가 없을 거예요.
정리하자면, 클린 코드의 역할은 결국 "정확한 기능을 추후 더 빠르게 개발할 수 있게 하기 위함" 이라고 볼 수 있겠네요.
실제로 저 역시 레거시 코드를 많이 다뤄 봤고, 레거시 코드가 가지는 위험성과 개발자의 피로도에 대해 인지하고 있습니다. 읽기 쉽고 안전하고 깔끔한 코드는 개발자에게 상당히 매력적이라는 것도 어느 정도 경험해 보았습니다.
오늘 살펴보고자 하는 포인트는 그럼 과연 클린 코드를 얼마나 중요하게 생각하고 지켜봐야 하는가입니다.
결론부터 말하자면, 저는 언제나 빠른 개발과 깔끔한 구조 사이의 적절한 합의점을 찾아야 한다고 생각하며, 클린 코드에 너무 집중하는 것은 불필요하다고 생각합니다.
아래와 같은 레거시 JavaScript 코드를 봅시다.
function check_adj_true_exists (map, coord) {
var ret = false;
if (map.getVal(coord.x - 1, coord.y - 1)) ret = ret && true;
if (map.getVal(coord.x - 1, coord.y)) ret = true;
if (map.getVal(coord.x - 1, coord.y + 1)) ret = true;
if (map.getVal(coord.x, coord.y)) ret = true;
if (map.getVal(coord.x + 1, coord.y - 1)) ret = true;
if (map.getVal(coord.x, coord.y + 1)) ret = true;
if (map.getVal(coord.x + 1, coord.y)) ret = true;
if (map.getVal(coord.x + 1, coord.y + 1)) ret = true;
if (map.getVal(coord.x, coord.y - 1))ret = true;
return ret;
}
이미 ret 가 true 가 되어도 아래 if문을 계속 돌리는 비효율적인 코드에, 로직의 반복으로 인해 이후 코드를 수정할 때 실수를 할 여지가 있고 (DRY
원칙), 요즘 JS라면 당연시되는 정적 타입 체킹
도 안 되어 있고, ES6 이후로 쓸 이유가 없어진 var
을 아직도 사용하고 있고, 주석도 안 달려 있네요. 변수명도 snake_case
랑 camelCase
가 막 섞여 있군요.
아래와 같은 TypeScript 코드로 리팩토링하고자 하는 욕심이 생깁니다.
interface Map {
getVal: (x: number, y: number) => boolean;
}
interface Coordinate {
x: number;
y: number;
}
// 인접한 9개의 element 중 true 가 있는지 확인
const checkAdjTrueExists = (
map: Map, { x, y }: Coordinate
) => {
for (let i = -1; i <= 1; i++)
for (let j = -1; j <= 1; j++)
if (map.getVal[x + i][y + j])
return true;
return false;
}
반복되는 로직이 없어졌고, 비교적 읽기 쉽고, 주석도 생겼고, 정적 타입 체킹도 되어 있네요.
하지만, 저는 몇 가지 이유로 이러한 리팩토링은 매우 주의해야 한다고 말하고 싶습니다.
앞서 클린 코드의 목적은 정확한 코드를 짜는 데 필요한 시간의 단축이라고 말했습니다. 위의 함수가 과연 자주 업데이트될까요? 자주 업데이트된다고 해도, 업데이트하는 데에 리팩토링에 소요되는 시간보다 더 많은 시간이 소요될까요?
물론 코드 퀄리티가 높으면 좋죠. 하지만 대부분의 상황에서 "충분히 잘" 동작하는 낮은 퀄리티의 코드가 문제를 일으키지는 않습니다. 당신이 완벽한 코드를 만드는 오픈소스 메인테이너라면 말이 다르겠습니다만, 코드 구조가 훌륭하고 레거시가 없다면 당신의 서비스에 도움이 되지 않는 일에 쓸모없는 시간을 허비하는 중은 아닐지 의심해 봐야 합니다.
기존 코드가 "예상한 대로 동작한다"는 보장은 없습니다. 가령 이런 리팩토링으로 인해 오히려 기존에 되던 게 안 될 수도 있습니다. 나중을 위해 리팩토링을 하는 건데 예상치 못한 버그가 생긴다면, 의도는 선했으나 팀과 회사에게 민폐가 될 수 있습니다.
위 리팩토링에서 이런 케이스가 발생하는 예시를 한 번 들어 볼게요. 가령 Map.getVal
이 getter function 처럼 네이밍되어 있어 idempotent 하다고 생각했지만, 실제로 그렇지 않을 수도 있습니다. 기존의 로직은 주위 8칸에 대해 모두 getVal
을 실행했으나 새로운 로직은 그렇지 않고, 이로 인해 예상치 못한 변화가 발생할 가능성이 있습니다.
시도 때도 없이 변하는 현대 개발 업계에서는 몇 년이면 또 새로운 기술이 나오고, 새로운 패러다임이 등장합니다. 추후 이 코드를 수정하고 싶어졌을 때 여러분의 신규 코드 역시 레거시로 보일 수도 있습니다.
또한, 코드의 원 작성자가 여러분의 코드를 clean 하다고 느끼지 않을 수도 있습니다. 기존의 방식이 더 편하고 깔끔하다고 말한다면, 충분한 이유를 들어 반박할 수 있을까요? 혹은 그럴 만한 가치가 있을까요?
클린 코드는 좋은 개발자라면 지향해야 할 훌륭한 습관입니다. 하지만 클린 코드는 개발자의 목적이 아니라, 빠른 피드백을 정확하게 할 수 있게 하기 위한 수단입니다.
클린 코드로의 리팩토링은 상당한 주의가 필요하고, 생각보다 많은 경우에 안 해도 괜찮은 작업입니다. 코드를 하나의 예술 작품으로 만드는 것은 사용자에게 직접적으로 단 하나의 영향도 미치지 못하니까요. (물론 필요한 청소를 제때 하지 않는 건 위험합니다.)
마지막으로, 클린 코드를 지향하지 말자는 이야기가 절대 아닙니다! 클린 코드는 분명 중요하고 좋아요. 다만 클린 코드를 왜 지향해야 하는지, 클린 코드가 우리에게 어떤 도움을 주는지 확실히 파악하는 게 중요하다는 이야기였습니다 :)
동의합니다. 모든 개발자가 "클린 코드는 생산성에 도움이 된다."라는 부분에 대해서는 이견이 없을 겁니다. 그러나 '클린 코드를 추구하는 것'은 도리어 생산성에 저해가 될 수 있다고 생각합니다.
말씀하신대로 클린 코드를 위해 코드를 수정하는 일 자체에 가장 귀중한 자원인 시간이 소모된다는 점이 제일 큰 이유라고 생각합니다.
그리고 클린 코드라는 건 마치 둥근 지구를 모험하는 것 같아서 목적지에 도달하면 또 다른 목적지가 보이고... 이러한 과정이 무한히 반복되는 것 같습니다.
그리고 이건 저의 주관적인 경험이지만, 생각보다 확장성을 고려하여 만든 코드를 확장할 일이 별로 없다는 것입니다. 제일 좋은 코드는 짧고, 쉽고, 말썽을 자주 일으키지 않는 코드가 아닐까 생각합니다.