Search
Duplicate
📒

[Java Study] 07-1. 자바 예외 활용, 체크 / 언체크 예외

상태
완료
수업
Java Study
주제
연관 노트
3 more properties
참고

자바 예외 사용법

예외의 잘못된 사용

NOTE
예외처리는 예상치 못한 상황이나 프로그램 실행 중 발생한 오류를 처리하기 위해 설계되었습니다. 즉 예상치 못한 상황이나 실행 중 발생한 오류에 대처하기 위함이지, 제어 흐름의 일부로 사용하면 안됩니다.
try { // 성능이 2배 정도 느리다 int i = 0; while(true) range[i++].climb(); } catch (ArrayIndexOutOfBoundsException e) {}
Java
복사
제어 흐름으로 써버린 경우 - 따라하지 말 것!
try { int value = Integer.parseInt("someString"); } catch (NumberFormatException e) { // NumberFormatException을 잡아서 정상적인 흐름 제어에 사용 }
Java
복사
예외 대신 유효한 정수인지 판단하는 방법을 사용하자.
잘 설계된 API는 클라이언트가 예외를 사용할 필요가 없는 정상적인 제어 흐름을 가져야 합니다. 특정 상태에서 호출할 수 있는 상태 의존적 메서드를 제공하는 클래스는 상태 검사 메서드를 함께 제공함으로써 예외 사용을 피할 수 있습니다.
// 두 메서드 덕분에 표준 for 관용구를 사용할 수 있음 for (Iterator<Foo> i = collections.iterator(); i.hasNext(); ) { Foo foo = i.next(); }
Java
복사
상태 검사 메서드 제공
try { Iterator<Foo> i = collection.iterator(); while(true) { Foo foo = i.next(); ... // 예외를 쓰게되면서 코드가 읽기 힘들어진다. } catch (NoSuchElementException e) { }
Java
복사
상태 검사 메서드 미제공

표준 예외의 활용

NOTE
자바 표준 라이브러리에서 제공하는 표준 예외를 사용하면 코드의 일관성을 유지하고, API 사용자에게 더 친숙하고 이해하기 쉬운 API를 제공할 수 있습니다.
다음은 자주 사용되는 표준 예외입니다:
IllegalArgumentException: 메서드에 부적절한 인수가 전달될 때.
IllegalStateException: 객체가 메서드 호출을 처리할 준비가 되어 있지 않은 상태일 때.
ex) 이미 종료된 상태의 객체에 대한 작업 요청, 메서드 호출 순서가 잘못된 경우
NullPointerException: 프로그램이 null 객체 참조를 잘못 사용할 때.
IndexOutOfBoundsException: 인덱스가 배열, 리스트 또는 다른 데이터 구조의 범위를 벗어날 때.
UnsupportedOperationException: 객체가 요청받은 작업을 지원하지 않을 때.

실패 원자성의 중요성

NOTE
실패 원자성은 연산 도중 예외가 발생하더라도, 해당 연산 이전의 상태를 보존함으로써 프로그램의 일관성을 유지하는 특성을 가리킵니다. 즉 “전부 아니면 전무”의 원칙을 따르며, 오류 발생시 복구가 용이하고 추가적인 오류 처리 로직을 최소화할 수 있습니다.
실패 원자적을 달성하는 방법은 대표적으로 4가지가 있습니다.
1.
불변객체 사용: 불견 객체는 상태가 변하지 않으므로 자연스럽게 실패 원자적입니다. 객체의 상태를 수정할 수 없기에, 연산 중 예외가 발생해도 객체의 상태가 변할 일이 없습니다.
2.
매개변수의 유효성을 검사: 매개변수의 모든 예외 상황을 체크한 후, 마지막에 상태를 변경하면 예외가 발생해도 객체의 상태가 변하지 않습니다.
3.
방어적 복사본 작업 수행: 객체의 상태를 변경하기 전에 임시 복사본에서 모든 작업을 수행하고, 작업이 성공적으로 완료된 후에만 상태를 업데이트 합니다. 예시로 객체의 정렬에서 원본데이터를 배열로 복사하고 정렬을 진행합니다.
4.
롤백 매커니즘 구현: 연산을 수행하는 동안 예외가 발생하면, 이전 상태로 돌아가는 로직을 구현합니다.
// 요소를 제거하고 반환. 이 연산은 실패 원자적이다. public Object pop() { if (size == 0) throw new EmptyStackException(); Object result = elements[--size]; elements[size] = null; // 사용하지 않는 참조 해제 return result; }
Java
복사
2번의 매개변수 예제

고려사항

실패 원자성을 구현할 때는 몇 가지 고려해야 할 점이 있습니다. 방어적 복사본이나 롤백 매커니즘을 사용하면 추가적인 자원이 필요하고 성능이 저하될 수 있으며 코드의 복잡성 또한 증가할 수 있습니다.
실패 원자성은 모든 상황에서 가능하거나 필요한 것은 아니지만 프로그램의 안전성과 신뢰성을 높이는데 크게 기여할 수 있으므로 실패 원자성을 염두에 두면서 개발하는 것이 좋습니다.

try-with-resources

NOTE
제대로 닫힘을 보장하는 수단으로 try-with-resource 문법을 사용하자!
static void copy(String src, String dst) throws IOException { InputStream in = new FileInputStream(src); try { OutputStream out = new FileOutputStream(dst); try { byte[] buf = new byte[BUFFER_SIZE]; int n; while ((n = in.read(buf)) >= 0) out.write(buf, 0, n); } finally { out.close(); } finally { in.close(); } }
Java
복사
try-finall구조 - 각각의 try-final을 작성해야해서 코드가 더럽고 위험하다.
static void copy(String src, String det) throws IOException { try (InputStream in = new FileInputStream(src); OutputStream out = new FileOutputStream(dst)) { byte[] buf = new byte[BUFFER_SIZE]; int n; while ((n == in.read(buf)) > = 0) out.write(buf, 0, n); }
Java
복사
try-with-resource - 훨씬 읽기 좋고, 문제진단에 편하다.
try-with-resource문법은 자바7부터 등장했다.
이러한 문법을 사용하기 위해 AutoCloseable기능을 구현해야 하는데 대부분의 API는 이를 지원한다.

예외 계층

NOTE
자바에서 예외는 기본적으로 잡아서 처리하거나(catch), 처리할 수 없으면 밖으로 던져야 합니다.
자바의 예외 처리 시스템은 Throwable 클래스에서 시작합니다. Throwable 클래스는 모든 예외와 에러의 최상위 클래스로, 이 클래스에서 2개의 하위 클래스인 Error와 Exception으로 나뉩니다.
자바 예외 계층
Error는 시스템 레벨에서 발생하는 심각한 문제를 나타내며, 이러한 에러는 개발자가 처리할 수 없거나 처리해서는 안되는 경우가 대부분입니다. 대표적인 Error는 OutofMemoryError, StackOverflowError가 있습니다.
Exception은 개발자가 처리할 수 있는 예외적인 상황을 나타냅니다. 이는 다시 체크 예외와 언체크 예외로 구분됩니다.
자바의 예외는 try-catch로 잡거나 혹은 throw로 던질 수 있습니다.
catch로 예외를 service에서 잡음
만약 예외를 모두 던진다면? → 예외를 계속해서 throw함 (프로그램 종료)
모든 예외를 끝까지 던지면 다음과 같은 결과가 발생합니다.
자바: main()에서 예외 로그를 출력하고 시스템이 종료됩니다.
웹 애플리케이션: 하나의 예외로 인해 종료되는 것은 피해야 하므로, 해당 예외를 처리하고 오류 페이지를 보여줍니다.

Exception - 체크 예외

NOTE
체크 예외는 컴파일러가 예외 처리를 강제하는 예외입니다. try-cath 혹은 thorw로 무조건 예외에 대한 처리를 해줘야합니다.
public static void main(String[] args) { // try-catch 구조가 필수다! try { String line = readFirstLineFromFile("/path/to/file.txt"); System.out.println(line); } catch (IOException e) { System.out.println("An error occurred while reading the file."); e.printStackTrace(); } } public static String readFirstLineFromFile(String path) throws IOException { try (BufferedReader br = new BufferedReader(new FileReader(path))) { return br.readLine(); } }
Java
복사
IOException은 대표적인 체크 예외다.
체크 예외는 일반적으로 호출하는 쪽에서 복구하리라 여겨지는 경우에 사용합니다.
컴파일러에서 개발자가 예외를 누락하지 않게 할 수 있지만 무조건적으로 처리해야 하고 의존관계에 따른 단점도 존재합니다.
ex) Repository에서 발생한 SQL Exception이 Service에 올라올 수 있습니다.

Runtime Exceptions - 언체크 예외

NOTE
언체크 예외의 경우 프로그램 실행 중 발생할 수 있는 오류를 나타내며, 대부분의 프로그래밍 오류를 나타냅니다. 언체크 예외는 예외처리가 컴파일 시점에 강제되지 않습니다.
int[] numbers = {1, 2, 3}; // 인덱스가 배열 범위를 벗어남 - ArrayIndexOutOfBoundsException System.out.println(numbers[3]);
Java
복사
catch로 잡지 않으면 자동으로 throw해준다.
언체크 예외는 catch-try가 강제되지 않으며 예외 발생시 자동으로 throws처리 해줍니다.
실제 웹개발에서는 대부분 RuntimeException을 통해서 던지며, 대부분 프로그래밍 단계에서 해당 예외를 사용한다.

필요없는 언체크 예외 사용은 피하자

NOTE
체크 예외는 메서드 사용자가 예외를 처리하도록 강제하는 예외 유형으로, API 설계 시 잘못 사용되었을 때 사용성을 크게 저하시킬 수 있습니다.
체크 예외는 이론상으로는 예외 상황에 대처하고 복구할 수 있도록 해주지만, 실제로 대부분의 예외 상황은 개발자가 복구가 불가능하므로 오히려 예외 처리 로직이 과도하게 복잡해질 수 있습니다.
스프링 프레임워크에서는 일반적으로 런타임 예외를 사용하여 예외 처리를 단순화합니다. 특히 ControllerAdvice를 활용해 애플리케이션 전반의 런타임 예외를 일괄적으로 처리할 수 있게 해줍니다.
SQLException을 해결이 불가능한 위치인 Controller, Service에서 의존하게되며 연쇄적으로 던지게된다.
만약 기술변경으로 다른 예외가 터지는경우 전부 수정해줘야 한다.
public class CheckedAppTest { @Test void checked(){ Controller controller = new Controller(); Assertions.assertThatThrownBy(() -> controller.request()) .isInstanceOf(Exception.class); } // 컨트롤러 static class Controller { Service service = new Service(); public void request() throws SQLException, ConnectException { service.logic(); } } // 서비스 (DI객체의 예외가 전파된다.) static class Service{ Repository repository = new Repository(); NetworkClient networkClient = new NetworkClient(); public void logic() throws SQLException, ConnectException { repository.call(); networkClient.call(); } } // 네트워크 static class NetworkClient { public void call() throws ConnectException { throw new ConnectException("연결 실패"); } } // 레포지토리 static class Repository{ public void call() throws SQLException { throw new SQLException("ex"); } } }
Java
복사
예외의 연쇄

언체크 예외(RuntimeException) 활용

NOTE
체크 예외가 발생한경우 언체크 예외로 변경해서 던져주게된다면 예외의 연쇄가 발생하지 않게됩니다.
체크 예외에 있던 예외연쇄 문제를 해결할 수 있다!
public class UnCheckedAppTest { @Test void unchecked(){ Controller controller = new Controller(); Assertions.assertThatThrownBy(() -> controller.request()) .isInstanceOf(Exception.class); } // 컨트롤러 static class Controller { Service service = new Service(); public void request(){ service.logic(); } } // 서비스 => thorw 코드가 없어졌다! static class Service{ Repository repository = new Repository(); NetworkClient networkClient = new NetworkClient(); public void logic(){ repository.call(); networkClient.call(); } } // 네트워크 static class NetworkClient { public void call(){ throw new RuntimeConnectException("연결 실패"); } } // 레포지토리 static class Repository{ public void call() { try{ runSQL(); }catch (SQLException e){ throw new RuntimeSQLException(e); } } public void runSQL() throws SQLException { throw new SQLException("ex"); } } // ConnectionException 체크 예외 => 언체크 예외 static class RuntimeConnectException extends RuntimeException{ public RuntimeConnectException(String message){ super(message); } } // SQLException 체크 예외 => 언체크 예외 static class RuntimeSQLException extends RuntimeException{ public RuntimeSQLException(Throwable cause){ super(cause); } } }
Java
복사
예제 코드

예외 전환 시, 기존 예외

NOTE
체크 예외 ⇒ 언체크 예외로 수정을 한다면 반드시 기존 예외를 포함시켜야 합니다. 기존 예외를 포함하지 않으면 Stack Trace에서 정확한 예외원인을 파악할 수 없습니다.
static class Repository{ public void call() { try { runSQL(); }catch (SQLException e){ // 예외를 포함한다 → StackTrace 정상 출력됨. throw new RuntimeSQLException(e); } } // 예외 발생 메소드 private void runSQL() throws SQLException { throw new SQLException("ex"); } } // SQLException 체크 예외 => 언체크 예외 static class RuntimeSQLException extends RuntimeException{ public RuntimeSQLException(Throwable cause){ super(cause); } }
Java
복사
Repository (체크예외 → 런타임예외로 변경)
@Test void printEx() { Controller controller = new Controller(); try { controller.request(); } catch (Exception e) { // 예외 테스트를 위해 잡는다. log.info("ex",e); } }
Java
복사
테스트 코드
만약 예외변환시 체크 예외를 제대로 포함해서 던져주지 않으면 StackTrace에서 최초의 원인이 되는 Exception을 찾을 수 없게됩니다.
체크 예외를 포함하지 않은 경우
체크 예외를 제대로 포함해서 던져준 경우 SQLException 발견