[책요약] 읽기 좋은 코드가 좋은 코드다 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 클래스가 있다.
    1. 연결 속도(kbps)
    2. 평균 대기시간(ms)
    3. 대기시간의 ‘흔들림’(ms)
    4. 패킷 손실 (%)
  • 이 코드는 다음과 같은 서로 다른 세 개의 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> 필드의 순서대로 나열하라
    • ‘가장 중요한 것’에서 시작해서 ‘가장 덜 중요한 것’까지 순서대로 나열하라
    • 알파벳 순서대로 나열하라
  • 어떤 순서를 사용하든 코드 전반에 걸쳐서 일관된 방식으로 나열해야 한다.
  • 다른 곳에서 순서를 바꾸면 혼동을 초래한다.

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