오류가 없는 프로그램을 작성하는 두 가지 방법이 있는데 사실 세 번째 방법만 통한다.
- 앨런J 펄리스
5-1. 빠른 실패
void setTargetSpeedKmh(double speedKmh) {
if (speedKmh < 0) {
throw new IllegalArgumentException();
} else if (speedKmh <= SPEED_LIMIT) {
targetSpeedKmh = speedKmh;
} else {
throw new IllegalArgumentException();
}
}
불필요하게 분기가 세번으로 나누어져 있다. 아래와 같이 매개변수 검증부분과 일반 로직부분을 분리해야 명확하다.
void setTargetSpeedKmh(double speedKmh) {
if (speedKmh < 0 || speedKmh > SPEED_LIMIT) {
throw new IllegalArgumentException();
}
targetSpeedKmh = speedKmh;
}
5-2. 항상 가장 구체적인 예외 잡기
예외를 단순히 가장 큰 개념인 Exception으로 받기 보다는, 구체적인 예외인 NumberFormatException 등으로 받아라. 그 외에 예외(ex. NullPointException)는 숨기기 보다는 예외가 터져서 노출되는게 낫다.
try {
int id = Integer.parseInt(rawId);
String content = rawContent.trim();
return new Transmission(id, content);
} catch (Exception e) {
throw new IllegalArgumentException("Bad message received!");
}
기존 Exception으로 받는 부분을 개선 후에 NumberFormatException만 받는다.
try {
int id = Integer.parseInt(rawId);
String content = rawContent.trim();
return new Transmission(id, content);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Bad message received!");
}
5-3. 메시지로 원인 설명
Exception 메시지를 생략하거나 단순 용어를 쓰기보다는 바라는 것, 받은 것, 전체 맥락 등 근본 원인을 추적할 수 있도록 설명을 추가하라.
if (rawMessage != null
&& rawMessage.length() != Transmission.MESSAGE_LENGTH) {
throw new IllegalArgumentException(
String.format("Expected %d, but got %d characters in '%s'",
Transmission.MESSAGE_LENGTH, rawMessage.length(),
rawMessage));
}
5-4. 원인 사슬 깨지 않기
catch문에서 예외를 throw할때 받은 e(Exception)을 추가로 다시 던져서, 메시지가 추적되도록 하라
try {
} catch (NumberFormatException e) {
// BAD! Cause chain interrupted!
throw new IllegalArgumentException(e.getCause());
}
위와 같이 하기보다는 아래와 같이 e 를 추가로 넘기기
try {
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Message", e);
}
5-5. 변수로 원인 노출
필요시 별도 Exception을 추가로 정의하여, 향후 변경에 대비하라.
final class MalformedMessageException extends IllegalArgumentException {
final String raw;
MalformedMessageException(String message, String raw) {
super(String.format("%s in '%s'", message, raw));
this.raw = raw;
}
MalformedMessageException(String message, String raw, Throwable cause) {
super(String.format("%s in '%s'", message, raw), cause);
this.raw = raw;
}
}
5-6. 타입 변환 전에 항상 타입 검증하기
void listen() throws IOException, ClassNotFoundException {
while (true) {
Object signal = inputStream.readObject();
CrewMessage crewMessage = (CrewMessage) signal;
interCom.broadcast(crewMessage);
}
}
아래와 같이 런타임에 형변환을 하려면 해당 객체타입인지 체크하는게 안전하다.
void listen() throws IOException, ClassNotFoundException {
while (true) {
Object signal = inputStream.readObject();
if(signal instanceof CrewMessage){
CrewMessage crewMessage = (CrewMessage) signal;
interCom.broadcast(crewMessage);
}
}
}
5-7. 항상 자원 닫기
자원 누출(IO처리 등)이 발생할 수 있는 부분은 try-with-resources 문으로 묶어서, try블록이 긑나면 알아서 close()되도록 처리하라.
try (DirectoryStream<Path> directoryStream =
Files.newDirectoryStream(LOG_FOLDER, FILE_FILTER)) {
for (Path logFile : directoryStream) {
result.add(logFile);
}
}
5-8. 항상 다수 자원 닫기
try ( open resource1; open resource2 ) {
} // try-with-resources 블록
try (DirectoryStream<Path> directoryStream =
Files.newDirectoryStream(LOG_FOLDER, FILE_FILTER);
BufferedWriter writer =
Files.newBufferedWriter(STATISTICS_CSV)) {
for (Path logFile : directoryStream) {
String csvLine = String.format("%s,%d,%s",
logFile,
Files.size(logFile),
Files.getLastModifiedTime(logFile));
writer.write(csvLine);
writer.newLine();
}
}
5-9. 빈 catch 블록 설명하기
catch블록에서 특별히 할 것이 없다면 e대신 ignored로 명명하고, 왜 무시하는지 주석을 추가하라.
} catch (NotDirectoryException ignored) {
// No directory -> no logs!
}