컴퓨터 공학/개발 상식

[개발 상식] 좋은 코드란 무엇인가?

highright96 2021. 11. 3.

많은 개발자가 좋은 코드에 대해 이야기하는데, 좋은 코드의 기준은 가진 경험에 따라 달라지는 것 같습니다. 개발 경험이 적은 개발자는 ‘~가 좋은 코드입니다.’라고 확신에 찬 답변을 내릴 수도 있고, 반면에 경험 많은 개발자는 명확한 답변을 내리지 못하고, 오히려 요즘 유행하는 클린 코드에 대해서도 의문을 품을 수 있습니다.

만약, ‘좋은 코드란 무엇일까요?’라는 질문이 들어온다면, 협업과 프로젝트를 진행하며 겪은 경험을 바탕으로 답변을 하는 것이 좋을 것 같습니다.

제가 느낀 ‘좋은 코드’의 조건은 다음과 같았습니다.
-  누구나 쉽게 따라가며 읽을 수 있을 것
-  언제든 원하는 기능을 다시 찾아갈 수 있을 것
-  5년 뒤에 내가 다시 보더라도 금세 이해할 수 있을 것
-  새로운 기능을 추가하더라도 크게 구조변경이 없을 것

 

가독성이 좋은 코드

위의 조건을 충족시키기 위한 중요한 핵심은 ‘가독성이 좋은 코드’라고 생각합니다. 코드를 다른 사람이 읽기 쉽게 작성해야 그만큼 코드 해석에 드는 비용을 줄일 수 있기 때문입니다.

 

코드의 가독성이란 ‘코드가 잘 읽히고, 해당 코드의 동작을 직관적으로 예측할 수 있는지’를 뜻합니다.

 

가독성을 아래 두 개념으로 나누어서 이야기하고자 합니다.​

 

표현적 가독성 (Legibility)

눈에 잘 들어오는 코드, 읽기 편한 코드를 뜻합니다. 표현적 가독성을 지키기 위해 따르면 좋을 몇 가지 규칙이 있습니다.

언어별 코딩 룰

언어마다 정해진 네이밍이나 디자인 방식이 있습니다. 물론 취향 차이와 그에 따른 논란은 있을 수 있지만, 취향의 자유보다 통일성 있는 코드가 더 중요하다고 생각합니다.

 

IDE 포맷터 세팅 및 형식 맞추기

IDE에서 읽기 편한 환경을 구성하기 위한 형식 맞추기 작업입니다. 대부분의 IDE는 포맷터를 설정할 수 있도록 지원하는데, 이 설정을 팀원들 간에 통일하면 서로 다른 포맷팅으로 인한 의미 없는 코드 변경을 줄일 수 있습니다.​

 

함수

‘함수는 한 가지 책임을 갖는다’라는 말이 있습니다. 이를 단일 책임 원칙이라 부릅니다.​

클린 코드에서는 20줄 미만의 함수를 권장합니다. 20줄이라는 기준은 함수를 물리적으로 제한하여 자연스럽게 책임을 제한하려는 의도입니다. 즉 함수의 길이는 현상이고, 본질은 함수의 책임입니다.​

짧은 함수 자체의 형식적인 가치도 있겠지만, 전체 함수가 한 화면에 들어오지 않으면 화면을 위아래로 오가면서 코드를 읽어야 해 가독성을 떨어뜨립니다. 팀원과 상의해 코드 길이의 적당한 마지노선을 잡으면 좋을 것 같습니다.

때로는 이 기준에 집착해서 필요 이상으로 함수를 쪼개기도 하는데, 이는 다소 잘못된 적용이라고 생각합니다.

 

주석

주석이 잘못되었다고 프로그램이 죽지는 않기 때문에 잘못된 코드는 빠르게 수정되지만, 잘못된 주석은 잘 수정되지 않습니다. 주석은 코드에 비해 중요도와 영향력이 낮아서 우선순위에서 밀리다가 잊혀 잘못된 내용의 주석이 될 수도 있습니다.

그래서 클린코드에서는 주석은 필요악이라고 이야기합니다. 관리가 잘 안 되는 만큼 정말 필요한 부분에만 최소한으로 사용하고 가능하면 코드 자체가 자신을 설명할 수 있게끔 작성해야 합니다.

 

기능적 가독성 (Readability)

변수, 함수, 클래스 등이 어떤 역할을 갖고, 어떤 동작을 하며, 서로 어떤 관계를 맺는지 직관적으로 파악할 수 있는 코드, 즉 이해하기 쉬운 코드를 뜻합니다.

기능적 가독성을 지키기 위해 따르면 좋을 몇 가지 규칙이 있습니다.

 

함수의 내려가기 규칙

함수의 내려가기 규칙은 기능적 가독성을 구성하는 가장 중요한 요소입니다. 내려가기 규칙은 아래 두 가지 원칙으로 구성됩니다.​

함수는 한 가지 추상화 단계를 처리합니다.
n 단계의 추상적인 함수는 n-1 단계의 추상적인 함수로 구성됩니다.​​

추상화 단계는 어려운 말이지만, 문제 혹은 과업(Task) 정도로 가볍게 풀이할 수도 있습니다.​  ‘식사를 한다’는 것이 가장 추상적인 과업이라면, ‘점심에 팀원들과 제육쌈밥’을 먹는 건 그보다 한 단계 구체적인 과업의 집합입니다.​

같은 예시를 활용하자면, 내려가기 규칙은 함수를 작성할 때 ‘점심 식사를 한다’와 같은 식으로 추상화 단계가 섞이도록 작성하지 말 것을 강조합니다. 바로 아래에 ‘저녁 식사를 한다’ 함수를 만들고 싶지 않다면 말입니다.​

내려가기 규칙은 낯설 수도 있지만, 기능 명세를 구성하다 보면 자연히 추상화 단계를 나타낼 수 있습니다.

 

// 식사의 기능 명세
식사를 합시다!
- 누가 먹나요?
- 어떤 메뉴를 먹지?
 	- 누가 먹는지, 언제 먹는지 중요
- 어디서 먹지?
 	- 어떤 메뉴인지, 몇 명 인지 중요 
- 그럼 진짜로 식사를 합시다!

public void haveMeal(Company c) {
  mealMembers = c.getMealMembers();
  menu = c.selectMenu(mealMembers, LocalDateTime.now());
  restaurant = c.selectRestoraunt(menu, mealMembers.size());
  c.haveMeal(mealMembers, menu, restaurant);
}

 

의미 있는 이름

팀원들에게 내 코드를 보여주고 의견을 주고받을 때, 상대방이 내가 작성한 변수명에 대해서 정확하게는 몰라도 최소 어느 정도 예상할 정도로 명확한 이름을 짓는 것이 중요합니다.

좋은 이름을 짓기 위해서는 모든 것에 이름을 붙인다고 생각하고, 짧고 불명확한 이름보다는 길고 명확한 이름이 낫다는 걸 명심해야 합니다.

1. 항상 이름 있는 상수(Named Constant)를 사용하고, 매직 넘버, 매직 스트링은 사용하지 않습니다.
2. -Info, -Data, tmp- 와 같은 무의미한 접미사, 접두사는 제거합니다. (tmpInfoData?)
3. 대명사, 축약, 생략은 알아보기 힘듭니다. ex) disp보다는 display가 더 명확하고, 해석하기 쉽습니다.
4. 서로 무관한 함수에서 같은 이름을 사용해서는 안 됩니다.
5. 마찬가지로 서로 연관된 함수에서 같은 대상을 다른 이름으로 불러도 안 됩니다(일관성).
6. 타입 인코딩(-string, -int 같이 타입을 적어두는 것)은 지양합니다.

 

추상화 단계에 따른 이름 짓기

함수의 이름은 내려갈수록 구체적으로​ 지어야 합니다. 이름은 충분히 설명적이어야 하지만, 상위 함수가 모든 동작을 설명할 수는 없습니다.

가령, 아래 Parse 함수를 DetermineFileExtensionAndParseConfigurationFile 로 바꾼다면 되려 읽기가 어렵습니다.​

함수의 이름은 자신의 추상화 단계를 따릅니다. 즉, 함수는 내려갈수록(깊어질수록) 구체적이고 설명적인 이름을 갖어야 합니다.

 

func Parse(filepath string) (Config, error) {
  switch fileExtension(filepath) {
    case "json":
    	return parseJSON(filepath)
    case "yaml":
    	return parseYAML(filepath)
    case "toml":
    	return parseTOML(filepath)
    default:
    	return Config{}, ErrUnknownFileExtension
  }
}

 

반대로, 변수의 이름은 내려갈수록 추상적으로 지어야 합니다.

앞서 본 것처럼, 함수는 내려갈수록 구체적입니다.​ 함수가 구체적이라는 의미는 해당 함수의 범위(scope)가 더 명확해진다(좁아진다)는 의미입니다.

좁은 범위에서 사용되는 변수명은 덜 명확해도 충분히 구체적일 수 있습니다.

반대로, 변수가 사용되는 범위가 넓어질수록 변수명은 구체적이어야 합니다. 즉, 변수의 이름은 선언과 사용이 멀어질수록 구체적이어야 합니다. 예를 들어 글로벌 변수는 매우 구체적인 이름을 가져야 합니다.

인자​

인자 수는 1~2개로 유지합니다. 3개를 넘어가는 경우 클래스(구조체)로 묶어서 주고받는 편이 낫습니다.​

클래스(구조체)를 넘기면 클래스(구조체) 자체에도, 각 인자에도 이름을 붙일 수 있습니다. 즉, 맥락을 주입하고 명확성을 보장할 수 있게 됩니다.

 

참고

댓글