출시·고도화 중
Java 안내서 · 5/6
Java는 오류를 예외(exception)로 알립니다. 예외는 두 종류로 나뉘는데, 컴파일러가 처리를 강제하는 검사 예외(checked)와 강제하지 않는 비검사 예외(unchecked)입니다. 이 장에서는 두 예외의 차이, try-catch-finally 와 try-with-resources, 직접 예외를 정의하는 방법, 그리고 JUnit으로 코드를 테스트하는 방법을 다룹니다.
모든 예외는 Throwable 을 상속합니다. 그 아래 Exception 계열은 프로그램이 다룰 수 있는 문제이고, Error 계열(OutOfMemoryError 등)은 보통 잡지 않는 심각한 문제입니다.
| 구분 | 예 | 컴파일러 강제 |
|---|---|---|
| 검사 예외 | IOException, SQLException | 잡거나 throws 로 선언해야 함 |
| 비검사 예외 | NullPointerException, IllegalArgumentException | 강제하지 않음 |
| 오류 | OutOfMemoryError, StackOverflowError | 보통 처리하지 않음 |
비검사 예외는 RuntimeException 을 상속합니다. 대체로 잘못된 인자나 상태처럼 프로그래밍 실수를 뜻하고, 검사 예외는 파일이나 네트워크처럼 호출자가 대비해야 하는 외부 실패를 뜻합니다.
package com.example.config;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class ConfigLoader {
// 검사 예외는 throws 로 호출자에게 넘길 수 있다
static String load(Path path) throws IOException {
return Files.readString(path);
}
static int parsePort(String value) {
int port = Integer.parseInt(value); // NumberFormatException(비검사)
if (port < 1 || port > 65535) {
throw new IllegalArgumentException("port out of range: " + port);
}
return port;
}
}예외를 잡으려면 try 블록으로 감싸고 catch 에서 처리합니다. 처리 방식이 같은 예외는 | 로 한 번에 잡을 수 있습니다(서로 상속 관계인 예외는 함께 적을 수 없습니다). catch 는 위에서부터 차례로 검사하므로 구체적인 예외를 먼저 적습니다. finally 블록은 예외가 나든 안 나든 항상 실행됩니다.
try {
String text = ConfigLoader.load(Path.of("app.conf"));
int port = ConfigLoader.parsePort(text.strip());
System.out.println("port = " + port);
} catch (NoSuchFileException | AccessDeniedException e) {
System.err.println("설정 파일에 접근할 수 없습니다: " + e.getFile());
} catch (IOException e) {
System.err.println("설정 파일을 읽지 못했습니다: " + e.getMessage());
} catch (IllegalArgumentException e) { // NumberFormatException 포함
System.err.println("잘못된 포트 값: " + e.getMessage());
} finally {
System.out.println("설정 읽기 시도 끝");
}예외를 잡아서 아무것도 하지 않고 넘기면 문제가 숨어 버립니다. 그 자리에서 처리할 수 없다면 잡지 말고 위로 던지거나, 의미 있는 예외로 감싸서 다시 던지는 편이 좋습니다.
파일, 소켓, 데이터베이스 연결처럼 사용 후 닫아야 하는 자원은 try-with-resources로 다룹니다. 괄호 안에서 연 자원은 블록이 끝날 때 예외가 나더라도 자동으로 close() 됩니다. AutoCloseable 을 구현한 객체라면 무엇이든 쓸 수 있고, 여러 자원을 세미콜론으로 나열하면 연 순서의 반대로 닫힙니다.
static long countErrors(Path log) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(log)) {
return reader.lines()
.filter(line -> line.contains("ERROR"))
.count();
} // reader.close()가 자동으로 호출됨
}도메인에 맞는 예외를 만들면 호출자가 실패 원인을 구분하기 쉬워집니다. 호출자가 반드시 대비해야 하면 Exception 을, 그렇지 않으면 RuntimeException 을 상속합니다. 원래 예외를 cause 로 넘기면 스택 추적에 원인이 함께 남습니다.
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException(long orderId) {
super("order not found: " + orderId);
}
public OrderNotFoundException(long orderId, Throwable cause) {
super("order not found: " + orderId, cause);
}
}JUnit은 Java의 표준 테스트 프레임워크입니다. JUnit 5부터 쓰는 Jupiter API로 테스트를 작성하며, Maven에서는 org.junit.jupiter:junit-jupiter 를 test 범위 의존성으로 추가합니다(Gradle 설정은 앞 장 참고). 테스트 클래스는 src/test/java 의 같은 패키지에 둡니다.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>테스트 메서드에는 @Test 를 붙이고, Assertions 의 메서드로 결과를 확인합니다. assertThrows 는 예외가 발생하는지 검사하고 그 예외를 돌려주므로 메시지까지 확인할 수 있습니다.
package com.example.config;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
class ConfigLoaderTest {
@Test
@DisplayName("올바른 포트는 숫자로 바뀐다")
void parsesValidPort() {
assertEquals(8080, ConfigLoader.parsePort("8080"));
}
@Test
void rejectsOutOfRangePort() {
var e = assertThrows(IllegalArgumentException.class,
() -> ConfigLoader.parsePort("70000"));
assertTrue(e.getMessage().contains("70000"));
}
@ParameterizedTest
@ValueSource(strings = {"0", "-1", "65536"})
void rejectsInvalidPorts(String value) {
assertThrows(IllegalArgumentException.class, () -> ConfigLoader.parsePort(value));
}
}mvn test 나 ./gradlew test 로 실행하면 실패한 테스트와 원인이 보고됩니다. @ParameterizedTest 는 같은 검사를 여러 입력으로 반복할 때 유용합니다. IDE에서는 테스트 메서드 옆의 실행 버튼으로 하나씩 실행하고 디버깅할 수 있습니다.
throws 선언이 강제되고, 비검사 예외는 RuntimeException 을 상속합니다.cause)을 함께 넘겨 추적 정보를 보존합니다.@Test, assertEquals, assertThrows 로 동작을 검증하고 빌드 도구로 실행합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.
첫 댓글을 남겨 보세요.