코드 퀄리티를 빠르게 높이는 방법

들어가며

작년 상반기 CS의 밑바닥을 다지고 싶어서 Nand2Tetris 프로젝트와 몇몇 CS도서를 학습했고, 하반기에는 ‘대용량 트래픽 문제해결 경험’을 얻고 싶어서 이를 연습하는 사이드 프로젝트를 구상했고, 성공적으로 마무리했다.


올해 상반기 때는 ‘코드 퀄리티’를 올리고 싶었고, 앞으로 이야기할 ‘코딩철학 만들기’를 통하여 실무 프로젝트에서 월평균 52개의 코드 리뷰를 작성하며, 굉장히 적극적인 자세로 ‘코드 퀄리티’에 대해 고민하게 되었다. 어떤 코드가 좋은 코드인지 명확히 설명하지 못했던 내가 어떻게 가장 활발한 코드리뷰어가 되었을까?

‘하나의 리뷰 쓰레드’를 생성한 경우만 카운팅에 포함했다. 댓글은 포함하지 않았다. 물런 ‘숫자’자체가 의미가 있는것은 아니다. 하지만 코드리뷰에서 매우 적극적인 자세로 임할 수 있게 되었다는 점에 큰 의미가 있다.

코드 퀄리티를 높이는 방법론

올해 2월경, 평소 즐겨보던 개발 유튜브 채널 에디의 개발 여행기에서 고민상담 신청을 받고 있었고, 나는 바로 신청했다. 무려 한시간 반동안 많은 이야기를 들었지만, 그 중 이 글에서 다루는 것은 코딩 철학에 대한 이야기다.

코딩 철학이란 코드를 쓰는 본인만의 원칙을 의미한다.

어떻게하면 코딩을 잘할 수 있나요? - 에디의 개발여행기 영상 中

problem1.png

  • 왼쪽 방법: 변수명을 통해 ‘어떤 정보’를 전달하는 방법
  • 오른쪽 방법: 더 간결하고, 변수를 저장하지 않아 메모리를 효율적 사용하는 방법

위 코드 중 어떤 코드가 잘 짜여진 코드일까? 당연히 정답은 없다. 따라서, 우리는 자신만의 코딩 철학을 먼저 세우고 그 철학에 맞는 ‘스타일’을 선택해야 한다.


summary.png

고민 상담 中 작성한 메모

코딩 철학 세우기

위 상담 후 얼마 지나지 않아, ‘프로그래머의 뇌’라는 책을 읽었고, 아래와 같은 코딩 철학을 만들었다.


아무리 작성할때 불편하더라도, 읽기 좋은 코드를 우선한다.


책 내용을 소개하는 글은 아니기에, 근거를 간단하게 언급하고 넘어가겠다.


Q. 어떤 코드가 읽기 힘든 코드일까?

  • 하나의 흐름이 너무 길어 읽다가 이전 내용을 잊어버리고, 다시 반복해서 앞 내용을 살펴볼 때
  • 읽다가 많은 내용을 기억해야 해서 머리가 아플때
  • 사람의 기억공간에는 한계가 있기 때문에 위와 같은 인지부하를 줄여야 한다

Q. 왜 읽기 좋은 코드를 우선해야할까?

  • 개발자는 30% 시간동안 코드를 작성하고 나머지 70% 시간 동안은 코드를 읽는다.
  • 코드는 몇번 쓰여지지만, 여러 사람에 의해 수십번 읽힌다. 즉, 장기적으로 봤을때 코드를 작성하는 사람보다 읽는 사람이 더 많다.

Q. 빠른 개발을 해야할때는?

  • 개발이 빠르다는건 언제나 ‘개발초기’에만 해당한다. 일단 코드를 만들면 반드시 수정할 일이 생기고, 수정할려면 먼저 코드를 읽어야 한다.
  • 비효율성은 ‘읽을 때’ 더 크게 발생한다.

코딩 철학을 바탕으로 반복 연습하기

이제 철학을 만들었으니 ‘실패와 극복’을 반복 연습하면 된다. 운이 좋게도 당시 실무에서 존재하던 레거시 입출금 시스템신규 입출금 시스템으로 마이그레이션 하는 과정에 있었고, 매우 많은 실패와 극복 연습을 할 수 있었다. 2년 가까이 직접 메인 유지보수 하던 시스템을 다시 만들어보는 경험은 너무 만족스러웠다.

레거시 유지보수는 어떤 능력을 향상시킬까? ‘운영’ 역량을 향상시키며, 개발자에게 유연한 구조와 코드퀄리티를 고민하게 만든다. (즉 신규개발과 유지보수는 필요한 역량이 다르다)


이제 몇가지 사례를 소개하겠다. 아래 사례들은 모두 실무때 남긴 코드리뷰이다. (가독성을 위해 몇몇 사례는 리뷰 스크린샷을 생략했다) 또한 대부분 ‘프로그래머의 뇌’에 나오는 생각들이다.

당연하게도, 옳은 코딩 스타일은 없으며 각각 장단이 존재한다. 또한 맥락 전체를 제공할 수도 없기 때문에 한계점이 존재하지만 ‘이런 관점도 존재하는구나’정도로 받아들여주면 좋겠다. 🙇‍♂️

생각이나 결론이 부족하더라도 넓은 이해 부탁한다. 🙇‍♂️

하나의 테스트케이스는 하나의 시나리오만 테스트하자

5.png

코딩 철학에 대한 리뷰는 아니지만…

테스트 케이스를 이해하는데 오히려 불필요한 정보

6.png

Q. 테스트 작성은 너무 오래걸린다. 일단 빠르게 완성하는게 좋지 않을까?

if 조건을 아주 조금만 변경하면 되는데, 관련 테스트 까지 함께 수정하다 보면 시간을 몇 배로 소모

생각하는 방식

  1. 빠르다는건 언제나 ‘개발초기’에만 해당한다. 코드를 만들면 반드시 수정할 일이 생기고, 수정할려면 먼저 코드를 읽어야 한다. 한번 작성한 코드는 각기 다른 사람들에게 수십번 읽힌다.
  2. ‘전체적인 개발 관점’에서 바라보면 이야기가 달라진다. 개발자가 코드를 수정하면 테스트해야한다. 수동으로 테스트할려면 일단 필요한 코드를 다 완성해서 DB에 데이터를 준비해두고, 외부 API들을 다 띄우고 직접 호출해야 한다. 이 과정에서 에러가 발생하면 전체 콜스택을 따라 많은 코드를 분석한다. 이러한 수동 테스트 시간을 줄일려면 테스트를 자동화해야한다.
  3. 물런 테스트 커버리지가 높다고 완벽하게 모든 문제를 없앨 수는 없다. 하지만 통과한 ‘케이스’에 대해서는 문제가 없다고 확신 가능하다. (또한 문제 상황이 생기면, 이미 작성된 테스트를 따라 쉽게 새로운 케이스 구현이 가능하다) 따라서 ‘테스트가 검증하는 범위’, 즉 커버리지가 높아야 한다. 따라서 정상적인 상황과 많은 예외적인 상황(Failed) 에 대한 테스트 코드를 작성하자.

if-else 보다 if-return이 읽기 쉽다.

아래 두 예시 중 어떤 코드가 더 읽기 편할까?

function calculateFee(user) {
  if (user.age > 20) {
    if (user.city === 'Seoul') {
      // some code
    } else if (user.city === 'Busan') {
      // some code
    } else {
      // some code
    }
  } else {
    // some code
  }
}
function calculateFee(user) {
  if (user.age <= 20) {
    // some code
    return;
  }

  if (user.city === 'Seoul') {
    // some code
    return;
  }
  
  if (user.city === 'Busan') {
    // some code
    return;
  }

  // some code
}

나는 2번째 함수(if-return 패턴)가 더 읽기 편하다고 생각한다.

그 이유는 if-else 패턴은 ”하나의 흐름이 너무 길어 읽다가 이전 내용을 잊어버리고, 다시 반복해서 앞 내용을 살펴보는 것“에 대한 가장 대표적 사례라고 생각한다. (else를 기억하며 코드를 읽어야 한다)

4.png

그러한 관점으로 개선 방향을 리뷰로 남겼다

if문에서 else와 else if를 지양하는 의견 정리

항상 최소 구현을 하고, 필요할 때 확장하자

function execute() {
  if (condition) {
    return false;
  }
  return true
}

execute()

8.png

의미없이 Boolean을 리턴하는것은 ‘최소 구현’이 아니다. 다른 개발자로 하여금 “상위에서 true나 false로 어떠한 판단을 하는 로직이 있구나” 등의 오해를 유발한다

즉, Input/Output 모두 필요한만큼만 최소로 구현해야 한다.

가장 좁게 구현하여, 리팩토링을 쉽게 만들자

// a
await sendSuccessDepositMsg(
  accountId,
  coin, 
  amount,
)
// b
await sendMsg(
  define.SEND_DEPOSIT,
  define.STATUS.SUCCESS,
  accountId,
  coin, 
  amount,
)

A는 내부 구현을 ‘숨기고’ 외부에 필요한 최소한의 Input만을 기능 단위로 구분했다.

하지만 B는 이를 외부로 드러냈다. B는 의존성에 따른 ‘수정’이 상위로 전파될 가능성이 큰 코딩 방법이다.


2.png


실제로 A,B 방법 모두 레거시 프로젝트에 공존하던 스타일이였고, 레거시 알림 시스템 통합작업이 발생하며, 전체를 수정할 일이 생겼다. 위와 같이 A 스타일의 코딩 방식은 내부 수정을 통해 어떠한 Input/Output 변화가 없었다. 즉 상위로 ‘코드 수정’이 전파되지 않았다.

당연하게도 B는 상위레이어로 코드 수정이 전파되었다.

Q. 반복문을 두 번 실행하는것보다 한번 실행하는게 효율적이다. 그런데 왜 읽기 힘들까?

예를들면 이러한 코드가 존재했다.

for (;;;) {
  // select and validate...
  // insert => update db...
  // etc...
}

1.png

매우 간단한 예시를 사용했지만, 많은 비지니스 로직이 섞여있었다. (한마디로 코드 블록이 상당히 길다. 매일 유지보수해도, 한눈에 이해할 수 없다.)

Q. (위 케이스에서) 반복문 내용은 간결해지더라도, 어차피 코드 내용은 똑같은것 아닌가요?

어떤 코드가 더 읽기 편할까?

const list = [...]
const target = 7

const resultIndex = binarySearch(list, target)
const list = [...]
const target = 7

let start = 0;
let end = list.length-1
let mid

while (start <= end) {
  mid = parseInt((start + end) / 2)

  if (target === arr[mid]) {
    return mid;
  } else {
    if(target < arr[mid]) {
      end = mid - 1
    } else {
      start = mid + 1
    }
  }
}

const resultIndex = mid ? mid : -1

첫번째 코드가 더 읽기 편하다고 생각한다.

그 이유는 간단하다, 실제 기능은 같음에도, 우리는 binarySearch라는 추상화된 함수이름만을 보고도 해당 코드가 무슨 일을 하는지 추측할 수 있다. 반면 2번째 코드는 해당 코드를 직접 읽고, 흐름 파악할때까지의 시간이 소요된다.


Q. 만약 binarySearch가 무엇인지 모르는 사람은 코드 깊이가 늘어났기 때문에 오히려 2번 코드가 더 좋은거 아닐까요?

  • 아니다, binarySearch를 모르는 사람의 경우는(함수명을 보고 기능추측이 안될 경우) 함수 안에 내용을 파악하고, 다시 상위함수로 돌아왔을때 추상화의 장점을 똑같이 누릴 수 있다.
  • depth가 증가하는 고통은 크지 않음으로, 가독성이 좋아지는 방향으로 메서드를 추출해야 한다.

마치며

반복 강조하지만, 이러한 코딩 스타일은 각가의 장단점이 있어서 쉽게 어떤 스타일이 더 좋다고 결론 내리기 힘들다. 그러나, 하나의 ‘코딩 철학’을 통해 우선순위를 매겨 가치판단할 수 있다는 점은 굉장한 것 같다. 애매하게만 느껴지고, ‘막연히 이게 좋지 않을까?’ 라고 생각해오던 내가 코딩 철학을 통해 매우 적극적인 코드 리뷰가 가능해졌다.

지금 글에서 설명한 방법이나 결론은 당연히 언제나 좋은 것도 아니며, 심지어 대부분의 경우 좋지 않을 수도 있다. 또한 내가 최고의 코딩 스타일을 찾았다는 것도 아니다. 다만 더 나은 방법을 찾고, 내 것으로 보다 빠르게 흡수할 수 있게 되었다고 생각한다.

다음 목표는?

현재 내 다음 목표는 최고의 개발자들의 코드를 보고 트랜드를 읽는 방법 - 잡상훈(오픈소스 코드 읽기)에 있다.


tronweb.png

에러가 생기거나 문서에 나오지 않는 기능을 개발해야 할때, 위와 같이 SDK의 버전 태그를 선택하고 해당 버전 소스코드에 필요한 부분을 중심으로 콜스택을 따라 분석하여 문제를 해결한다.

그러나 아직은 ‘익숙한 언어’의 한계와 ‘문제해결 중심 리딩’이라는 한계점이 있다고 생각한다. 올해 오픈소스의 기여하는 도전도 성공했지만, 아직은 그러한 한계점을 깨지 못한것 같다. 앞으로 더욱 더 노력해서 장기적으로 더 좋은 퍼포먼스를 보이고 싶다.


Written by@Juna
I love Node.js