내 코드는 깨끗할까? - 클린코드

YeongWoooo·2021년 9월 8일
0

📑Clean Code

들어가며


여러번 협업을 진행하며 코드 정의와 코드가 길어질 수록 코드의 가독성과 유지보수에 대해 의문점이 들어 책을 추천받아 읽어보게 되었습니다. 로버트 C.마틴의 저서 Clean Code: 애자일 소프트웨어 장인 정신을 보고 정리한 내용입니다. 책을 보며 스스로를 반성하게 된 부분도 있었고, 새롭게 알아가는 부분도 있어 저에게 유익하다고 생각해 이번 발표에 다루게 되었습니다👨‍💻 가볍게 개발초보의 독후감을 본다고 생각하고 들어주시면 감사합니다!

저서에서는 이름, 함수, 주석, 형식, 객체와 자료구조, 오류처리, 단위 테스트, 클래스, 시스템, 창발성, 동시성 등 코드의 전체적인 특징을 설명하고, 저자가 생각하기에 올바른 방향을 제시하고 있습니다. 다양한 주제 중 인상깊은 몇 가지만 소개드리려고 해요! 여기 있는 글이 꼭 맞는 말이고는 할 수는 없지만, 이런 방법도 있다는 것을 가볍게 참고하시면 좋을 것 같습니다.

클린 코드? 더티 코드?


더티코드란 쉽게 말해서 유지보수가 힘들고, 눈에 잘 들어오지 않는 코드를 말합니다. 코드는 돌아만가면 되는 것이 아니냐고 생각한다면, 아닙니다. 작은 프로젝트에서는 큰 문제가 되지 않지만 큰 프로젝트나 장기 프로젝트에서는 아주 큰 영향을 끼칩니다.

새로운 프로젝트를 시작할 때, 편집세션을 사용하여 코딩하는 장면을 빨리보기로 시청하면 우리는 코드를 짜는 시간보다 코드를 보는 시간이 10배는 족히 넘는다고 말합니다.(많이 공감되는 부분이네요) 대부분의 프로그래머들이 소비자가 요구하는 기한에 맞추려면 나쁜 코드를 양산할 수 밖에 없다고 하지만, 오히려 멍장진창인 상태로 인해 속도가 곧바로 늦어지고, 결국 기한을 놓치게 됩니다. 기한을 맞추는 방법은 언제나 코드를 깨끗하게 유지하는 습관입니다.

세계적으로 유명한 다양한 개발자들은 클린코드를 각자 우아한 코드, 가독성이 높은 코드, 다른 사람이 고치기 쉬운 코드, 주의 깊게 짠 코드, 중복이 없는 코드, 작게 추상화 한 코드, 읽으면서 놀랄 일이 없는 코드 등 정의를 내리고 있습니다. 이 책에서 Javadoc에서 @author필드에 개발자의 이름을 넣는 것 처럼 코드를 짜는 우리를 '저자'로 표현하고 있고, 저자는 '독자'와 잘 소통할 책임도 있다고 말합니다. 이 책에서는 결국 오브젝트 멘토 진영이 생각하는 깨끗한 코드를 설명합니다.

이름


의도를 명확히 밝히는 이름은 굉장히 중요합니다! 예시를 봅시다!

// 코드가 하는 일을 짐작하기 어렵습니다! 코드의 단순성이 문제가 아니라 함축성이 문제가 되기 때문입니다.
public List<int[]> getThem() {
    List<int[]> list1 = new ArrayList<int[]>();
    for (int[] x: theList)
        if (x[0] == 4)
            list1.add(x);
    return list1;
}

위 같은 코드는 다음과 같은 정보가 드러나지 않습니다.

  • theList에 무엇이 들어있는가.

  • theList에서 0번째 값이 어째서 중요한가?

  • 값 4는 어떤 의미인가?

  • 함수가 반환하는 리스트 list1을 어떻게 사용한느가?

    이를 다음과 같은 코드로 바꿔보겠습니다.

public List<int[]> getFlaggedCells() {
    List<int[]> flaggedCells = new ArrayList<int[]>();
    for (int[] cell: gameBoard)
        if (cell[STATUS_VALUE] == FLAGGED)
            flaggedCells.add(cell);
    return flaggedCells;
}

코드의 단순성은 변하지 않았지만, 게임판에서 각 셀에 대한 플래그를 표시하고, 배열에서 0번째 값은 칸 상태를 뜻한다는 것을 알 수 있습니다. 각 개념에 이름만 붙여도 이런 효과를 볼 수 있습니다! 물론 더 나아진 코드를 작성할 수 있습니다. int[]대신 Cell이라는 클래스로 선언해도 되겠고, cell[STATUS_VALUE] == FLAGGED구문을 cell.isFlagged()라는 함수를 통해 더 명시적으로 선언해도 되겠습니다.

그리고 서로 비슷한 이름을 사용하지 않도록 합니다! 한 모듈에서 XYZControllerForEfficientHandlingOfStrings라는 이름과 XYZControllerForEfficientStrorageOfStrings라는 이름을 사용한다면 혼동이 올 것이기 때문입니다.

하지만 유사한 개념은 유사한 표기법을 사용합니다. 이것도 정보이기 때문입니다. 일관성이 떨어지는 표기법은 그릇된 정보입니다. 유사한 표기법을 사용한다면, 코드 자동완성 기능을 통해 개발속도가 훨씬 빨라지는 것을 경험할 수 있습니다!

// 이런 코드는 그릇된 정보를 담고 있습니다... 1과 l, 0과 O
let a = l;
if ( O === l )
    a = 'O1';
else
    l = '01';

그리고 의미 있는 구분을 해야합니다. 인터프리터를 통과하겠다는 마음으로만 변수명을 작성하는 방식은 옳지 않습니다! 예를 들어 한 모듈안에 function copyChar(a, b)...이런식의 연속적인 숫자나 문자를 붙이는 것은 정보를 제공하는 이름도 아니며, 저자의 의도가 드러나지 않습니다! 차라리 ab를 각각 sourcedestination으로 고쳐주는 것이 훨씬 좋습니다.

그리고 발음하기 쉬운 이름을 사용해야 합니다. 만약 오프라인으로 만나서 코드에 대해서 이야기를 나누는 것도 사회적인 활동이기 때문입니다. 다음은 틀린 예와 옳은 예입니다.

// X
const DtaRcrd102 = {
    genymdhms: 'Date', // 제니엠디에이치엠에스?
    modymdhms: 'Date',
    pszqint: '102' //프스즈퀸트?
}

// O
const Customer = {
    generationTimeStamp: 'Date',
    modificationTimeStamp: 'Date',
    recordId: '102'
}

검색하기 쉬운 이름을 사용해야 합니다. 문자 하나를 사용하는 이름이나 상수는 텍스트 코드에서 찾아내기 힘듭니다. e라는 변수명 대신 addButtonEvent같은 긴 이름을 사용하는 것이 훨씬 검색에 좋습니다.

클래스명은 명사나 명사구가 적합합니다. Customer, WikiPage, Account같은 단어가 좋은 예입니다. Manager, Processor, Data ...등의 단어는 피하고, 동사는 사용하지 않습니다. 그에 반해 메서드명은 동사나 동사구가 적합합니다. postPayment, deletePage, save등이 좋은 예입니다. 이때 한 개념에 한 단어를 사용하는 것이 중요합니다. 같은 기능을 하는 메서드인데 클래스마다 fetch, retrieve, get으로 각각 정의했다면 혼란스럽습니다. 그렇다면 불러오는 클래스 소스코드를 봐야하고, 그것을 살피느라 시간을 소모해야합니다.

함수

함수를 만드는데 여러가지 규칙이 있습니다. 그중 첫 번째 규칙은 '작게!'입니다. 함수를 만드는 두 번째 규칙은 '더 작게!!!!!'입니다. 일반적으로 함수에서 if문/else문/while에 들어가는 블록은 한줄이어야 한다는 의미 입니다. 만약 조건문 블록에 몇 줄이 추가되어야 한다면, 그것을 함수로 만들고 호출하는 형식으로 진행되어야 합니다.(물론 이때 만드는 함수의 이름은 잘 지어야합니다!!) 이렇게 된다면 함수는 짧아지고, 이해는 더 쉬워지는 코드가 완성이 됩니다. 또한 중첩 if문이 들어가면 안됩니다. 허용할 수 있는 범위는 2단까지!!

if(true) {
    // 가능!
    if(true) {
        // Umm...
        if(true) {
            // 불가능!
        }
    }
}

위와 같은 함수를 만드려면 기능을 엄청 쪼개야한다는 생각이 듭니다. 맞습니다! 함수는 한 가지, 딱 한가지 기능만 해야합니다. 그리고 그 딱 한가지를 잘해야합니다. 기준은 각자 추상화의 기준에 따라서 다릅니다. 여러 함수를 모아 한 레이아웃을 렌더링 한다던지, 한 개의 함수를 받아 재정의해서 리턴한다던지 본인의 기준에 맞게 딱 한가지의 기능만 구현하면 됩니다. (그렇다고 한 개의 함수에서 여러가지를 처리하면 안됩니다! ex. 알림도 보내고~ 삭제도 하면서~ 조회도 하고~) 책에서 제시하는 '한 가지'만 하는지 판단하는 방법은 다음과 같습니다.

단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출해낼 수 있다면, 그 함수는 여러 작업을 하는 셈이다.

코드는 위에서 아래로 이야기처럼 읽혀야 좋습니다. 즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아지는 것입니다. 이것을 저자는 '내려가기 규칙'이라고 합니다. 다르게 표현하자면 다음의 TO문단을 읽듯이 프로그램이 읽혀야 합니다.

TO 설정 페이지와 해제 페이지를 포함하려면, 설정 페이지를 포함하고, 테스트 페이지 내용을 포함하고, 해제 페이지를 포함한다.
	TO 설정 페이지를 포함하려면, 슈트이면 슈트 설정 페이지를 포함한 후 일반 설정 페이지를 포함한다. 
	TO 슈트 설정 페이지를 포함하려면, 부모 계층에서 "SuiteSetUp"페이지를 찾아 include 문과 페이지 경로를 추가한다.
	TO 부모 계층을 검색하려면
	...

함수에서 이상적인 인수(파라미터) 개수는 0개(무항)입니다. 다음은 1개이고(단항), 다음은 2개(이항)이다. 3개(삼항)은 가급적 피하는 것이 좋고, 4개 이상은 특별한 이유가 필요합니다. 하지만 특별한 이유가 있어도 사용하면 안됩니다! 기본적으로 인수는 함수의 개념을 이해하기 어렵게 만들지만, 테스트 관점에서 봤을 때 더 어렵기 때문입니다. 갖가지 인수를 채우기 위해 테스트 케이스를 만드는 것은 굉장히 부담스러운 일입니다. 또한 출력인수도 금지입니다. 다음은 출력인수의 예입니다.

appendFooter(s); //S라는 인수를 footer에 추가하는지 s에 footer를 추가하는지 알 수 없다.
report.appendFooter() // report를 footer에 추가하는 것을 알 수 있다.

또한 함수 내에서 오류코드(if문)를 표현식으로 구분하기 쉬운 탓에 많이 사용합니다. 하지만 이 방법은 오류코드를 반환하면 호출자는 오류코드를 곧바로 처리해야 한다는 문제에 부딪힙니다. 반면 예외코드(try-catch구문)을 사용하면 코드가 훨씬 간편해집니다.

// 오류코드 사용
if (deletePage(page) == E_OK) {
    if (registry.deleteRefernce(page.name) == E_OK) {
        if (configKeys.deleteKsy(page.name.makeKey()) == E_OK) {
            logger.log("page deleted");
        } else {
            logger.log("configKey not deleted");
        }
    } else {
        logger.log("deleteRference from registry failed");
    }
} else {
    logger.log("delete failed");
    return E_ERROR;
}

// 예외코드 사용
try {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
} catch (Exception e) {
    logger.log(e.getMessage());
}

// Try-Catch 블록 뽑아내기하면 더 좋겠죠??

오류처리도 한가지 작업만 해야합니다. 오류처리 또한 함수에 속하기 때문입니다.

중복은 모든 소프트웨어에서 악의 근원입니다. 많은 원칙과 기법이 중복을 없애거나 제어할 목적으로 나왔습니다. 관계형 데이터베이스에서는 정규형식을, 객체지향 프로그래밍에서는 코드를 부모 클래스로 몰아 중복을 없앱니다. 고로, 다양한 함수에서 똑같이 사용되는 알고리즘들은 별도의 함수로 빼는 것이 좋습니다.

주석

나쁜코드에 주석을 달지 마라. 새로 짜라. - 브라이언 W. 커니핸, P.J. 플라우거

잘 달린 주석은 그 어떤 정보보다 유용하지만, 경솔하고 근거 없는 주석은 코드를 이해하기 어렵게 만듭니다. 주석이 필요없이 코드에 의도를 충분히 담는 것만으로도 만족 할 수 있습니다. 코드는 항상 변합니다. 파일과 파일 사이를 움직이기도 하고, 나눠지면서, 합쳐지기도 합니다. 불행하게도 주석은 항상 코드를 따라다닐 수 없고, 시간이 지날수록 코드에서 멀어지기 때문입니다! 대부분 주석은 나쁜코드가 있기때문에 추가하는 경우가 많습니다. 이렇게 임시방편으로 주석을 다는 것 보다 코드를 정리하는 것이 더 좋습니다!

어떤 주석은 필요하거나 유익할 수 있습니다! 지금부터 line값을 하는 주석 몇 가지를 소개하려고 합니다. 하지만 최고의 주석은 주석을 달지 않을 방법을 찾아낸 주석이라는 것을 잊지 않아야 합니다.

  • 법적인 주석

    // Copyright (C) 2003, 2004 by Object Mentor, Inc. All rights reserved.
    // GNU General Public License 버전 2 이상을 따르는 조건으로 배포한다.
  • 정보를 제공하는 주석

    // 테스트 중인 Responder 인스턴스를 반환한다.
    protected abstract Responde responderInstance();

    다음과 같은 주석같이 기본적인 정보를 제공하면 편리합니다. 이 주석은 추상메서드가 반환할 값을 설명하고 있습니다. 그래도! 함수 이름을 바꾸면 주석이 필요없어 질 수 있겠죠?

    다음은 더 나은 예제입니다.

    // kk,mm,ss EEE, MMM dd, yyyy 형식이다.
    Pattern timeMatcher = Pattern.compile(
        "\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*");

    이 또한 시각과 날짜를 변환하는 크랠스를 만들 코드를 옮겨주면 더 깔끔해질 수 있을 것입니다.

  • 의도를 설명하는 주석

    때때로 주석은 이해하게 도와주는 선을 넘어 결정에 깔린 의도까지 설명합니다. 예를 들어 두 객체를 비교할 때, 저자는 어떤 객체부다 자기 객체에 높은 순위를 주는지 독자에게 알리고 싶어하게 되는데, 그렇다면 리턴 값이나 메소드에 // 오른쪽 유형이므로 정렬 순위가 더 높다라는 주석을 달 수 있습니다. 또한 주석은 개발하고 있는 현재 기능에 대한 의도를 설명합니다. // 스레드를 대량생산 하는 방향으로 어떻게든 경쟁 조건을 만들려 시도한다. 같은 예가 되겠습니다.

  • 결과를 경고하는 주석

    때로 다른 프로그래머에게 결과를 경고할 목적으로 주석을 사용합니다. 다음은 특정 테스트 케이스를 꺼야하는 이유를 설명하는 주석입니다.

    // 여유 시간이 없으면 실행하면 안됩니다.
    public void _testWithReallyBigFile(){
        대충 시간이 많이 걸리는 코드...
    }
  • TODO 주석

    앞으로 해야할 일을 // TODO: 주석으로 남겨두면 편합니다. VS코드에는 todo주석을 하이라이팅 하고 북마크 해두는 확장 프로그램도 있습니다! 하지만 어떤 용도로 사용하든 시스템에 나쁜 코드를 남겨놓는 핑계가 되어서는 안됩니다.

마치며

아직 더 다루고 싶은 주제가 많지만, 모든 개발환경에서 중요하고 기본적인 세 가지를 다루어 봤습니다!! 개인적으로 공부하며 몰랐던 부분도 알게되고 공감되는 부분도 있어서 재미있게 읽었던 책이었습니다. 기본으로 Java를 다루는 책이었지만 모든 언어와 개발환경에 적용될 수 있는 내용들이었던 것 같습니다.

profile
개발 재밌다!

0개의 댓글