Chapter 03. 슬기롭게 주석 사용하기

Yeseong31·2023년 8월 20일
0

자바 코딩의 기술

목록 보기
3/8

훌륭한 코드는 그 자체로 최고의 설명서다. 주석을 추가하기 전에
“주석이 필요없도록 코드를 향상시킬 방법이 없을까?”라고 자문해보자. - 스티브 맥코넬


지나치게 많은 주석 없애기

대부분의 주석은 코드가 전하는 내용을 반복할 뿐이라서 불필요하다.

  • 다음의 코드를 살펴보자.
// 리팩토링 전 Intentory 클래스
class Inventory {

    // 필드(하나만 있음)
    List<Supply> supplies = new ArrayList<>(); // 제품 리스트

    // 메서드
    int countContaminatedSupplies() {
        // TODO: 필드가 이미 초기화되었는지(널이 아닌지) 검증한다

        int contaminatedCounter = 0; // 카운터 
        // 제품이 없으면 변질도 없다는 뜻이다
        for (Supply supply : supplies) { // FOR 시작
            if (supply.isContaminated()) { 
                contaminatedCounter++; // 카운터를 증가시킨다!
            } // 제품이 변질되었으면 IF 끝
        }// FOR 끝

        // 변질된 제품 개수를 반환한다.
        return contaminatedCounter; // 유의해 처리한다!
    }
} // Inventory 클래스 끝
  • 지금의 코드는 주석이 너무 많이 들어 있다.
  • 문서화가 잘 되어 있다고 생각할 수 있지만, 대부분의 주석은 코드가 전하는 내용을 반복할 뿐이다.

주석은 중요한 정보(이유)를 설명할 때에만 유용하다.

  • 주석을 제거해서 코드를 더욱 간결하게 해보자.
    • 코드 한 줄만 읽으면 바로 알 수 있는 주석은 작성하지 말자.
    • 코드 블록의 끝을 의미하는 주석은 작성하지 말자.
    • 클래스 구조(필드와 메서드, 반환)를 강조하는 주석은 작성하지 말자.
    • “카운터 증가”처럼 코드를 바꾸어 설명하는 주석은 작성하지 말자.
    • TODO 주석을 수정하는 대신 문제를 해결할 수 있는 검증 로직을 추가하자.
    • TODO 주석을 수정하는 대신 문제를 수정할 때까지 이슈 트래커에 이슈를 생성하자.
// 리팩토링 후 Intentory 클래스
class Inventory {

    List<Supply> supplies = new ArrayList<>();

    int countContaminatedSupplies() {

        if (supplies == null || supplies.isEmpty()) {
            // 제품이 없으면 오염도 없다는 뜻이다
            return 0;
        }

        int contaminatedCounter = 0;
        for (Supply supply : supplies) { 
            if (supply.isContaminated()) { 
                contaminatedCounter++; 
            } 
        }

        return contaminatedCounter; 
    }
}
  • 이렇게 남게 되는 주석은 코드만 보아서는 드러나지 않는 정보가 들어간 주석뿐이다.


주석 처리된 코드 제거

  • 다음의 코드를 살펴보자.
class LaunchChecklist {

    List<String> checks = Arrays.asList( 
        "Cabin Leak",
        // "Communication",  // 휴스턴과 정말 통신하고 싶은가?
        "Engine",
        "Hull",
        // "Rover",  // 내 생각에는 필요 없는데...
        "OxygenTank"
        //"Supplies"
    );

    Status prepareLaunch(Commander commander) { 

        for (String check : checks) {

            boolean shouldAbortTakeoff = commander.isFailing(check); 

            if (shouldAbortTakeoff) {
                //System.out.println("REASON FOR ABORT: " + item);
                return Status.ABORT_TAKE_OFF; 
            }
        }

        return Status.READY_FOR_TAKE_OFF; 
    }
}
  • 현재 LaunchChecklist에는 몇 개의 코드가 주석 처리되어 있다.
    • 리스트 항목 몇 개가 주석 처리되어 있고, 항목에 붙은 주석에도 주석이 붙어 있다.
    • prepareLaunch()에도 print 문이 주석 처리되어 있다.
    • 이때 print 문 내부에서는 item 변수에 접근하고 있어서 주석을 함부로 지울 수 없다.

주석 처리된 코드는 심각한 문제가 될 수 있다.
코드를 설명하는 주석과는 달리 ‘설명’하는 부분도 없고, 코드에 혼란만 가중시킨다.

문제를 해결하는 방법은 간단하다. 그냥 지우면 된다.

class LaunchChecklist {

    List<String> checks = Arrays.asList( 
        "Cabin Leak",
        "Engine",
        "Hull",
        "OxygenTank"
    );

    Status prepareLaunch(Commander commander) { 

        for (String check : checks) {

            boolean shouldAbortTakeoff = commander.isFailing(check); 

            if (shouldAbortTakeoff) {
                return Status.ABORT_TAKE_OFF; 
            }
        }

        return Status.READY_FOR_TAKE_OFF; 
    }
}

혹여나 삭제한 코드가 미래에 다시 사용될 것 같아도 걱정하지 말자.
Git과 같은 버전 관리 도구를 사용하면 코드 변경 이력을 추적할 수 있다.



주석을 상수로 대체

주석은 코드를 설명하는 데 필요하다. 하지만 코드로 직접 설명하는 것이 훨씬 낫다.

  • 다음의 코드를 살펴보자.
enum SmallDistanceUnit {

    CENTIMETER,
    INCH;

    double getConversionRate(SmallDistanceUnit unit) { 

        if (this == unit) {
            return 1; // 동등 변환률
        }

        if (this == CENTIMETER && unit == INCH) { 
            return 0.393701; // 1센티미터당 인치
        } else {
            return 2.54; // 1인치당 센티미터
        } 
    }
}
  • 현재 예제에서의 주석은 매직 넘버에서 수의 의미를 부여하는 역할로 사용 중이다.
  • 분명 주석이 도움이 되기는 하지만 코드 자체로 더 의미 있게 만들 수 있다.
enum SmallDistanceUnit {

    CENTIMETER,
    INCH;

    static final double INCH_IN_CENTIMETERS = 2.54;
    static final double CENTIMETER_IN_INCHES = 1 / INCH_IN_CENTIMETERS; 
    static final int IDENTITY = 1;

    double getConversionRate(SmallDistanceUnit unit) { 

        if (this == unit) {
           return IDENTITY;
        }

        if (this == CENTIMETER && unit == INCH) { 
            return CENTIMETER_IN_INCHES;
        } else {
            return INCH_IN_CENTIMETERS;
        } 
    }
}
  • 상수를 사용하면 주석 없이도 코드의 의미를 쉽게 파악할 수 있다.

주석을 코드에 임베딩한 것으로 생각해도 된다.



주석을 유틸리티 메서드로 대체

  • 다음의 코드를 살펴보자.
class FuelSystem {

    List<Double> tanks = new ArrayList<>();

    int getAverageTankFillingPercent() {

        double sum = 0;

        for (double tankFilling : tanks) { 
            sum += tankFilling;
        }

        double averageFuel = sum / tanks.size();
        // 정수 백분율로 반올림
        return Math.toIntExact(Math.round(averageFuel * 100));
    }
}
  • 위 예제에서 ‘마지막 명령문’이 무엇을 하는 것인지 쉽게 알아차릴 수 있는 이유는 주석 덕분이다.
  • 하지만 주석 없이도 코드를 더 명확하게 할 수 있다.
class FuelSystem {

    List<Double> tanks = new ArrayList<>();

    int getAverageTankFillingPercent() {

        double sum = 0;

        for (double tankFilling : tanks) { 
            sum += tankFilling;
        }

        double averageFuel = sum / tanks.size();
        return roundedToPercent(averageFuel);
    }
}

static int roundedToPercent(double value) {
	return Math.toIntExact(Math.round(value * 100));
}

주석/변수를 추가하는 방법보다 유틸리티 메서드를 이용하면 몇 가지 장점이 있다.

  1. 코드가 무엇을 하는지 이름만으로 설명 가능하다.
  2. 메서드의 길이가 짧아져서 코드를 더 쉽게 이해할 수 있다.
  3. 다른 메서드에서 새로운 메서드를 재사용할 수 있다. -> 모듈화
  4. 메서드에 계층 구조를 만들 수 있다.
  5. 들여쓰기의 수준/깊이를 줄일 수 있다.


구현 결정 설명하기

코드를 작성하다 보면 어려운 결정을 내려야 할 때가 있다.
주석은 그러한 상황에서 “선택의 근거”로 사용할 수 있다.

  • 다음의 코드를 살펴보자.
class Inventory {

	private List<Supply> list = new ArrayList<>();

	void add(Supply supply) {
	
		list.add(supply);
		Collections.sort(list);
	}

	boolean isInStock(String name) {
		
		// 빠른 구현
		return Collections.binarySearch(list, new Supply(name)) != -1;
	}
}
  • 위 예제에서는 “빠른 구현”을 위해 binarySearch()를 사용한 것을 주석으로 작성했다.
  • 하지만 왜 빨라야 하는지, 빠른 구현으로 인한 트레이드 오프 등은 설명하고 있지 않다.
class Inventory {

	// 리스트는 정렬된 채로 유지한다. isInStock()을 참고한다.
	private List<Supply> list = new ArrayList<>();

	void add(Supply supply) {
	
		list.add(supply);
		Collections.sort(list);
	}

	boolean isInStock(String name) {
		/*
		 * 재고가 남았는지 재고명으로 확인해야 한다.
		 * 하지만 재고가 천 개 이상이 되면 심각한 성능 이슈가 나타날 수 있다.
         * 1초 안에 항목을 추출하기 위해
         * 비록 재고를 정렬된 채로 유지해야 하지만
         * 이진 검색 알고리즘을 쓰기로 결정했다.
         */
		return Collections.binarySearch(list, new Supply(name)) != -1;
	}
}
  • 이와 같이 사용 사례, 우려사항, 해법, 트레이드 오프 등을 명시하면 코드 사용에 대한 이유를 명확히 알 수 있다.

유용한 주석은 아래의 템플릿을 사용하여 간단히 채울 수 있다.

In the context of [USE CASE] [사용 사례]의 맥락에서
facing [CONCERN] 직면하는 [우려사항]
we decided for [OPTION] 우리가 선택한 [해법]으로
to achieve [QUALITY] 얻게 되는 [품질]
accepting [DOWNSIDE] 받아들여야 하는 [단점]



예제로 설명하기

  • 다음의 코드를 살펴보자.
class Supply {
    /**
     * 아래 코드는 어디서든 재고를 식별한다.
     *
     * S로 시작해 숫자 다섯자리 재고 번호가 나오고
     * 뒤이어 앞의 재고 번호와 구분하기 위한 역 슬래시가 나오고
     * 국가 코드가 나오는 엄격한 형식을 따른다.
     * 국가 코드는 반드시 참여 국가인 (US, EU, RU, CN) 중
     * 하나를 뜻하는 대문자 두 개여야 한다.
     * 이어서 마침표와 실제 재고명이 소문자로 나온다.
     */
    static final Pattern CODE = 
        Pattern.compile("^S\\d{5}\\\\(US|EU|RU|CN)\\.[a-z]+$"); 
}
  • 위 예제에서는 복잡한 정규식에 대한 설명을 주석으로 길게 적어놓았다.
  • 언뜩 보기에는 괜찮아 보인다. 코드와 함께 주석이 있으니 말이다.
  • 하지만 문제는 설명서가 덜 정확하고, 숙련된 개발자라면 정규식 코드만 보고도 흐름을 알 수 있다는 데 있다.
class Supply { 
    /**
     * 아래 표현식은 어디서든 재고 코드를 식별한다.
     *
     * 형식: "S<inventory-number>\<COUNTRY-CODE>.<name>"
     *
     * 유효한 예: "S12345\US.pasta", "S08342\CN.wrench",
     * "S88888\EU.laptop", "S12233\RU.brush"
     *
     * 유효하지 않은 예:
     * "R12345\RU.fuel."     (재고가 아닌 자원)
     * "S1234\US.light"      (숫자가 다섯 개여야 함)
     * "S01234\AI.coconut"   (잘못된 국가 코드. US나 EU, RU, or CN 중 하나를 사용해야 함)
     * " S88888\EU.laptop "  (전후로 여백이 있음)
     */
    static final Pattern SUPPLY_CODE = 
        Pattern.compile("^S\\d{5}\\\\(US|EU|RU|CN)\\.[a-z]+$");
}
  • 이렇게 예제를 주석으로 작성하면, 내용은 길어지지만 코드에 대한 더 많은 정보를 제공한다.
    • 시작하는 문장은 이전의 코드와 같지만, 형식: 부분에서 앞 예제의 내용을 한 줄로 요약했다.
    • 그다음으로 구체적인 예제를 작성하여 정규식에 대한 이해를 도왔다.
    • 마지막으로 패턴에 대해 더 의미 있는 이름인 SUPPLY_CODE 변수에 부여했다.


패키지를 JavaDoc으로 구조화하기

JavaDoc은 자바 API가 제공한느 문서화 기능이다.
패키지를 비롯해 코드에서 public인 요소를 설명하는 데 사용한다.

/**
 * 제품 재고를 관리하는 클래스
 *
 * <p>
 * 주요 클래스는 {@link logistics.Inventory}로서 아래를 수행한다.
 * <ul>
 * <li> {@link logistics.CargoShip}으로 선적하고,
 * <li> 변질된 {@link logistics.Supply}를 모두 버리고
 * <li> 이름으로 어떤 {@link logistics.Supply}든 찾는다.
 * 
 * <p>
 * 이 클래스는 제품을 내리고 변질된 제품은 즉시 모두 버릴 수 있게 해준다.
 * <pre>
 * Inventory inventory = new Inventory();
 * inventory.stockUp(cargoShip.unload());
 * inventory.disposeContaminatedSupplies();
 * inventory.getContaminatedSupplies().isEmpty();  // true
 * </pre>
 */
package logistics;
  • 소개문은 패키지 내 클래스로 무엇을 할 수 있는지 매우 짧은 요약을 제공한다.
  • 두 번째 부분은 패키지 내 주요 클래스로 무엇을 할 수 있는지 설명한다.
  • 세 번째 부분에서는 주요 사용 사례를 어떻게 구현하는지 예제를 통해 보여준다.


클래스와 인터페이스를 JavaDoc으로 구조화하기

모든 public 클래스나 인터페이스는 JavaDoc으로 설명해야 한다.
이는 대부분의 자바 프로젝트에 적용되는 규칙이다.

  • 다음의 예제를 살펴보자.
/**
 * 이 클래스는 화물선을 나타낸다.
 * 제품의 {@link Stack}를 내릴 수 있고, 제품의 {@link Queue}를 실을 수 있으며
 * long 타입으로 remainigCapacity를 보여줄 수 있다.
 */
interface CargoShip {

	Stack<Supply> unload();
	Queue<Supply> load(Queue<Supply> supplies);
	int getRemainingCapacity();
}
  • 위 예제에서는 JavaDoc을 사용하여 요약과 클래스 기능애 대한, 상세한 모든 설명을 포함하고 있다.
  • 하지만 요약과 상세 설명이 분리되어 있지 않고 붙어 있다.
  • 또한상세 설명은 단순히 인터페이스의 메서드 서명을 반복할 뿐이다.
    • 심지어 주석을 작성한지 조금 되었는지 getRemainingCapacity()는 더 이상 long을 반환하지 않는다.

그렇다면 인터페이스의 메서드 서명을 반복하지 않는 주석은 어떻게 생겼을까?

  • 다음의 예제를 살펴보자.
/**
 * 화물선은 용량에 따라 제품을 싣고 내릴 수 있다.
 * 
 * <p>
 * 제품은 순차적으로 선적되고 LIFO(Last-In-First-Out) 순으로 내려진다.
 * 화물선은 용량만큼만 제품을 저장할 수 있다.
 * 용량은 절대 음수가 아니다.
 */
interface CargoShip {

	Stack<Supply> unload();
	Queue<Supply> load(Queue<Supply> supplies);
	int getRemainingCapacity();
}
  • 이렇게 하면 JavaDoc 주석 최상단의 요약문이 눈에 더 잘 띈다.
  • 이어지는 설명에서는 코드 동작을 상세히 설명하고 있고, 인터페이스 호출 시 capacity조건을 명시하고 있다.

인터페이스와 퍼블릭 클래스를 위한 훌륭한 JavaDoc 주석은 다음의 형태로 작성하면 좋다.

  • 짧고 간결한 요약으로 시작하기
  • 요약을 클래스나 인터페이스가 보장하는 불변과 수직으로 분리하기
  • 메서드 서명을 되풀이하지 않기
  • 클래스나 인터페이스의 용법을 나타낼 예제 만들기


메서드를 JavaDoc으로 구조화하기

메서드는 객체의 동작을 표현한다.
메서드를 호출하면 상태 변경과 부수 효과가 나타난다.
따라서 다른 어떤 JavaDoc 주석 유형보다 메서드의 JavaDoc 설명이 중요하다.

interface CargoShip {

	Stack<Supply> unload();

    /**
     * 제품을 화물선에 싣는다.
     *
     * <p>
     * 남은 용량만큼만 제품을 싣게 해준다.
     *
     * 예:
	 * <pre>
     * int capacity = cargoShip.getRemainingCapacity();  // 1
     * Queue&lt;Supply> supplies = Arrays.asList(new Supply("Apple"));
     * Queue&lt;Supply> spareSupplies = cargoShip.load(supplies);
     * spareSupplies.isEmpty();  // true
     * cargoShip.getRemainingCapacity() == 0;  // true
     * </pre>
     *
     * @param 적재할 제품; 널이면 안 된다.
     * @return 용량이 작아 실을 수 없었던 제품;
     *          모두 실었다면 empty
     * @throws 제품이 널이면 NullPointerException
     * @see CargoShip#getRemainingCapacity() 용량을 확인하는 함수
     * @see CargoShip#unload() 제품을 내리는 함수
     */
    Queue<Supply> load(Queue<Supply> supplies);

    int getRemainingCapacity();
}

}
  • 위 예제는 설명과 코드 예제로 만든 훌륭한 ‘계약서’이다.
    • <pre>는 XML 환경이기 때문에 <pre> 내부에 위치한 < 문자는 &lt;로 탈출시켰다.
    • null과 같이 유효하지 않은 입력을 @param에 명시하였고, 이를 위반하면 NPE@throw한다고 언급했다.
    • @see 표기로 다른 메서드를 참조하도록 했다.

JavaDoc 주석은 계약서처럼 읽히면 좋다. (계약서가 되어야 한다는 것은 아님)



생성자를 JavaDoc으로 구조화하기

생성자의 JavaDoc 주석은 프로그래머가 생성자를 사용하는 데 필요한 모든 요소를 설명해야 한다.

class Inventory {

	List<Supply> supplies;

	/**
	 * 빈 재고를 생성한다.
	 * 
	 * @see Inventory#Inventory(Collection) 초기 제품을 초기화하는 함수
	 */
	Inventory() {
		this(new ArrayList<>());
	}

    /**
     * 제품을 처음으로 선적한 재고를 생성한다.
     *
     * @param initialSupplies  제품을 초기화한다.
     *                         널이면 안 되고 빌 수 있다.
     * @throws NullPointerException initialSupplies가 널일 때
     * @see Inventory#Inventory() 제품없이 초기화하는 함수
     */
    Inventory(Collection<Supply> initialSupplies) {
        this.supplies = new ArrayList<>(initialSupplies);
    }
}

원하는 대로 동작하려면 어떤 전제 조건을 충족해야 하는지 알아야 한다.

  • 기본 생성자에는 전제 조건과 입력 매개변수가 전혀 없지만, 두 번째 생성자에는 있다.

  • Supply 객체의 CollectioninitialSupplies를 호출하므로 이에 대해서도 설명을 적었다.

    • 예제의 경우, null을 입력하면 안 되고, 입력하면 NPE가 발생한다.
    • 이에 대한 설명은 @throw에 명시했다.
  • 예제의 경우, 두 번째 생성자 종료 후에는 객체 상태 정보를 알아야 한다.

    • 이는 객체 상태에 따라 그 시점에서 호출할 수 있는 메서드가 결정되기 때문이다.
    • 이를 사후 조건(postcondition)이라고 한다.
    • 예제에서는 재고가 ‘빈’ 상태가 되거나 ‘초기 제품’으로 채워진다.
  • @see 표기 주석은 두 생성자의 관계를 설명하는 힌트로 작성되었다.

생성자에는 이름이 없기 때문에 JavaDoc의 역할이 크다.


이 장의 내용은 [자바 코딩의 기술: 똑똑하게 코딩하는 법]의 3장 내용을 정리한 것입니다.

profile
역시 개발자는 알아야 할 게 많다.

0개의 댓글

관련 채용 정보