[클린코드] 4장 주석

wlsh44·2022년 10월 1일
0

클린코드

목록 보기
4/8

사실상 주석은 기껏해야 필요악이다.
프로그래밍 언어 자체가 표현력에 풍부하다면, 아니 우리에게 프로그래밍 언어를 치밀하게 사용해 의도를 표현할 능력이 있다면, 주석은 거의 필요하지 않으리라.
아니, 전혀 필요하지 않으리라.

주석은 정보를 쉽게 전달하려는 목적으로 작성된다.
다만 여기에는 약간의 모순이 있다.
의도가 잘 드러나는 코드인 경우에는 주석이 필요하지 않다.
반면 의도가 잘 드러나지 않은 코드인 경우는 주석이 필요하다기보다 깨끗한 코드의 작성에 실패했을 확률이 높고, 주석이 아닌 리팩토링을 통해 해결해야 한다.

주석은 오래될수록 코드에서 멀어진다. 오래될수록 완전히 그릇될 가능성도 커진다.
이유는 단순하다. 프로그래머들이 주석을 유지하고 보수하기란 현실적으로 불가능하니까.

주석이 담긴 코드를 수정하면 반드시 주석을 수정하거나 지워야 하는 작업을 하게 된다.
그 정도야 하면 된다고 생각을 할 수 있지만 협업을 하다보면 언제나 예기치 못하는 일들이 발생한다.
만약 이를 잘 지킬 수 있다면 객체지향에서 접근 제어자라는 개념이 존재하지 않고 따로 getter setter를 귀찮게 만들 일이 없었을 것이다.

주석은 나쁜 코드를 보완하지 못한다

코드의 품질이 좋지 않아 주석을 추가하게 되면 코드의 품질이 좋아지는 것이 아니라 오히려 떨어지는 경우가 많다.
안그래도 코드의 품질이 좋지 않아 읽기 힘든 상태에서 주석까지 읽어야 하고 오히려 코드가 더 복잡해질 가능성이 있다.
주석을 다는 것이 아닌 코드의 품질을 개선하는 방식을 선택하자.

코드로 의도를 표현하라!

의도를 코드로 표현하다보면 품질이 개선되는 경우가 많다.

// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))

if (employee.isEligibleForFullBenefits())

책에 나온 예제로, 주석을 메서드의 이름으로 의도를 표현한 경우다.

좋은 주석

책에서 생각하는 좋은 주석이다.
중요하다고 생각되는 부분에 볼드체를 해놓았다.

  • 법적인 주석
    저작권, 소유권, 라이센스 정보 등이 포함된다.

  • 정보를 제공하는 주석
    정말로 주석이 필요한 경우가 있다. 개인적으로 정규표현식이 그렇다고 생각한다.

    //최소 8 자, 최소 하나의 문자, 하나의 숫자 및 하나의 특수 문자
    "^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$"

    정규표현식에 정말 익숙한 사람이 아니라면 위 주석 없이 저 표현식을 이해하는데 시간이 굉장히 오래 걸릴 것이다.
    사실 정규표현식은 거의 무조건 주석이 필요하기 때문에 최대한 지양해야된다고 생각한다.

  • 의도를 설명하는 주석
    이 부분은 사실 위에서도 말했지만 코드로 표현하는 것이 제일 좋다.
    최대한 코드로 의도를 전달하자.

  • 의미를 명료하게 밝히는 주석
    책에 나온 예제다.
    이 부분도 나중에 테스트 챕터에서 보겠지만 테스트 하나 당 하나의 논리만을 가지는 것이 좋기 때문에 각각의 테스트로 나누게 되면 주석이 필요가 없어진다.

assertTrue(a.compareTo(a) == 0);	// a == a
assertTrue(a.compareTo(b) != 0);	// a != b
assertTrue(ab.compareTo(ab) == 0);	// ab == ab
...
  • 결과를 경고하는 주석
    책의 예제처럼 특정 테스트가 굉장히 오래 걸려 급할 때는 꺼야 하는 경고문을 줄 수 있다.
    이런 주석은 정말 좋은 주석이라고 생각한다. (Junit에서는 @Ignore로 테스트를 끄면서 경고문을 줄 수 있다.)
// 여유 시간이 충분하지 않다면 실행하지 마십시오.
void testBigFile() {...}
  • TODO 주석
    개인적으로 자주 쓰는 주석이다.
    특히 IDE에서 TODO주석을 달아놓은 부분을 한 눈에 찾아볼 수 있기 때문에 정말 편한 것 같다.
    대신 실제 완성된 코드에서 TODO 주석이 남아있으면 안 된다.

  • 중요성을 강조하는 주석
    중요성을 강조한다는 것은 왜 코드를 이렇게 짰는지에 대한 설명이 들어갈 확률이 높기 때문에 의도를 설명하는 주석이라고 볼 수 있다.
    어떤 문자나 데이터의 형식을 맞출 때 코드로는 설명이 안 되는 경우가 있기 때문에 주석을 달 수 있지만, 개인적으로는 형식이라는 것은 개발 과정에서 사람들끼리 협약을 하기 때문에 formatData 정도의 메서드로만 해놓아도 사람들이 알아챌 수 있지 않을까 생각한다.

  • 공개 API에서 Javadocs
    공개 API의 경우에는 당연히 해야 한다고 생각한다.

책을 처음 읽을 때는 별 생각이 없었는데 블로깅을 위해 다시 보니까 공감이 되지 않는 부분이 좀 있었던 것 같다.
나중에 경험이 쌓이고 다시 읽게 되면 또 다시 공감이 될까 궁금하기도 하는 부분이다.🙂

나쁜 주석

  • 주절거리는 주석
    의도를 잘 전달하지 못하는 주석이 이렇게 될 가능성이 높다.
    또한 코드를 작성을 미뤄두고 주석만 작성한 경우에도 이렇게 될 가능성이 높다.
    이런 경우에는 꼭 TODO주석을 사용하자.

  • 같은 이야기를 중복하는 주석

  • 오해할 여지가 있는 주석
    이런 주석은 당연히 있어서는 안 된다.

  • 의무적으로 다는 주석
    42서울을 할 때 모든 파일에 42헤더를 넣어야 하는 규칙이 존재했다.

    사실 이 정도는 약과이긴 하지만 지금도 저 주석의 역할이 뭔지 모르겠다.
    그저 과제를 내기 위해 의무적으로 달았던 기억이 난다.

  • 있으나 마나 한 주석
    너무나 당연한 사실을 설명하는 주석이 여기에 해당한다.
    이런 주석이 있다면 빨리 제거하자.

  • 함수나 변수로 표현할 수 있으면 주석을 달지 마라
    위에서 계속 했던 이야기다.
    의도는 코드로 전달하자.

  • 위치를 표시하는 주석
    소스 파일의 특정 위치를 표시하거나 섹션을 나누는 주석을 말한다.
    이런 경우 아마 객체지향적으로 설계가 되지 않아 발생했을 확률이 높다.
    설계를 다시 해야되는 상황이 아닌지 고민해보자.

  • 닫는 괄호에 다는 주석
    아래와 같은 주석을 말한다.
    개인적으로 이 책에서 처음 봤기 때문에 이런 주석을 사용한 사람이 있었다는게 흥미로웠다. 🤔

try {
	while (...) {
    	...
    } //while
} //try
  • 공로를 돌리거나 저자를 표시하는 주석
    VCS로 해결하자

  • 주석으로 처리한 코드
    멘토링하던 도중 리팩토링할 때 이런 식으로 처리한 경우가 많았다.
    당연히 PR때 이런 주석은 지우는게 좋다는 피드백을 받았다. 😂
    이 또한 VCS가 있으니 겁먹지 말고 이전 코드는 지워버리자

  • HTML 주석
    백엔드를 진로로 잡은 이유 중 하나가 바로 html과 css의 장황하고 긴 코드를 보기 싫어서이다.
    여기에 주석까지 달려있으면 정말 끔찍할 것 같다.
    여담으로 스프링을 쓸 때 사용하는 thymeleaf는 html 주석이 적용되지 않아 애를 먹었던 기억이 있다.
    html 주석은 여러모로 사람을 힘들게 하는 것 같다.

  • 전역 정보
    세부적인 코드 구현에 전역 정보를 담은 주석을 적으면 안 된다.
    세부 코드는 전역 정보를 통제하지 못하기 때문이다.

  • 너무 많은 정보
    코드만큼이나, 또는 코드보다 주석이 더 많은 경우 당연히 가독성을 떨어트릴수밖에 없다.

  • 모호한 관계
    주석과 코드간의 관계가 명확해야 한다.
    주석으로 코드를 이해할 수 없다면 당연히 나쁜 주석이다.

  • 함수 헤더
    책에서도 나와있듯, 짧은 함수는 설명이 필요 없다.
    함수가 짧으면서 한 가지 일만 하면 당연히 주석도 필요 없어진다.

예제

책에 있는 예제이며, 내 방식대로 리팩토링을 진행해볼 예정이다.

/**
 * 이 클래스는 사용자가 지정한 최대 값까지 소수를 생성한다. 사용된 알고리즘은
 * 에라스토테네스의 체다.
 * <p>
 *     에라스토테네스: 기원전 276년에 리비아 키레네에서 출생, 기원전 194에 사망
 *     지구 둘레를 최초로 계산한 사람이자 달력에 윤년을 도입한 사람.
 *     알렉산드리아 도서관장을 역임.
 * </p>
 * 알고리즘은 상당히 단순하다. 2에서 시작하는 정수 배열을 대상으로
 * 2의 배수를 모두 제거한다. 다음으로 남은 정수를 찾아 이 정수의 배수를 모두 지운다.
 * 최대 값의 제곱근이 될 때까지 이를 반복한다.
 *
 * @Author Alphonse
 * @version 13 Feb 2002 atp
 */
public class GeneratePrimes {
    /**
     * @param maxValue 는 소수를 찾아낼 최대 값
     */
    public static int[] generatePrimes(int maxValue) {
        if (maxValue >= 2) { // 유일하게 유요한 경우
            //선언
            int s = maxValue + 1; //배열 크기
            boolean[] f = new boolean[s];
            int i;

            // 배열을 참을 초기화
            for (i = 0; i < s; i++) {
                f[i] = true;
            }

            // 소수가 아닌 알려진 숫자를 제거
            f[0] = f[1] = false;

            //체
            int j;
            for (i = 2; i < Math.sqrt(s) + 1; i++) {
                if (f[i]) { // i가 남아 있는 숫자라면 이 숫자의 배수를 구한다.
                    for (j = 2 * i; j < s; j += i) {
                        f[j] = false; // 배수는 소수가 아니다.
                    }
                }
            }

            //소수 개수는?
            int count = 0;
            for (i = 0; i < s; i++) {
                if (f[i]) {
                    count++; // 카운트 증가
                }
            }

            int[] primes = new int[count];

            //소수를 결과 배열로 이동한다.
            for (i = 0, j = 0; i < s; i++) {
                if (f[i]) { // 소수일 경우에
                    primes[j++] = i;
                }
            }

            return primes; //소수를 반환한다.
        } else { //maxValue < 2
            return new int[0]; //입력이 잘못되면 비어 있는 배열을 반환한다.
        }
    }
}

우선 문제점을 파악해보자.

/**
 * 이 클래스는 사용자가 지정한 최대 값까지 소수를 생성한다. 사용된 알고리즘은
 * 에라스토테네스의 체다.
 * <p>
 *     에라스토테네스: 기원전 276년에 리비아 키레네에서 출생, 기원전 194에 사망
 *     지구 둘레를 최초로 계산한 사람이자 달력에 윤년을 도입한 사람.
 *     알렉산드리아 도서관장을 역임.
 * </p>
 * 알고리즘은 상당히 단순하다. 2에서 시작하는 정수 배열을 대상으로
 * 2의 배수를 모두 제거한다. 다음으로 남은 정수를 찾아 이 정수의 배수를 모두 지운다.
 * 최대 값의 제곱근이 될 때까지 이를 반복한다.
 *
 * @Author Alphonse
 * @version 13 Feb 2002 atp
 */

html 주석, 너무 많은 정보, 주절거리는 주석, 저자를 표시하는 주석 등 총체적 난국이다.

/**
* @param maxValue 는 소수를 찾아낼 최대 값
*/

함수를 설명하는 주석이다.
파라미터에 대한 설명은 인수 이름만으로 알 수 있다.

if (maxValue >= 2) { // 유일하게 유요한 경우

조건문과 주석의 설명이 매치가 잘 되지 않는다.
또한 if문 내의 조건은 함수로 빼는 것이 좋아보인다.

//선언
int s = maxValue + 1; //배열 크기
boolean[] f = new boolean[s];
int i;

// 배열을 참을 초기화
for (i = 0; i < s; i++) {
	f[i] = true;
}

// 소수가 아닌 알려진 숫자를 제거
f[0] = f[1] = false;

우선 선언과 같은 있으나 마나 한 주석은 제거해야 한다.
변수, 함수 이름으로 표현 가능한 주석도 보인다.

//체
int j;
for (i = 2; i < Math.sqrt(s) + 1; i++) {
	if (f[i]) { // i가 남아 있는 숫자라면 이 숫자의 배수를 구한다.
		for (j = 2 * i; j < s; j += i) {
			f[j] = false; // 배수는 소수가 아니다.
		}
	}
}

//소수 개수는?
int count = 0;
for (i = 0; i < s; i++) {
	if (f[i]) {
		count++; // 카운트 증가
	}
}

j가 체는 아닐 것이다.
주석과 코드가 멀리 있음을 알 수 있다.

코드로 의도를 표현하지 못해 주석이 계속 들어아고 있음을 알 수 있다.

있으나 마나 한 주석도 존재한다.

//소수를 결과 배열로 이동한다.
for (i = 0, j = 0; i < s; i++) {
	if (f[i]) { // 소수일 경우에
		primes[j++] = i;
	}
}

모든 for 문에 주석이 들어가느라 가독성만 떨어진다.
앞에서도 계속 나오지만 코드로 의도 전달에 실패한 것을 볼 수 있다.

	return primes; //소수를 반환한다.
} else { //maxValue < 2
	return new int[0]; //입력이 잘못되면 비어 있는 배열을 반환한다.
	}
}

주석이 없어도 알 수 있는 정보가 있다.
또 if문이 너무 길어져 else에 대한 정보를 주석으로 표현해야 하는 상황이 발생했다.

else의 리턴 문 또한 코드만으로 의도를 전달받을 수 있다.


주석 외에도 코드 자체에 있는 문제가 많이 보인다.

public class GeneratePrimes {

클래스 명이 동사이면 안 된다.
또한 클래스 명으로 에라스토테네스 체임을 알 수 있어야 한다.

그리고 함수가 하나의 일을 하고 있지 않고 너무 길다.

변수의 이름 또한 그 의미를 잘 보여주지 못한다.

이를 염두에 두고 리팩토링을 해보자

예제 리팩토링

public interface PrimeGenerator {
    int[] generatePrimes(int maxValue);
}

public enum PrimeGenerateAlgorithm {
    SIEVE_OF_ERATOSTHENES;
}

public class PrimeGeneratorFactory {

    public static PrimeGenerator createPrimeGenerator(PrimeGenerateAlgorithm algorithm) {
        switch (algorithm) {
            case SIEVE_OF_ERATOSTHENES -> {
                return new SieveOfEratosthenes();
            }
        }
        throw new IllegalArgumentException();
    }
}

public class SieveOfEratosthenes implements PrimeGenerator {

    public static final int MINIMUM_PRIME_VALUE = 2;

    private int size;
    private boolean[] sieve;

    public int[] generatePrimes(int maxValue) {
        if (!isValidValue(maxValue)) {
            return new int[0];
        }

        size = maxValue + 1;
        sieve = initSieve();

        filterSieve();

        return findPrimes();
    }

    private boolean isValidValue(int maxValue) {
        return maxValue >= MINIMUM_PRIME_VALUE;
    }

    private boolean[] initSieve() {
        boolean[] sieve = new boolean[size];

        for (int i = MINIMUM_PRIME_VALUE; i < size; i++) {
            sieve[i] = true;
        }
        return sieve;
    }

    private void filterSieve() {
        for (int i = MINIMUM_PRIME_VALUE; i < iterLimit(); i++) {
            if (isPrimeNum(sieve[i])) {
                filterMultipleOfPrime(i);
            }
        }
    }

    private int[] findPrimes() {
        int[] primes = initPrimes();

        for (int i = 0, j = 0; i < size; i++) {
            if (isPrimeNum(sieve[i])) {
                primes[j++] = i;
            }
        }
        return primes;
    }

    private int iterLimit() {
        return (int) Math.sqrt(size) + 1;
    }

    private void filterMultipleOfPrime(int primeNum) {
        for (int multipleNum = MINIMUM_PRIME_VALUE * primeNum; 
        	multipleNum < size; multipleNum += primeNum) {
            sieve[multipleNum] = false;
        }
    }

    private int[] initPrimes() {
        int count = countPrimes();

        return new int[count];
    }

    private int countPrimes() {
        int count = 0;

        for (int i = 0; i < size; i++) {
            if (isPrimeNum(sieve[i])) {
                count++;
            }
        }
        return count;
    }


    private boolean isPrimeNum(boolean sieve) {
        return sieve;
    }
}

책에 있는 내용과 다른 점이 몇 가지 있다.
우선 책에서는 맨 처음 알고리즘에 대한 설명과 어떤 알고리즘인지 정도는 남겨 주석을 달아놓았다.
또한 클래스 명을 PrimeGenerator라고 변경했다.

하지만 개인적으로는 클래스의 이름으로 어떤 알고리즘인지 알 수 있어야 하고 PrimeGenerator는 소수를 생성하는 추상 클래스여야 한다고 생각했기 때문에 이 둘을 분리했다.
그리고 팩토리 패턴을 이용해 SieveOfEratosthenes라는 구체 클래스를 만드는 방식을 선택했다.

또한 알고리즘에 대한 설명이 담긴 주석 또한 없어져야 하는게 맞다고 생각했다.
만약 알고리즘이 글로 설명하기 힘들 정도로 길어지면 주석으로 설명할 수 없을 것이 뻔하며 이는 일관성이 사라질 위험이 생긴다.

그리고 책에서는 반복문의 범위를 구하는 함수에 설명을 적어놓았다.
이 부분은 에라스토테네스 체가 아닌 소수를 구할 때의 범위에 대한 정보라고 생각해 제거를 해도 괜찮지 않을까 해서 지우게 됐다.
하지만 이 정도는 있어도 괜찮다고 생각한다.

여튼 그러다보니 주석이 전부 사라졌다...😂

코드
https://github.com/wlsh44/clean-code/tree/main/src/ch4

느낀 점

다른 사람들이 내 코드를 볼 때 쉽게 이해하지 못할 수도 있다고 생각한다.
나도 블로그에 작성하려고 코드를 다시 봤을 때 솔직히 좀 코드가 별로라고 생각하긴 했다...
이는 아직 내가 부족하다는 증거라고 여기고 조금 더 열심히 하려고 노력해야겠다는 것을 느꼈다. 🙂

profile
정리정리

0개의 댓글