오류가 없는 프로그램을 작성하는 두 가지 방법이 있는데 사실 세 번째 방법만 통한다. - 앨런 J. 펄리스
버그가 전혀 없는 프로그램(이 있을까 싶지만) 역시 오류에 철저히 대비해야 한다.
class CruiseControl {
static final double SPEED_OF_LIGHT_KMH = 1079252850;
static final double SPEED_LIMIT = SPEED_OF_LIGHT_KMH;
private double targetSpeedKmh;
void setTargetSpeedKmh(double speedKmh) {
if (speedKmh < 0) {
throw new IllegalArgumentException();
} else if (speedKmh <= SPEED_LIMIT) {
targetSpeedKmh = speedKmh;
} else {
throw new IllegalArgumentException();
}
}
}
class CruiseControl {
static final double SPEED_OF_LIGHT_KMH = 1079252850;
static final double SPEED_LIMIT = SPEED_OF_LIGHT_KMH;
private double targetSpeedKmh;
void setTargetSpeedKmh(double speedKmh) {
if (speedKmh < 0 || speedKmh > SPEED_LIMIT) {
throw new IllegalArgumentException();
targetSpeedKmh = speedKmh;
}
}
빠른 실패를 사용하면 메서드 전체를 읽는 데 이해하기 쉬워지고, 들여쓰기도 한 단계 줄어든다.
자바의 예외는 비교적 복잡한 타입 계층 구조를 가진다.
예외를 잡으려면 항상 가장 구체적인 예외 타입을 잡아야 한다.
class TransmissionParser {
static Transmission parse(String rawMessage) {
if (rawMessage != null
&& rawMessage.length() != Transmission.MESSAGE_LENGTH) {
throw new IllegalArgumentException("Bad message received!");
}
String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
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은 자바에서 가장 일반적인 예외 유형이다.Exception의 상위 타입인 Throwable 뿐이다.Throwable을 잡으면 OutOfMemoryError와 같은 VM 오류까지 잡힐 수 있다.Exception과 같이 일반적인 타입을 잡으면 잡아서는 안 될 오류까지 함께 잡힐 위험이 있다.개발자는 체크 예외만 잡고, 일반적인 예외들은 잡으면 안 된다.
class TransmissionParser {
static Transmission parse(String rawMessage) {
if (rawMessage != null
&& rawMessage.length() != Transmission.MESSAGE_LENGTH) {
throw new IllegalArgumentException("Bad message received!");
}
String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
try {
int id = Integer.parseInt(rawId);
String content = rawContent.trim();
return new Transmission(id, content);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Bad message received!");
}
}
}
try ~ catch문에서 Exception을 잡은 부분을 구체적인 예외를 잡도록 수정했다.구체적인 예외를 잡다 보면
catch이 여러 개 작성될 수 있다.
하지만 코드가 늘어난다고 해서 일반적인 예외 유형을 잡는 것이 더 나은 것은 아니다.
버그가 적은 긴 코드가 버그가 많은 짧은 코드보다 항상 낫다.
자바 7부터는 다중 캐치 블록으로 여러 개의 체크 예외를 하나의
catch문으로 잡을 수 있게 되었다.try { ... } catch (NumberFormatException | IOException e) { ... }
class TransmissionParser {
static Transmission parse(String rawMessage) {
if (rawMessage != null
&& rawMessage.length() != Transmission.MESSAGE_LENGTH) {
throw new IllegalArgumentException();
}
String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
try {
int id = Integer.parseInt(rawId);
String content = rawContent.trim();
return new Transmission(id, content);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Bad message received!");
}
}
}
예외 처리는 예외를 잡는 것뿐만 아니라 던지는 것까지 포함한다.
IllegalArgumentException이 발생한다.throw new IllegalArgumentException("Bad message received")처럼 작성하더라도 단순히 메시지만 적어놓은 형태이기 때문에 실제 예외 처리에는 도움이 되지 않는다.class TransmissionParser {
static Transmission parse(String rawMessage) {
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));
}
String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
try {
int id = Integer.parseInt(rawId);
String content = rawContent.trim();
return new Transmission(id, content);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
String.format("Expected number, but got '%s' in '%s'",
rawId, rawMessage));
}
}
}
위 예제의 예외 메시지에는 다음의 템플릿이 사용되었다.
Expected [EXPECTED], but got [ACTUAL] in [CONTEXT]
예외를 직접 처리할 수 없는 경우에는 어떻게 해야 할까?
이럴 때에는 예외를 다시 던지거나 더 일반적인 예외 유형으로 변환해야 한다.
이는 아래 원인 사슬 깨지 않기에서 설명한다.
예외는 또 다른 예외를 발생시킬 수 있다.
예외를 잡았지만 처리할 수 없다면 반드시 다시 던져야 한다.
class TransmissionParser {
static Transmission parse(String rawMessage) {
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));
}
String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
try {
int id = Integer.parseInt(rawId);
String content = rawContent.trim();
return new Transmission(id, content);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
String.format("Expected number, but got '%s' in '%s'",
rawId, rawMessage));
}
}
}
NumberFormatException을 잡았지만 IllegalArgumentException을 던지고 있다.IllegalArgumentException의 스택 추적을 살펴보면, NumberFormatException이 왜 발생했는지, 어떤 코드에서 예외가 발생했는지에 대한 힌트가 없다.class TransmissionParser {
static Transmission parse(String rawMessage) {
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));
}
String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
try {
int id = Integer.parseInt(rawId);
String content = rawContent.trim();
return new Transmission(id, content);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
String.format("Expected number, but got '%s' in '%s'",
rawId, rawMessage), e);
}
}
}
Throwable을 전달하는 생성자도 있다.Throwable을 전달함으로써 예외의 원인을 연관시키고, 그로써 원인 사슬이 만들어진다.catch 블록에서 예외를 던질 때에는 메시지와 잡았던 예외를 즉시 원인으로 전달해야 한다.class TransmissionParser {
static Transmission parse(String rawMessage) {
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));
}
String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
try {
int id = Integer.parseInt(rawId);
String content = rawContent.trim();
return new Transmission(id, content);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
String.format("Expected number, but got '%s' in '%s'",
rawId, rawMessage), e);
}
}
}
IllegalArgumentException의 메시지에 두 번이나 %s로 rawMessage를 넣고 있어서 코드가 중복된다.rawMessage를 예외 메시지로 인코딩하고 있어서 소프트웨어의 최종 사용자에게 어떤 종류의 메시지가 오류를 일으켰는지 알리고 싶은 경우에 추출이 어렵다.class TransmissionParser {
static Transmission parse(String rawMessage) {
if (rawMessage != null
&& rawMessage.length() != Transmission.MESSAGE_LENGTH) {
throw new MalformedMessageException(
String.format("Expected %d, but got %d characters in '%s'",
Transmission.MESSAGE_LENGTH, rawMessage.length()),
rawMessage);
}
String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
try {
int id = Integer.parseInt(rawId);
String content = rawContent.trim();
return new Transmission(id, content);
} catch (NumberFormatException e) {
throw new MalformedMessageException(
String.format("Expected number, but got '%s'", rawId),
rawMessage, e);
}
}
}
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;
}
}
raw 메시지 필드가 들어간 MalformedMessageException을 정의하면 된다.raw 필드를 추출하기만 하면 된다.
message는 이전에 메시지로 원인 설명에서 소개했던 템플릿을 그대로 사용하여 일관성을 유지하고 있다.
프로그램에서 동적 객체를 사용하려면 명시적으로 어떤 타입으로든 변환해야 한다.
변환을 제대로 하지 않으면RuntimeException과 같은 예외가 발생할 수 있다.
class Network {
ObjectInputStream inputStream;
InterCom interCom;
void listen() throws IOException, ClassNotFoundException {
while (true) {
Object signal = inputStream.readObject();
CrewMessage crewMessage = (CrewMessage) signal;
interCom.broadcast(crewMessage);
}
}
}
위 예제는 InputStream의 readObject() 메서드를 호출하여 메시지를 읽는 클래스를 보여주고 있다.
이러한 스트림은 자바에서 매우 유연한 편이다.
하지만 유연성을 약속하는 대신 정의된 타입 없이 Object로 제공되고 있다.
CrewMessage로 강제 형 변환이 잘 되고 있지만 스트림이 다른 타입을 반환하면 문제가 된다.class Network {
ObjectInputStream inputStream;
InterCom interCom;
void listen() throws IOException, ClassNotFoundException {
while (true) {
Object signal = inputStream.readObject();
if (signal instanceof CrewMessage) {
CrewMessage crewMessage = (CrewMessage) signal;
interCom.broadcast(crewMessage);
}
}
}
}
instanceof 연산자를 통해 타입을 검증하고, signal에 대해 변환을 수행하는 식으로 코드를 수정하면 된다.instanceof로 차례대로 검증하면 된다.ClassNotFoundException이 발생한다.ClassNotFoundException은 타입 검증으로 거를 수 없다.프로그램이 외부와 상호작용할 때에는 항상 예상치 못한 입력을 처리할 수 있도록 대비해야 한다.
한 프로그램이 자원을 해제하지 않으면 전체 환경이 망가질 수 있다.
class Logbook {
static final Path LOG_FOLDER = Paths.get("/var/log");
static final String FILE_FILTER = "*.log";
List<Path> getLogs() throws IOException {
List<Path> result = new ArrayList<>();
DirectoryStream<Path> directoryStream =
Files.newDirectoryStream(LOG_FOLDER, FILE_FILTER);
for (Path logFile : directoryStream) {
result.add(logFile);
}
directoryStream.close();
return result;
}
}
DirectoryStream을 사용하고 close()로 자원을 해제하고 있다.close()가 호출되지 않는다.class Logbook {
static final Path LOG_FOLDER = Paths.get("/var/log");
static final String FILE_FILTER = "*.log";
List<Path> getLogs() throws IOException {
List<Path> result = new ArrayList<>();
try (DirectoryStream<Path> directoryStream =
Files.newDirectoryStream(LOG_FOLDER, FILE_FILTER)) {
for (Path logFile : directoryStream) {
result.add(logFile);
}
}
return result;
}
}
try-with-resources 구문으로 자원을 안전하게 닫을 수 있다.try 뒤의 소괄호 안에서 자원을 열기만 하면 된다.finally 블록을 만들고, 자원을 닫는다.class Logbook {
static final Path LOG_FOLDER = Paths.get("/var/log");
static final Path STATISTICS_CSV = LOG_FOLDER.resolve("stats.csv");
static final String FILE_FILTER = "*.log";
void createStatistics() throws IOException {
DirectoryStream<Path> directoryStream =
Files.newDirectoryStream(LOG_FOLDER, FILE_FILTER);
BufferedWriter writer =
Files.newBufferedWriter(STATISTICS_CSV);
try {
for (Path logFile : directoryStream) {
final String csvLine = String.format("%s,%d,%s",
logFile,
Files.size(logFile),
Files.getLastModifiedTime(logFile));
writer.write(csvLine);
writer.newLine();
}
} finally {
directoryStream.close();
writer.close();
}
}
}
try 전에 자원을 열고 finally 블록 안에서 자원을 닫고 있다.finally로 자원을 닫게 되면, 이전에 살펴보았듯이 중간에 예외가 발생하면 자원이 닫히지 않는다.try-with-resource 구문을 사용하면 문제가 해결된다.class Logbook {
static final Path LOG_FOLDER = Paths.get("/var/log");
static final Path STATISTICS_CSV = LOG_FOLDER.resolve("stats.csv");
static final String FILE_FILTER = "*.log";
void createStatistics() throws IOException {
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();
}
}
}
}
try-with-resources 블록은 여러 자원을 동시에 처리할 수 있다.; 구분하기만 하면 된다.예외는 예외를 의미 있게 처리할 수 있을 때에만 잡아야 한다.
class Logbook {
static final Path LOG_FOLDER = Paths.get("/var/log");
static final String FILE_FILTER = "*.log";
List<Path> getLogs() throws IOException {
List<Path> result = new ArrayList<>();
try (DirectoryStream<Path> directoryStream =
Files.newDirectoryStream(LOG_FOLDER, FILE_FILTER)) {
for (Path logFile : directoryStream) {
result.add(logFile);
}
} catch (NotDirectoryException e) {
}
return result;
}
}
catch 블록이 그러한 상태이다.catch 블록을 마주하면 아무 것도 하지 않아도 되는 것인지 괜히 의심스러울 수 있다.catch 블록에 대한 설명 내지는 힌트가 전혀 없기 때문이다.class Logbook {
static final Path LOG_FOLDER = Paths.get("/var/log");
static final String FILE_FILTER = "*.log";
List<Path> getLogs() throws IOException {
List<Path> result = new ArrayList<>();
try (DirectoryStream<Path> directoryStream =
Files.newDirectoryStream(LOG_FOLDER, FILE_FILTER)) {
for (Path logFile : directoryStream) {
result.add(logFile);
}
} catch (NotDirectoryException ignored) {
// 디렉터리가 없으면 -> 로그도 없다!
}
return result;
}
}
이를 위해 먼저 예외 변수명 e를 ignored로 변경하였다.
ignored라는 이름을 이해해서 catch 블록이 비어 있다고 경고하지 않는다.또한 예외를 왜 무시하는지 주석을 추가했다.
CONDITION -> EFFECT라는 템플릿을 가지고 왜 예외를 던졌는지, 그로 인한 영향을 왜 넘기는지 설명하고 있다.이 장의 내용은 [자바 코딩의 기술: 똑똑하게 코딩하는 법]의 5장 내용을 정리한 것입니다.