71. 필요 없는 검사 예외 사용은 피하라
검사 예외(Checked Exception)
컴파일러가 확인하는 예외
비검사 예외(Unchecked Exception)
컴파일러가 확인하지 않는 예외
검사 예외는 프로그램이 실행되기 전에 컴파일러가 확인하는 예외
이런 예외가 발생할 수 있는 코드를 작성할 때는 반드시 예외 처리를 해야 하고, 그렇지 않으면 프로그램이 컴파일되지 않는다
검사 예외의 문제점
검사 예외는 안전한 프로그램을 만드는 데 도움이 될 수 있지만, 과도하게 사용하면 문제가 생긴다
1. 코드가 복잡해진다
// 검사 예외를 사용하는 코드
try {
readFile("데이터.txt");
processData();
saveResults();
} catch (IOException e) {
// 예외 처리 코드
System.out.println("파일 처리 중 오류 발생: " + e.getMessage());
}
여러 개의 검사 예외를 처리해야 한다면 코드는 더 복잡해진다
try {
readFile("데이터.txt"); // IOException 발생 가능
processData(); // DataException 발생 가능
saveResults(); // DatabaseException 발생 가능
} catch (IOException e) {
// 파일 관련 오류 처리
} catch (DataException e) {
// 데이터 처리 오류 처리
} catch (DatabaseException e) {
// 데이터베이스 오류 처리
}
2. Java 8 이후의 스트림과 함께 사용하기 어렵다
// 검사 예외를 던지는 메서드는 스트림에서 직접 사용할 수 없다
List<String> fileNames = Arrays.asList("파일1.txt", "파일2.txt", "파일3.txt");
// 컴파일 오류 발생! - 스트림에서 검사 예외를 처리할 수 없음
fileNames.stream()
.map(this::readFile) // readFile이 IOException을 던진다면
.forEach(System.out::println);
검사 예외를 피하는 방법
방법 1: Optional 사용하기
검사 예외 대신 Optional을 반환하면 예외 처리 없이 코드를 작성할 수 있다
// 검사 예외를 사용하는 코드
public User findUser(Long id) throws UserNotFoundException {
if (!userExists(id)) {
throw new UserNotFoundException("사용자를 찾을 수 없습니다: " + id);
}
return getUserById(id);
}
// 사용 예시
try {
User user = findUser(1L);
System.out.println("사용자 찾음: " + user.getName());
} catch (UserNotFoundException e) {
System.out.println(e.getMessage());
createNewUser(1L);
}
Optional을 사용하면 다음과 같이 바꿀 수 있다
// Optional을 사용하는 코드
public Optional<User> findUser(Long id) {
if (!userExists(id)) {
return Optional.empty();
}
return Optional.of(getUserById(id));
}
// 사용 예시
User user = findUser(1L)
.orElseGet(() -> createNewUser(1L));
System.out.println("사용자: " + user.getName());
방법 2: 메서드 분할하기
검사 예외를 던지는 메서드를 두 개로 나누는 방법도 있다
첫 번째 메서드: 작업이 가능한지 boolean으로 확인
두 번째 메서드: 실제 작업 수행
// 검사 예외를 사용하는 코드
public void transferMoney(Account from, Account to, double amount)
throws InsufficientFundsException {
if (from.getBalance() < amount) {
throw new InsufficientFundsException("잔액 부족");
}
from.withdraw(amount);
to.deposit(amount);
}
// 사용 예시
try {
transferMoney(account1, account2, 1000);
} catch (InsufficientFundsException e) {
System.out.println(e.getMessage());
// 잔액 부족 처리
}
메서드를 분할하면 다음과 같이 바꿀 수 있다
// 메서드를 분할한 코드
public boolean canTransferMoney(Account from, double amount) {
return from.getBalance() >= amount;
}
public void transferMoney(Account from, Account to, double amount) {
// 호출 전에 canTransferMoney로 확인했다고 가정
from.withdraw(amount);
to.deposit(amount);
}
// 사용 예시
if (canTransferMoney(account1, 1000)) {
transferMoney(account1, account2, 1000);
} else {
System.out.println("잔액 부족");
// 잔액 부족 처리
}
검사 예외와 비검사 예외 비교
컴파일러가 확인함
컴파일러가 확인하지 않음
반드시 처리해야 함
처리는 선택사항
API 호출자에게 부담
더 유연한 코드 작성 가능
예: IOException
예: NullPointerException
스트림에서 사용 어려움
스트림에서 사용 가능
어떤 예외를 사용해야 할까?
호출자가 예외 상황에서 복구할 수 있을까?
YES: 옵셔널을 고려해볼 수 있다
NO: 비검사 예외(RuntimeException)를 사용한다
옵셔널로 충분한 정보를 제공할 수 있을까?
YES: 옵셔널을 사용한다
NO: 검사 예외를 고려한다
예시로 배우는 예외 선택
은행 송금 시스템 예시
// 1. 검사 예외 사용
public void transfer(Account from, Account to, double amount)
throws InsufficientFundsException {
if (from.getBalance() < amount) {
throw new InsufficientFundsException("잔액 부족: " + (amount - from.getBalance()));
}
from.withdraw(amount);
to.deposit(amount);
}
// 2. 옵셔널 사용
public Optional<TransferResult> transfer(Account from, Account to, double amount) {
if (from.getBalance() < amount) {
return Optional.empty();
}
from.withdraw(amount);
to.deposit(amount);
return Optional.of(new TransferResult(true, "이체 성공"));
}
// 3. 비검사 예외 사용
public void transfer(Account from, Account to, double amount) {
if (from.getBalance() < amount) {
throw new InsufficientFundsRuntimeException("잔액 부족");
}
from.withdraw(amount);
to.deposit(amount);
}
// 4. 메서드 분할 사용
public boolean canTransfer(Account from, double amount) {
return from.getBalance() >= amount;
}
public void transfer(Account from, Account to, double amount) {
from.withdraw(amount);
to.deposit(amount);
}
🧩 어려웠던 점
처음에는 검사 예외와 비검사 예외의 차이점과 각각의 장단점을 이해하는 데 어려움이 있었다
메서드를 분할하는 방식이 항상 더 좋은 대안인지도 의문이었다
💡 느낀 점
예외 처리는 단순히 오류를 잡아내는 것이 아니라 API 설계의 중요한 부분임을 깨달았다
호출자가 실제로 복구할 수 있는 상황인지, 그리고 그 복구에 필요한 정보를 충분히 제공하고 있는지를 고민하는 것이 좋은 API 설계의 핵심임을 배웠다
Last updated