훌륭한 코드는 그 자체로 최고의 설명서다. 주석을 추가하기 전에
“주석이 필요없도록 코드를 향상시킬 방법이 없을까?”라고 자문해보자. - 스티브 맥코넬
대부분의 주석은 코드가 전하는 내용을 반복할 뿐이라서 불필요하다.
// 리팩토링 전 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 클래스 끝
주석은 중요한 정보(이유)를 설명할 때에만 유용하다.
// 리팩토링 후 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));
}
주석/변수를 추가하는 방법보다 유틸리티 메서드를 이용하면 몇 가지 장점이 있다.
- 코드가 무엇을 하는지 이름만으로 설명 가능하다.
- 메서드의 길이가 짧아져서 코드를 더 쉽게 이해할 수 있다.
- 다른 메서드에서 새로운 메서드를 재사용할 수 있다. -> 모듈화
- 메서드에 계층 구조를 만들 수 있다.
- 들여쓰기의 수준/깊이를 줄일 수 있다.
코드를 작성하다 보면 어려운 결정을 내려야 할 때가 있다.
주석은 그러한 상황에서 “선택의 근거”로 사용할 수 있다.
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은 자바 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;
모든
public
클래스나 인터페이스는 JavaDoc으로 설명해야 한다.
이는 대부분의 자바 프로젝트에 적용되는 규칙이다.
/**
* 이 클래스는 화물선을 나타낸다.
* 제품의 {@link Stack}를 내릴 수 있고, 제품의 {@link Queue}를 실을 수 있으며
* long 타입으로 remainigCapacity를 보여줄 수 있다.
*/
interface CargoShip {
Stack<Supply> unload();
Queue<Supply> load(Queue<Supply> supplies);
int getRemainingCapacity();
}
getRemainingCapacity()
는 더 이상 long
을 반환하지 않는다.그렇다면 인터페이스의 메서드 서명을 반복하지 않는 주석은 어떻게 생겼을까?
/**
* 화물선은 용량에 따라 제품을 싣고 내릴 수 있다.
*
* <p>
* 제품은 순차적으로 선적되고 LIFO(Last-In-First-Out) 순으로 내려진다.
* 화물선은 용량만큼만 제품을 저장할 수 있다.
* 용량은 절대 음수가 아니다.
*/
interface CargoShip {
Stack<Supply> unload();
Queue<Supply> load(Queue<Supply> supplies);
int getRemainingCapacity();
}
capacity
의 조건을 명시하고 있다.인터페이스와 퍼블릭 클래스를 위한 훌륭한 JavaDoc 주석은 다음의 형태로 작성하면 좋다.
- 짧고 간결한 요약으로 시작하기
- 요약을 클래스나 인터페이스가 보장하는 불변과 수직으로 분리하기
- 메서드 서명을 되풀이하지 않기
- 클래스나 인터페이스의 용법을 나타낼 예제 만들기
메서드는 객체의 동작을 표현한다.
메서드를 호출하면 상태 변경과 부수 효과가 나타난다.
따라서 다른 어떤 JavaDoc 주석 유형보다 메서드의 JavaDoc 설명이 중요하다.
interface CargoShip {
Stack<Supply> unload();
/**
* 제품을 화물선에 싣는다.
*
* <p>
* 남은 용량만큼만 제품을 싣게 해준다.
*
* 예:
* <pre>
* int capacity = cargoShip.getRemainingCapacity(); // 1
* Queue<Supply> supplies = Arrays.asList(new Supply("Apple"));
* Queue<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>
내부에 위치한 <
문자는 <
로 탈출시켰다.null
과 같이 유효하지 않은 입력을 @param
에 명시하였고, 이를 위반하면 NPE
를 @throw
한다고 언급했다.@see
표기로 다른 메서드를 참조하도록 했다.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
객체의 Collection
이 initialSupplies
를 호출하므로 이에 대해서도 설명을 적었다.
null
을 입력하면 안 되고, 입력하면 NPE
가 발생한다.@throw
에 명시했다.예제의 경우, 두 번째 생성자 종료 후에는 객체 상태 정보를 알아야 한다.
@see
표기 주석은 두 생성자의 관계를 설명하는 힌트로 작성되었다.
생성자에는 이름이 없기 때문에 JavaDoc의 역할이 크다.
이 장의 내용은 [자바 코딩의 기술: 똑똑하게 코딩하는 법]의 3장 내용을 정리한 것입니다.