종제로
종제로 Devlog
종제로
전체 방문자
오늘
어제
  • 분류 전체보기 (43)
    • C, C++ (22)
      • C, C++ (10)
      • Modern C++ (4)
      • 전문가를 위한 C++ (책) (8)
    • DirectX 자체엔진 개발 (8)
    • 자료구조 알고리즘 (10)
      • 공부 (9)
      • 문제풀이 (1)
    • 자기 계발 (1)
    • 기타 (2)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 모두의C언어
  • directX
  • C
  • c++ 17
  • DirectX11
  • 알고리즘
  • C++
  • 전문가를 위한 C++
  • c++ 11
  • 자료구조

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
종제로
C, C++/전문가를 위한 C++ (책)

전문가를 위한 C++ : 3장 코딩 스타일

C, C++/전문가를 위한 C++ (책)

전문가를 위한 C++ : 3장 코딩 스타일

2022. 6. 11. 15:28

3장 코딩 스타일

제대로 작동하는 코드를 작성하는 것만이 프로그래머가 할 일의 전부는 아니다.

기본적인 코드 작성법은 누구나 쉽게 배울 수 있다. 진정한 실력은 코드를 얼마나 세련되게 작성하느냐에서 드러난다.

 

 

1. 코딩 스타일의 중요성

코드를 작성할 때는 항상 누군가가 이 코드를 넘겨받는다고 생각하자.

자신이 직접 작성한 코드도 나중에 보면 기본 로직조차 기억나지 않을 수 있다.

 

잘 작성된 코드에서 볼 수 있는 공통적인 속성은 다음과 같다.

- 문서화
- 분할
- 명명 규칙
- 언어 사용
- 포매팅

 

 

2. 코드 문서화

프로그래밍에서 말하는 문서화(documentation)란 주로 소스 파일에 작성된 주석을 의미한다. 주석은 여러분이 코드를 작성할 당시 가졌던 생각을 다른 이에게 전할 수 있는 좋은 기회다. 코드만 봐서는 뚜렷이 드러나지 않는 사항을 표현하기 좋다.

 

주석을 작성하는 이유

주석을 사용하면 좋다는 것은 알지만 그 이유를 진지하게 생각해본 적은 별로 없을 것이다.

주석을 사용해야 하는 이유는 다양하다. 하나씩 살펴보자.

 

(1) 사용법을 알려주는 주석

주석을 작성하는 한 가지 이유는 클라이언트에 코드를 사용하는 방법을 알려주기 위해서다. 일반적으로 개발자는 함수의 이름, 리턴값의 타입, 매개변수의 이름 및 타입만 보고도 그 함수의 기능을 쉽게 파악할 수 있어야 한다. 하지만 모든 것을 코드만으로 표현할 수는 없다. 때로는 함수를 호출하기 전과 후에 특정 조건을 만족해야 하는데, 이러한 사항은 주석 외에는 표현할 방법이 없다. 함수가 던지는 익셉션도 주석에 남겨야 한다. 이때 꼭 필요한 정보만 주석에 남겨야 한다.

 

주석은 코드로 직접 표현하기 힘든 내용을 자연어로 표현하기 좋은 기회다. 예를 들어 데이터베이스 객체를 다룰 때 openDatabase()를 호출하지 않은 상태에서 saveRecord() 메서드부터 호출하면 익셉션이 발생한다는 사실을 c++ 코드만으로는 표현할 수 없다. 이러한 제약사항은 다음과 같이 주석으로 전달하기 딱 맞다.

/*
 * openDatabase()를 먼저 호출하지 않은 상태에서
 * 이 메서드를 호출하면 DatabaseNotOpenedException이 발생한다.
 */
int saveRecord(Record& record);

 

C++에서는 메서드를 선언할 때는 반드시 리턴 타입을 지정해야 한다. 하지만 리턴값이 구체적으로 무엇을 가리키는지 표현할 방법은 제공하지 않는다. 예를 들어 saveRecord() 메서드를 선언할 때 int 값을 리턴한다는 사실은 표현할 수 있지만(바람직 하지 않은 디자인 방식이다) 이 선언문만 보고 int 값이 구체적으로 무엇을 의미하는지 정확히 알 수는 없다.
이때 다음과 같이 주석으로 표현하면 된다.

/*
 * 리턴값 : int
 *		저장된 레코드의 ID를 표현하는 정수
 * 발생 가능한 익셉션 :
 *		openDatabase()부터 호출하지 않으면
 *		DatabaseNotOpenedException이 발생한다.
 */
int saveRecord(Record& record);

 

함수와 관련된 모든 사항을 일정한 형식에 맞게 작성하도록 정해두기도 한다.
예를 들어 saveRecord() 메서드에 대한 주석을 다음과 같이 작성할 수 있다.

/*
 * saveRecord()
 * 지정한 레코드를 데이터베이스에 저장한다.
 * 
 * 매개변수 : 
 *		Record& record : 데이터베이스에 저장할 레코드
 * 리턴값 : int
 *		저장된 레코드의 ID를 표현하는 정수
 * 발생 가능한 익셉션 :
 *		openDatabase()부터 호출하지 않으면
 *		DatabaseNotOpenedException이 발생한다.
 */
int saveRecord(Record& record);

하지만 이렇게 표현하는 것은 그리 좋은 방법이 아니다. 첫 번째와 두 번째 줄은 전혀 쓸모없다. 함수 이름만 봐도 알 수 있기 때문이다. 매개변수에 대한 설명 역시 특별히 새로운 정보를 제공하지 않는다. 하지만 이 버전의 saveRecord()는 int라는 제네릭 타입을 리턴하기 때문에 이 타입이 구체적으로 가리키는 대상만큼은 반드시 문서에 남겨야 한다. 참고로 int 대신 RecordID와 같은 구체적인 타입으로 리턴하도록 디자인하는 것이 좋다. 그러면 리턴 타입에 대한 주석을 따로 달 필요가 없기 때문이다. RecordID는 int에 대한 타입 앨리어스(type alias)에 불과하지만 좀 더 많은 정보를 담을 수 있다. 결국 앞에 나온 함수에서 주석으로 명시할 부분은 익셉션뿐이다.

필자가 권장하는 방식으로 주석을 수정하면 다음과 같다.

/*
 * 발생 가능한 익셉션 :
 *		openDatabase()부터 호출하지 않으면
 *		DatabaseNotOpenedException이 발생한다.
 */
int saveRecord(Record & record);

 

결론 : 회사의 코딩 가이드라인에 함수 주석 방식을 따로 정해두지 않았다면 상식적으로 판단해서 작성한다. 이 때 함수의 이름, 리턴값의 타입, 매개변수의 이름 및 타입으로 분명히 드러나지 않는 정보만 주석으로 남긴다.

 

(2) 코드의 알고리즘을 설명하는 주석

다음 코드를 살펴보자. 구현 자체에는 문제가 없지만 구체적으로 무슨 일을 하는지는 쉽게 드러나지 않는다. 여기서 사용한 알고리즘을 예전에 본 사람은 무슨 내용인지 쉽게 파악하겠지만 처음 보는 사람은 이 코드의 작동 방식을 이해하지 못할 수 있다.

void sort(int inArray[], size_t inSize)
{
	for (size_t i = 1; i < inSize; i++)
	{
		int element = inArray[i];
		size_t j = i - 1;
		while (j >= 0 && inArray[j] > element)
		{
			inArray[j + 1] = inArray[j];
			j--;
		}
		inArray[j + 1] = element;
	}
}

 

이때는 코드에서 사용하는 알고리즘과 (반복문의) 불변 속성에 대한 주석을 추가하면 좋다. 여기서 불변 속성이란 루프와 같은 일정한 코드 영역을 실행하는 동안 반드시 만족해야 할 조건을 의미한다. 예를 들어 앞에 나온 코드에 다음과 같이 주석을 추가할 수 있다. 먼저 코드에 적용된 알고리즘을 개략적으로 설명하는 주석을 함수 앞에 달고, 코드 중간에 이해하기 힘든 부분마다 인라인(그때마다 즉시 처리하는) 주석을 달았다.

/*
 * 삽입 정렬(insertion sort) 알고리즘을 구현한다. 이 알고리즘은 주어진 배열을
 * 두 부분으로 나눈다. 하나는 정렬된 부분이고, 다른 하나는 정렬되지 않은 부분이다.
 * 각 원소의 위치는 1부터 시작하며 모든 원소를 차례대로 검사한다. 이 배열의 앞 부분은
 * 모두 정렬된 부분이므로 현재 원소를 삽입할 정확한 지점을 찾을 때까지 각 원소를
 * 하나씩 검사한다. 마지막 원소까지 알고리즘을 수행하고 나면 전체 배열이 정렬된다.
 */
void sort(int inArray[], size_t inSize)
{
	// 위치 1부터 시작해서 모든 원소를 하나씩 검사한다.
	for (size_t i = 1; i < inSize; i++)
	{
		// 불변 속성 : 
		//		0부터 i-1 사이에 있는 원소(경곗값 포함)는 모두 정렬된 상태이다.

		int element = inArray[i];
		// j는 정렬된 부분의 마지막 지점을 가리키며, 그 뒤에 원소를 추가한다.
		size_t j = i - 1;
		// 정렬된 배열에서 현재 위치가 이 원소보다 높다면 오른쪽 자리에 원소를
		// 삽입할 자리를 확보하도록 값을 오른쪽으로 이동한다.
		// (그래서 삽입 정렬이라 부른다.)
		while (j >= 0 && inArray[j] > element)
		{
			inArray[j + 1] = inArray[j];
			j--;
		}
		// 이 시점에서 정렬된 배열의 현재 위치는 현재 원소보다 높지 않다.
		// 따라서 이 자리가 원소의 새 위치가 된다.
		inArray[j + 1] = element;
	}
}

이렇게 하면 이전보다 좀 장황해지지만 정렬 알고리즘을 모르는 사람도 주석만 보고 쉽게 이해할 수 있다.

 

주석 스타일

(1) 문장 단위 주석

다음 문장을 보자.

if (result % 2 == 0)	// 결과에 대한 모듈로 2의 결과가 0이면...

이 문장은 단지 코드를 풀어쓰기만 했다. result 값에 모듈로 2 연산을 적용한 이유에 대해서는 한마디도 없다. 이를 좀 더 개선하면 다음과 같다.

if (result % 2 == 0)	// 결과가 짝수면...

2에 대한 모듈로 연산을 사용한 이유가 결과가 짝수인지 검사하기 위해서라는 정보가 추가로 들어갔다. 물론 프로그래머라면 대부분 굳이 주석을 보지 않아도 충분히 알 수 있는 내용이다.

 

장황하고 불필요한 말이 많아 보이긴 하지만 이해하기 힘든 코드에 과도할 정도로 주석을 많이 달아두면 나름 도움 된다. 다음 코드도 모든 문장마다 주석을 달았지만 이해하는데 상당히 도움 된다.

// doodad 값을 계산한다. 시작값, 끝값, 오프셋값은 "Doodad API v1.6"의
// 96쪽에 있는 테이블에 나와 있다.
result = doodad.calculate(kStart, kEnd, kOffset);
// 계산이 제대로 됐는지 확인하기 위해 결괏값을 현재 프로세서에 맞는 마스크값으로
// 비트 단위 AND 연산을 수행한다("Doodad API v1.6", 201쪽 참조).
result &= getProcessorMask();
// 사용자 필드값은 "Marigold Formula"에 따라 설정한다.
// ("Doodad API v1.6", 136쪽 참조)
setUserField((result + kMarigoldOffset) / MarigoldConstant + MarigoldConstant);

코드의 일부분만 나와 있어서 정확한 문맥을 알기 힘들 수 있음에도 불구하고 각 문장에 나온 주석만 봐도 대략 어떤 일을 하는지 파악할 수 있다. 주석이 없다면 &가 달린 문장과 'Marigold Formula'가 무엇을 의미하는지 도저히 알 수 없다.

 

결론 : 모든 문장마다 주석을 다는 방식이 바람직하지 않을 때가 많지만, 코드가 굉장히 복잡하고 난해해서 굳이 이 방식을 적용해야 한다면 코드를 그대로 번역하지 말고 앞에 나온 예제처럼 코드의 작성 의도를 설명한다.

 

(2) 머리말 주석

파일을 새로 생성할 때 자동으로 머리말 주석이 제일 먼저 나오는 템플릿을 작성하게 하는 개발 환경도 있다. 서브버전(Subversion, SVN)을 비롯한 몇몇 소스 관리 시스템은 메타데이터를 추가하는 기능도 제공한다. 예를 들어 주석에 $ID$란 스트링을 적어두면 그 자리에 작성자, 파일명, 리비전 번호 그리고 작성 일자를 SVN이 넣어준다.
머리말 주석의 예는 다음과 같다.

/*
 * $ID: Watermelon.cpp,123 2004/03/10 12:52:33 march $
 * 
 * 수박의 기본 기능을 구현한다. 모든 단위는 세제곱센티미터 당 씨앗 수로 표현한다.
 * 여기서 적용한 수박 이론은 '수박 처리에 대한 알고리즘'이란 백서에 나온 내용을 따른다.
 * 
 * 이 코드의 저작권: (c) copyright 2017, FruitSoft, Inc. ALL RIGHTS RESERVED
 */

 

(3) 고정 양식 주석

최근에는 주석을 외부 문서화 도구로 처리할 수 있도록 표준 양식에 따라 작성하는 사례가 늘고 있다. C++ 프로그래머는 HTML 기반 문서, 클래스 다이어그램, 유닉스 맨페이지(man page)를 비롯한 여러 가지 유용한 문서를 자동으로 생성해주는 Doxygen(www.doxygen.org)이란 무료 툴을 많이 사용한다.

https://cypsw.tistory.com/entry/Doxygen-%EB%A7%A4%EB%89%B4%EC%96%BC-1-Simple-version?category=896895 

 

Doxygen 사용법 (1) Simple version

1.   Doxygen 이란 소스코드에 대한 설명을, 간단하게 생성할 수 있게 해 주는 프로그램 입니다. 이 설명의 범주에는 다음과 같은 것이 포함됩니다. 1) 클래스 별 관계도 2) 클래스, 함수에 대한 개

cypsw.tistory.com

 

(4) 임의 주석

정해진 형식과 관계없이 필요할 때마다 주석을 달 때가 있다.
이런 주석을 작성할 때는 다음과 같은 가이드라인을 따른다.

  • 주석을 작성하기 전에 굳이 주석을 달 필요가 없도록 코드를 수정할 수 없는지 검토한다. 예를 들어 변수, 함수, 클래스의 이름을 변경하거나, 코드의 처리 순서를 변경하거나, 변수의 이름을 잘 짓는 것만으로도 도움이 될 때가 많다.
  • 코드에 자기 이름을 남기지 않는다. 이러한 정보는 소스 코드 관리 시스템에서 자동으로 관리해준다.
  • API를 사용하는 과정이 명확하지 않다면 사용한 API에 대한 참고 문헌을 남긴다.
  • 코드를 업데이트할 때 반드시 주석도 함께 업데이트한다. 주석이 정확하지 않은 코드만큼 이해하기 힘든 것도 없다. 
  • 한 함수를 여러 부분으로 나누기 위해 주석을 작성할 때 그 함수를 더 작은 단위의 함수로 나눌 수 없는지 검토한다.

 

(5) 주석이 필요 없는 코드

잘 작성된 코드는 대체로 주석이 적다. 좋은 코드는 읽기 쉽다. 모든 문장마다 주석을 달아야 한다면 주석에서 표현하는 내용에 가깝게 코드를 수정할 수 없는지 검토한다. 예를 들어 함수, 매개변수, 변수, 클래스의 역할이 잘 드러나도록 이름을 고친다. const도 적절히 활용한다. 다시 말해 수정하면 안 되는 변수를 const로 지정한다. 함수 안에서 작업을 처리하는 순서도 내부 과정이 명확히 드러나도록 조정한다. 알고리즘을 좀 더 이해하기 쉽도록 이름을 잘 정한 중간 단계의 변수를 추가한다. c++은 일종의 언어다. 주 목적은 컴퓨터가 처리할 일을 알려주는데 있지만 코드를 읽는 이에게 어떤 내용을 전달하는 역할도 한다. 주석이 필요 없는 코드를 작성하기 위한 또 다른 방법은 코드를 더 작은 단위로 분할하는 것이다. 분할은 다음 절에서 자세히 설명한다.

 

결론 : 술술 읽히면서 주석에는 꼭 필요한 정보만 담긴 코드가 좋은 코드다.

 

 

3. 코드 분할

코드 분할이란 코드를 더 작은 단위로 나눠서 작성하는 방식이다. 소스 코드 파일을 열어보니 수백 줄에 달하는 함수들로 가득 차 있고, 수많은 블록이 복잡하게 중첩되어 있는 것을 보면 가슴이 꽉 막히는 기분이 들 것이다. 가장 바람직한 형태는 함수나 메서드마다 한 가지 작업만 수행하는 것이다. 한 작업을 처리하는 데 필요한 부분 작업이 상당히 복잡하면 이를 별도의 함수나 메서드로 분할한다.

 

리팩토링을 통한 코드 분할

프로그래밍을 하다 보면 몰입 상태에 빠진 채 엄청난 속도로 코드를 작성할 때가 있다. 그런데 이렇게 작업한 결과를 보면 의도한 기능은 제대로 동작하지만, 작성된 코드의 스타일은 형편없는 경우가 많다. 프로그래머라면 누구나 한번쯤 이런 경험을 해봤을 것이다. 이렇게 짧은 시간에 엄청난 양의 코드를 작성할 때가 전체 프로젝트 기간 중에서도 가장 생산성이 높은 시점이기도 하다. 하지만 이렇게 작성된 코드를 바탕으로 수정 작업을 하다 보면 금세 코드가 빽빽해진다. 여기에 새로운 요구사항이나 버그 픽스가 들어오면서 기존 코드에 자잘하게 덧붙이는 부분이 늘어난다. 이렇게 자잘한 코드가 누적되면서 원래 세련됐던 코드가 패치나 특수한 경우를 처리하는 코드로 뒤덮이게 되는 것을 흔히 누더기가 됐다고 표현한다.

 

리팩토링이란 코드의 구조를 재조정하는 작업이다. 코드를 리팩토링하는 데 사용하는 기법의 예로 다음과 같은 것들이 있다.

  • 추상화 수준을 높이는 기법
    • 필드 캡슐화 : 필드를 private으로 설정하고, 게터나 세터 메서드로 접근하게 만든다.
    • 타입 일반화 : 코드를 좀 더 공유할 수 있도록 좀 더 일반적인 타입을 사용한다.
  • 코드를 좀 더 논리적으로 분할하는 기법
    • 메서드 추출 : 거대한 메서드를 좀 더 이해하기 쉽도록 일부를 뽑아내서 새로운 메서드로 정의한다.
    • 클래스 추출 : 기존 클래스에 있는 코드 중 일부를 새 클래스로 옮긴다.
  • 명칭과 위치를 개선하는 기법
    • 메서드 및 필드 이동 : 좀 더 적합한 클래스나 소스 파일로 이동한다.
    • 메서드 및 필드 이름 변경 : 목적이 잘 드러나도록 이름을 바꾼다.
    • 올리기 : OOP에서 기본(base) 클래스로 옮기는 기법
    • 내리기 : OOP에서 상속(derived) 클래스로 옮기는 기법

 

코드를 처음부터 읽기 힘든 누더기로 작성했거나 원래는 깔끔했는데 갈수록 누더기로 변했다면 주기적으로 리팩토링해서 임시방편으로 작성한 코드를 제거해야 한다. 리팩토링을 수행할 때 기존 코드를 다시 검토해서 이해하고 유지하기 쉬운 형태로 변환한다. 리팩토링은 코드 분할을 수행할 좋은 기회이기도 하다. 코드의 목적이 변경됐거나 지금껏 한 번도 분할한 적이 없다면 눈을 가늘게 뜨고 코드의 개략적인 모양새를 훑어보면서 더 작은 단위로 분할할 필요가 있는지 판단한다.

 

디자인 기준으로 코드 분할하기

프로그램을 구현할 때 모든 기능을 빠짐없이 코드로 작성하지 말고, 코드 분할 기법을 적용해서 나중에 모듈, 메서드, 함수에서 구현할 부분을 따로 빼놓는 방식으로 작성하면 코드의 밀집도를 낮추고 구조를 좀 더 체계적으로 만들 수 있다.

그리고 당연한 말이지만 곧바로 코드부터 작성하지 말고 먼저 프로그램 디자인부터 해야 한다.

 

 

4. 명명 규칙

c++ 컴파일러는 다음과 같은 명명 규칙을 따른다.

  • 이름의 첫 글자로 숫자가 나올 수 없다. (예 : 9to5)
  • 더블 언더스코어는 특정한 용도로 사용되기 때문에 이름에 넣을 수 없다. (예 : my__name)
  • 언더스코어로 시작하는 이름도 특정한 용도로 사용하기 때문에 쓸 수 없다. (예 : _name, _Name)

 

 

참고

  • 전문가를 위한 c++ 책
저작자표시 비영리 변경금지 (새창열림)

'C, C++ > 전문가를 위한 C++ (책)' 카테고리의 다른 글

전문가를 위한 C++ 4장 : 프로그램 디자인 (1)  (1) 2022.06.14
전문가를 위한 C++ : 2장 스트링 (2)  (0) 2022.06.09
전문가를 위한 C++ : 2장 스트링 (1)  (0) 2022.06.08
전문가를 위한 C++ : 1장 (4)  (0) 2022.06.07
전문가를 위한 C++ : 1장 (3)  (0) 2022.06.06
  • 3장 코딩 스타일
  • 1. 코딩 스타일의 중요성
  • 2. 코드 문서화
  • 3. 코드 분할
  • 4. 명명 규칙
'C, C++/전문가를 위한 C++ (책)' 카테고리의 다른 글
  • 전문가를 위한 C++ 4장 : 프로그램 디자인 (1)
  • 전문가를 위한 C++ : 2장 스트링 (2)
  • 전문가를 위한 C++ : 2장 스트링 (1)
  • 전문가를 위한 C++ : 1장 (4)
종제로
종제로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.