[책요약] 읽기 좋은 코드가 좋은 코드다 4장 미학
2020-05-07
4장 미학
- 좋은 코드는 ‘눈을 편하게’ 해야 한다.
- 빈칸, 정렬, 코드의 순서를 이용하여 읽기 편한 코드를 작성하는 방법을 살펴 볼 것이다.
- 특히 다음과 같은 세가지 원리가 이용된다.
- 코드를 읽는 사람이 이미 친숙한, 일관성 있는 레이아웃을 사용하라.
- 비슷한 코드는 서로 비슷해 보이게 만들어라.
- 서로 연관된 코드는 하나의 블록으로 묶어라.
4.1 미학이 무슨 상관인가?
class StatsKepper {
public:
// 일련의 더블 변수값을 저장하는 클래스
void Add(double d); // 그리고 그런 값들에 대한 간단한 통계를 계산하는 메소드
private: int count; /* 지금까지 몇 개가 저장되었는가
*/ public:
double Average();
private: double minimum;
list<double>
past_items
; double maximum;
}
- 다음 코드보다 위 코드를 이해하는 데 시간이 오래 걸릴 것이다.
// 일련의 더블 변수값을 저장하는 클래스
// 그리고 그런 값들에 대한 간단한 통계를 계산하는 메소드
class StatsKeeper {
public:
void Add(double d);
double Average();
private:
list<double> past_items;
int count; // 지금까지 몇 개가 저장되었는가
double minimum;
double maximum;
}
- 미학적으로 보기 좋은 코드가 사용하기 더 편리하다는 사실은 명백하다
- 잘 생각해보면 여러분이 보내는 시간 대부분은 코드를 그저 바라보는 데 소요된다!
- 코드를 훓어 보는데 걸리는 시간이 적을 수록, 사람들은 코드를 더 쉽게 사용할 수 있다.
4.2 일관성과 간결성을 위해서 줄 바꿈을 재정렬하기
- 다양한 네트워크 연결 속도에 따라서 프로그램 수행동작이 달라지는 방식을 측정하는 자바 코드를 작성한다고 하자
- 생성자에 파라미터 네 개를 받아들이는 TcpConnectionSimulator 클래스가 있다.
- 연결 속도(kbps)
- 평균 대기시간(ms)
- 대기시간의 ‘흔들림’(ms)
- 패킷 손실 (%)
- 이 코드는 다음과 같은 서로 다른 세 개의 TcpConnectionsSimulator가 필요하다
public class PerfomanceTester {
public static final TcpConnectionSimulator wifi = new TcpConnectionSimulator(
500, /* Kbps */
80, /* millisecs 대기시간 */
200, /* 흔들림 */
1 /* 패킷 손실 % */ );
public static final TcpConnectionSimulator t3_fiber =
new TcpConnectionSimulator(
45000, /* Kbps */
10, /* millisecs 대기시간 */
0, /* 흔들림 */
0 /* 패킷 손실 % */ );
public static final TcpConnectionSimulator cell = new TcpConnectionSimulator(
100, /* Kbps */
400, /* millisecs 대기시간 */
250, /* 흔들림 */
5 /* 패킷 손실 % */ );
}
- 이 예제는 코드가 줄당 80글자를 넘으면 안 된다는 제한 조건을 충족하려고 많은 줄 바꿈을 했다.
- 여러분의 회사의 코딩표준에 이런 제한조건이 있다고 가정하자.
- 줄 바꿈은 불행하게도 t3_fiber 코드가 주변 코드와 달라보이게 한다.
- ‘비슷한 코드는 비슷하게 보여야 한다’는 원리에 위배된다.
- 추가적인 줄 바꿈을 도입해서 코드를 일관성 있게 할 수 있다.(그리고 주석의 들여쓰기 위치도 맞추자)
public class PerfomanceTester {
public static final TcpConnectionSimulator wifi =
new TcpConnectionSimulator(
500, /* Kbps */
80, /* millisecs 대기시간 */
200, /* 흔들림 */
1); /* 패킷 손실 % */
public static final TcpConnectionSimulator t3_fiber =
new TcpConnectionSimulator(
45000, /* Kbps */
10, /* millisecs 대기시간 */
0, /* 흔들림 */
0); /* 패킷 손실 % */
public static final TcpConnectionSimulator cell =
new TcpConnectionSimulator(
100, /* Kbps */
400, /* millisecs 대기시간 */
250, /* 흔들림 */
5); /* 패킷 손실 % */
}
- 위 코드는 일관성 있는 패턴을 가지므로 훑어보기 용이하다.
- 하지만 수직방향으로 너무 많은 빈칸을 사용하는 점이 마음에 걸린다.
- 그리고 똑같은 주석도 세 번씩 반복하고 있다.
- 아래는 같은 클래스의 내용을 좀 더 간결하게 작성한 것이다.
public class PerfomanceTester {
// TcpConnectionsSiulator (처리량, 지연속도, 흔들림, 패킷_손실)
// [Kpbs] [ms] [ms] [percent]
public static final TcpConnectionSimulator wifi =
new TcpConnectionSimulator(500, 80, 200, 1);
public static final TcpConnectionSimulator t3_fiber =
new TcpConnectionSimulator(45000, 10, 0, 0);
public static final TcpConnectionSimulator cell =
new TcpConnectionSimulator(100, 400, 250, 5);
}
4.3 메소드를 활용하여 불규칙성을 정리하라
- 다음과 같은 함수를 제공하는 개인적인 데이터베이스가 있다고 하자.
// 'Doug Adams'처럼 간단하게 쓰인 partial_name을 'Mr. Douglas Adams'로 바꾼다.
// 그게 가능하지 않으면, 이유와 함께 'error'가 채워진다.
string ExpandFullName(DatabaseConnection dc, string partial_name, string* error);
- 그리고 이 함수가 다음과 같은 일련의 코드를 이용해서 테스트된다고 하자.
DatabaseConnection database_connection;
string error;
assert(ExpandFullName(database_connection, 'Doug Adams', &error)
== 'Mr. Douglas Adams');
assert(error == '');
assert(ExpandFullName(database_connection, 'Jake Brown', &error)
== 'Mr. Jacob Brown III');
assert(error == '');
assert(ExpandFullName(database_connection, 'No Such Guy', &error) == '');
assert(error == 'no match found');
assert(ExpandFullName(database_connection, 'John', &error) == '');
assert(error == 'more than one result');
- 이 코드는 미학적으로 만족스럽지 않다.
- 어떤 줄은 너무 길어서 다음 줄 까지 이어진다.
- 또한 아름답지 않고 일관성 있는 패턴도 결여되었다.
- 하지만 이 경우에는 코드의 가독성이 줄 바꿈 정도로는 별로 향상되지 않는다.
- ‘assert(ExpandFullName(database_connection,….)’ 과 같은 문자열이 반복되고
- 줄 사이에 ‘error’가 나타나는 점이 더 큰 문제이기 때문이다.
- 이 코드를 향상시키려면 헬퍼 메소드가 필요하다.
CheckFullName('Doug Adams', 'Mr. Douglas Adams', '');
CheckFullName('Jake Brown', 'Mr. Jacob Brown III', '');
CheckFullName('No Such Guy', '', 'no match found');
CheckFullName('John', '', 'more than one result');
void CheckFullName(string partial_name,
string expected_full_name,
string expected_error) {
string error;
string full_name = ExpandFullName(database_connection, partial_name, &error);
assert(error == expected_error);
assert(full_anem == expected_full_name);
}
- 코드가 미학적으로 더 개선되는 걸 의도했지만, 다음과 같이 의도하지 않았던 장점도 있다.
- 중복된 코드를 없애서 코드를 더 간결하게 한다.
- 이름이나 에러 문자열 같은 테스트의 중요 부분들이 한 눈에 보이게 모아졌다.
- 새로운 테스트 추가가 훨씬 쉬워졌다.
- 코드를 ‘보기 예쁘게’ 만드는 작업은 표면적인 개선 이상의 결과를 가져온다는것이 핵심이다.
- 즉 코드의 구조 자체를 개선시킨다.
4.4 도움이 된다면 코드의 열을 맞춰라
- 직선으로 뻗은 끝선과 열은 텍스트를 쉽게 훑어보게 한다.
- 경우에 따라서 ‘열 정렬’로 코드를 더 읽기 쉽게 할 수도 있다.
- 예를 들어 앞 절에서 본 코드에 빈칸을 더 추가해서 CheckFullName()의 시작 열이 딱 맞게 할 수 있다.
CheckFullName('Doug Adams', 'Mr. Douglas Adams', '');
CheckFullName('Jake Brown', 'Mr. Jacob Brown III', '');
CheckFullName('No Such Guy', '', 'no match found');
CheckFullName('John', '', 'more than one result');
- 이렇게 하면 CheckFullName()에 주어지는 두 번째와 세 번째 파라미터가 무엇인지 더 쉽게 구별할 수 있다.
- 다음은 많은 변수가 정의된 간단한 예다.
# POST 파라미터를 지역 변수에 저장한다.
details = request.POST.get('details')
location = request.POST.get('location')
phone = equest.POST.get('phone')
email = request.POST.get('email')
url = request.POST.get('url')
- 세 번째 줄을 보면 request가 아니라 equest라는 잘못된 변수명을 사용하고 있다.
- 코드의 열이 잘 맞으면 이와 같은 종류의 버그가 금방 눈에 들어온다.
- wget 코드베이스 안에는 100개가 넘는 명령행용 명령어가 다음과 같이 나열되어 있다.
commands[] = {
...
{'timeout', NULL, cmd_spec_timeout},
{'timestamping', &opt.timestamping, cmd_boolean},
{'tries', &opt.ntry, cmd_number_inf},
{'useproxy', &opt.use_proxy, cmd_boolean},
{'useragent', NULL, cmd_spec_useragent},
- 이렇게 하면 명령어를 찾아 열에서 열로 이동하기가 매우 쉽다
반드시 열 정렬을 사용해야 하는가?
- 열의 끝선은 쉽게 훑어볼 수 있는 ‘시각적’ 손잡이’를 제공한다.
- 이는 ‘비슷한 코드는 비슷하게 보여야 한다’는 원리에 해당하는 좋은 예다.
- 하지만 어떤 프로그래머는 열 정렬을 좋아하지 않는다.
- 열을 정렬하려면 노력이 필요하기 때문이다.
- 또 다른 이유는 이렇게 수정해서 ‘diff’를 수행하면 필요 이상으로 광범위한 결과가 보고된다.
- 한 줄을 바꾸려는 수정이(대부분 빈 칸의 정렬 때문에) 다섯 줄에 해당하는 수정을 낳을 수 있는 것이다.
- 하지만 이러한 정렬을 일단 시도해보라
- 이런 정렬을 수행하는 일이 프로그래머들이 두려워할 정도로 어렵지는 않다.
- 정 불편하다 느껴지면 그때 그만둬도 상관없다.
4.5 의미 있는 순서를 선택하고 일관성 있게 사용하라
- 코드의 순서가 코드의 정확성에 아무런 영향을 미치지 않는 경우가 많다.
- 다음의 변수 다섯 개는 어떤 순서로 정의되어도 상관이 없다.
details = request.POST.get('details')
location = request.POST.get('location')
phone = request.POST.get('phone')
email = request.POST.get('email')
url = request.POST.get('url')
- 이런 경우 임의의 순서가 아니라, 의미 있는 순서로 나열하는 편이 낫다.
- 다음은 이러한 과정에 도움이 될 만한 사실이다
- 변수의 순서를 HTML폼에 있는
<input>
필드의 순서대로 나열하라 - ‘가장 중요한 것’에서 시작해서 ‘가장 덜 중요한 것’까지 순서대로 나열하라
- 알파벳 순서대로 나열하라
- 변수의 순서를 HTML폼에 있는
- 어떤 순서를 사용하든 코드 전반에 걸쳐서 일관된 방식으로 나열해야 한다.
- 다른 곳에서 순서를 바꾸면 혼동을 초래한다.
4.6 선언문을 블록으로 구성하라
- 우리의 뇌는 자연스럽게 그룹과 계층구조를 따라서 동작하므로, 코드를 이런 방식으로 조직하면 코드를 읽는데 도움을 준다
- 다음 코드는 클라이언트에게 서비스를 제공하도록 전면에 배치된 서버를 위한 c++클래스의 메소드 선언문을 포함한다.
class FrontendServer {
public:
FrontendServer();
void ViewProfile(HttpRequest* request);
void OpenDatabase(string location, string user);
void SaveProfile(HttpRequest* request);
string ExtractQueryParam(HttpRequest* request, string param);
void ReplyOK(HttpRequest* request, string html);
void FindFriends(HttpRequest* request);
void ReplyNotFound(HttpRequest* request, string error);
void CloseDatabase(string location);
~FrontendServer();
}
- 이 코드는 끔찍하진 않지만, 코드를 읽는 사람이 메소드 전체를 쉽게 파악하도록 전체적인 레이아웃이 구성되지는 않았다.
- 메소드 전체를 하나의 그룹으로 묶는 대신, 아래와 같이 논리적인 영역에 따라서 여러 개의 그룹으로 나누면 더 좋을 것이다.
class FrontendServer {
public:
FrontendServer();
~FrontendServer();
// 핸들러들
void ViewProfile(HttpRequest* request);
void SaveProfile(HttpRequest* request);
void FindFriends(HttpRequest* request);
// 질의/응답 유틸리티
string ExtractQueryParam(HttpRequest* request, string param);
void ReplyOK(HttpRequest* request, string html);
void ReplyNotFound(HttpRequest* request, string error);
// 데이터베이스 헬퍼들
void OpenDatabase(string location, string user);
void CloseDatabase(string location);
}
- 이 버전이 더 이해하기 쉽다.
- 코드를 구성하는 줄 수는 늘어났지만 더 읽기 쉽기 때문이다
- 네 개의 큰 섹션을 개략적으로 확인한 다음, 필요하면 각 섹션의 자세한 내용을 살펴볼 수 있다.
4.7 코드를 ‘문단’으로 쪼개라
- 일반 텍스트가 여러 개의 문단으로 나뉘어진 데에는 이유가 있다.
- 비슷한 생각을 하나로 묶어서, 성격이 다른 생각과 구분한다.
- 문단은 ‘시각적 디딤돌’ 역할을 수행한다. 문단이 없으면 하나의 페이지 안에서 읽던 부분을 놓치기 쉽다.
- 하나의 문단에서 다른 문단으로의 전지을 촉진시킨다.
- 이 같은 이유로 코드를 여러 ‘문단’으로 분할할 필요가 있다.
- 다음과 같이 하나의 거대한 덩어리 코드를 읽기 좋아하는 사람은 없다.
# 사용자의 이메일 주소를 읽어 들여 시스템에 존재하는 사용자와 매치시킨다.
# 그 다음 해당 사용자와 이미 친구인 사람들의 리스트를 나타낸다.
def suggest_friends(user, email_password):
friends = user.friends()
friend_emails = set(f.email for f in friends)
contacts = import_contacts(user.email, email_password)
contact_emails = set(c.email for c in contacts)
non_friend_emails = contact_emails - friend_emails
suggested_friends = User.objects.select(email__in=non_friend_emails)
display['user'] = user
display['friends'] = friends
display['suggested_friends'] = suggested_friends
return render('suggested_friend.html', display)
- 뚜렷하게 드러나지는 않지만, 이 함수는 구별되는 여러 단계를 거친다.
- 따라서 그러한 줄을 여러 문단으로 나누면 확실히 도움이 된다.
def suggest_friends(user, email_password):
# 사용자 친구들의 이메일 주소를 읽는다
friends = user.friends()
friend_emails = set(f.email for f in friends)
# 이 사용자의 이메일 계정으로부터 모든 이메일 주소를 읽어들인다.
contacts = import_contacts(user.email, email_password)
contact_emails = set(c.email for c in contacts)
# 아직 친구가 아닌 사용자들을 찾는다.
non_friend_emails = contact_emails - friend_emails
suggested_friends = User.objects.select(email__in=non_friend_emails)
# 사용자 리스트를 화면에 출력한다
display['user'] = user
display['friends'] = friends
display['suggested_friends'] = suggested_friends
return render('suggested_friend.html', display)
- 각 문단의 주석 처리를 확인하라. 이는 사용자가 코드를 훑어보는데 도움을 준다.
4.8 개인적인 스타일 대 일관성
- 미학적 선택 중에는 기본적으로 개인적 스타일의 문제로 귀결되는 사안이 있다.
- 예를 들어 블록을 여는 괄호를 어디에 놓는가 하는 경우가 그렇다
class Logger {
...
}
class Logger
{
...
}
- 두 가지 스타일 중에서 어느 하나를 선택한다고 가독성에 실질적인 영향을 주지는 않는다
- 하지만 두 스타일이 뒤섞이면 가독성에 영향을 준다.
- 우리가 했던 프로젝트 중에, 우리 기준에는 ‘잘못된’ 스타일을 사용하는 경우도 많았지만, 일관성 유지가 훨씬 더 중요했으므로 해당 프로젝트의 스타일을 따랐다.
일관성 있는 스타일은 ‘올바른’ 스타일보다 더 중요하다.
4.9 요약
- 누구나 미학적으로 보기 좋은 코드를 읽고 싶어 한다.
- 자신의 코드를 일관성 있게, 의미있는 방식으로 ‘정렬’하여, 읽기 더 편하고 빠르게 만들 수 있다.
- 우리가 논의한 개별적인 테크닉은 다음과 같다
- 여러 블록에 담긴 코드가 모두 비슷한 일을 수행하면, 실루엣이 동일해 보이게 만들어라
- 코드 곳곳을 ‘열’로 만들어서 줄을 맞추면 코드를 한 눈에 훑어보기 편하다
- 코드의 한 곳에서 A, B, C가 이 순서대로 언급되고 있으면, 다른 곳에서 B, C, A와 같은 식으로 언급하지 말라. 의미있는 순서를 정하여 모든 곳에서 그 순서를 지켜라
- 빈 줄을 이용하여 커다란 블록을 논리적인 ‘문단’으로 나누어라.
Comments