728x90
예외 계층
1) Error
- 메모리 부족이나 시스템 오류처럼 애플리케이션에서 복구 불가능한 시스템 예외를 말한다
- 시스템에 무엇인가 비정상적인 상황이 발생한 경우에 사용된다
- 주로 자바 가상 머신(JVM)에서 발생시키는 것이며, 예외와 반대로 이를 애플리케이션 코드에서 잡을 수 없다
- 이는 개발자가 미리 예측하여 처리할 수 없기 때문에 예외 처리에 신경 쓰지 않아도 된다
- ex) OutOfMemoryError, ThreadDeath, StackOverflowError 등...
2) Exception
- 체크 예외
- 입력 값에 대한 처리가 불가능하거나 프로그램 실행 중에 참조된 값이 잘못된 경우 등 정상적인 프로그램의 흐름을 어긋나는 것을 의미한다
- 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외이다
- Exception과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다
- Java에서 예외는 개발자가 직접 처리할 수 있기 때문에 예외 상황을 미리 예측하여 핸들링할 수 있다
3) RuntimeException
- 언체크 예외 또는 런타임 예외라고 한다
- 컴파일러가 체크하지 않는 언체크 예외이다
예외 기본 규칙
예외는 잡아서 처리하거나, 처리할 수 없다면 밖으로 던져야 한다
예외를 잡거나 던질 때, 지정한 예외뿐만 아니라 그 예외의 자식들도 함께 처리된다
- 5번에서 예외를 처리하면 이후 애플리케이션 로직은 정상 흐름으로 작동한다
- 만약 예외를 처리하지 못한다면 호출한 곳으로 예외를 계속 던지게 된다
💡 예외를 처리하지 못하고 계속 던지는 경우
- 자바 main() 스레드의 경우, 예외 로그를 출력하면서 시스템이 종료된다
- 웹 애플리케이션의 경우, WAS가 해당 예외를 받아서 처리한다
- 주로 개발자가 지정한 오류 페이지를 사용자에게 보여준다
예외 처리
예외를 처리하는 방법에는 예외 복구, 예외 처리 회피, 예외 전환 방법이 있다
1) 예외 복구
- 예외 상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 방법
- 예외를 잡아서 일정 시간 및 조건만큼 대기하고 다시 재시도를 반복한다
- 최대 재시도 횟수를 넘기게 되는 경우 예외를 발생시킨다
final int MAX_RETRY = 100;
public Object someMethod() {
int maxRetry = MAX_RETRY;
while(maxRetry > 0) {
try {
...
} catch(SomeException e) {
// 로그 출력. 정해진 시간만큼 대기한다.
} finally {
// 리소스 반납 및 정리 작업
}
}
// 최대 재시도 횟수를 넘기면 직접 예외를 발생시킨다.
throw new RetryFailedException();
}
2) 예외처리 회피
- 예외 처리를 직접 담당하지 않고 호출한 쪽으로 던져 회피하는 방법
- 그래도 예외 처리의 필요성이 있다면 어느 정도는 처리하고 던지는 것이 좋다
- 긴밀하게 역할을 분담하고 있는 관계가 아니라면 예외를 그냥 던지는 것은 무책임하다
// 예시 1
public void add() throws SQLException {
// ...생략
}
// 예시 2
public void add() throws SQLException {
try {
// ... 생략
} catch(SQLException e) {
// 로그를 출력하고 다시 날린다!
throw e;
}
}
3) 예외 전환
- 예외 회피와 비슷하게 메서드 밖으로 예외를 던지지만, 그냥 던지지 않고 적절한 예외로 전환해서 넘기는 방법
- 조금 더 명확한 의미로 전달되기 위해 적합한 의미를 가진 예외로 변경한다
- 예외 처리를 단순하게 만들기 위해 포장(wrap)할 수도 있다
// 조금 더 명확한 예외로 던진다.
public void add(User user) throws DuplicateUserIdException, SQLException {
try {
// ...생략
} catch(SQLException e) {
if(e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY) {
throw DuplicateUserIdException();
}
else throw e;
}
}
// 예외를 단순하게 포장한다.
public void someMethod() {
try {
// ...생략
}
catch(NamingException ne) {
throw new EJBException(ne);
}
catch(SQLException se) {
throw new EJBException(se);
}
catch(RemoteException re) {
throw new EJBException(re);
}
}
체크 예외 기본 이해
- Exception과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다
- (단, RuntimeExcetion은 예외로 한다)
- 체크 예외는 잡아서 처리(try-cathc) 하거나 또는 밖으로 던지는(throw) 등의 에러 처리를 해야한다
- 그렇지 않으면 컴파일 오류가 발생한다
Exception을 상속 받은 예외는 체크 예외가 된다
static class MyCheckedException extends Exception {
public MyCheckedException(String message) {
super(message);
}
}
체크 예외를 잡아서 처리하는 코드
public void callCatch() {
try {
repository.call();
} catch (Exception e) {
//예외 처리 로직
}
}
체크 예외를 밖으로 던지는 코드
public void callThrow() throws MyCheckedException {
repository.call();
}
체크 예외 장점
- 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아준다
체크 예외 단점
- 실제로는 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에 번거롭다
언체크 예외 기본 이해
- RuntimeException의 하위 클래스들을 의미한다
- 언체크 예외는 컴파일러가 예외를 체크하지 않는다
- 체크 예외가 기본적으로 동일하지만 차이가 있다면, 예외를 던지는 throws를 선언하지 않고 생략할 수 있다는 점이다
- 즉, 체크 예외와 달리 에러 처리를 강제하지 않는다
- 이 경우 자동으로 예외를 던진다
언체크 예외를 잡아서 치리하는 코드
try {
repository.call();
} catch (MyUncheckedException e) {
//예외 처리 로직
}
- 언체크 예외도 필요한 경우 이렇게 잡아서 처리가 가능하다
언체크 예외를 밖으로 던지는 경우
public void callThrow() {
repository.call();
}
- 언체크 예외는 체크 예외와 다르게 throws 예외를 선언하지 않아도 된다
- 참고로 언체크 예외도 throws 예외를 선언해도 된다
- 언체크 예외는 주로 생략하지만, 중요한 예외의 경우 선언해두면 해당 코드를 호출하는 개발자가 이런 예외가 발생한다는 점을 IDE를 통해 좀 더 편리하게 인지할 수 있다
언체크 예외 장점
- 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있다
언체크 예외 단점
- 언체크 예외는 개발자가 실수로 예외를 누락할 가능성이 있다
체크 예외 활용
체크 예외의 기본 원칙은 기억하자!
- 기본적으로 언체크 예외를 사용하자
- 체크 예외는 비즈니스 로직 상 의도적으로 던지는 예외에만 사용하자 (ex. 계좌 이체 실패, 로그인 ID/PW 불일치 등)
체크 예외의 발생 흐름
- Service는 Repository와 NetworkClient를 둘 다 호출한다
- 따라서 두 곳에서 올라오는 체크 예외인 SQLException과 ConnectException을 처리해야 한다
- 하지만 서비스는 이 둘을 처리할 방법을 모르기 때문에 밖으로 던진다
- 컨트롤러도 두 예외를 처리할 방법이 없기 때문에 예외를 밖으로 던진다
- 웹 애플리케이션이라면 서블릿의 오류 페이지나 스프링 MVC가 제공하는 ControllerAdvice에서 이런 예외를 공통으로 처리해준다
- 이렇게 해결 불가능한 공통 예외는 별도의 오류 로그를 남기고, 개발자가 오류를 빨리 인지할 수 있도록 전달받아야 한다
체크 예외의 문제점
위의 예시를 통해 크게 2가지 문제가 있다는 것을 알 수 있다
1) 복구 불가능한 문제
- 대부분의 예외는 복구가 불가능하다
- 대부분의 서비스나 컨트롤러는 SQLException과 같은 문제를 해결할 수 없으므로, 오류 로그를 남기고 개발자가 해당 오류를 빠르게 인지하는 것이 필요하다
- 서블릿 필터, 스프링 인터셉터, 스프링의 ControllerAdvice를 사용하면 이런 부분을 깔끔하게 해결할 수 있다
2) 의존 관계에 대한 문제
- throws SQLException, ConnectException 처럼 예외를 던지는 부분을 코드에 선언하면 서비스와 컨트롤에서 java.sql.SQLException을 의존하기 때문에 문제가 된다
- 만약 JDBC를 JPA 같은 기술로 변경한다면 예외도 함께 변경해야 하며, 해당 예외를 던지는 모든 부분도 함께 변경해야 한다
언체크 예외 활용
- 런타임 예외이기 떄문에 서비스, 컨트롤러는 해당 예외들을 처리할 수 없다면 별도의 선언 없이 그냥 두면 된다
- 런타임 예외를 사용하면 서비스나 컨트롤러가 복구 불가능한 예외를 신경쓰지 않아도 된다
- 런타임 예외는 해당 객체가 처리할 수 없는 예외는 무시하면 되므로, 체크 예외처럼 예외를 강제로 의존하지 않아도 된다
런타임 예외 사용 변환 코드
static class NetworkClient {
public void call() {
throw new RuntimeConnectException("연결 실패");
}
}
static class Repository {
public void call() {
try {
runSQL();
} catch (SQLException e) {
throw new RuntimeSQLException(e);
}
}
private void runSQL() throws SQLException {
throw new SQLException("ex");
}
}
- Repository에서 체크 예외인 SQLException이 발생하면, 런타임 예외인 RuntimeException으로 전환해서 예외를 던진다
- NetworkClient는 단순히 기존 체크 예외를 RuntimeConnectException이라는 런타임 예외가 발생하도록 하였다
💡 참고
- throw new RuntimeSQLException(e); 에서 기존 예외 e를 포함하지 않는다면, 기존에 발생한 java.sql.SQLException과 Stack Trace를 확인할 수 없다
- 변환한 RuntimeSQLException부터 예외를 확인할 수 있으므로 꼭 기존 예외를 포함해야 한다
런타임 예외 throws 생략
class Controller {
public void request() {
service.logic();
}
}
class Service {
public void logic() {
repository.call();
networkClient.call();
}
}
- 런타임 예외이기 때문에 컨트롤러나 서비스가 해당 예외를 처리할 수 없다면 다음 부분을 생략할 수 있다
- method() throws RuntimeSQLException, RuntimeConnectException
- 런타임 예외를 사용하면 중간에 기술이 변경되어도 해당 예외를 사용하지 않는 컨트롤러, 서비스에서는 코드를 변경하지 않아도 된다
- 공통 처리하는 한 곳만 변경되기 때문에 변경의 영향 범위는 최소화된다
체크 예외와 언체크 예외의 Rollback 여부
- 체크 예외 : Rollback이 되지 않고 트랜잭션이 commit까지 완료된다
- 언체크 예외 : Rollback이 된다
💡 Checked Exception vs Unchecked Exception
- 예외 복구 전략이 명확하고 복구가 가능하면 Checked Exception을 try-catch로 잡아서 예외 복구를 하거나, 코드의 흐름으로 제어하는 것이 좋다
- 그러나 이러한 경우는 흔하지 않기 때문에 Checked Exception이 발생하면 더 구체적인 Unchecked Exception을 발생시키고 예외에 대한 메시지를 명확하게 전달하는 것이 효과적이다
- 무첵임하게 상위 메서드에 throw로 예외를 던지면 상위 메서드들의 책임이 그만큼 증가하기 떄문에 되도록 상위 메서드에 던지지 않는 것이 좋다
- Checked Exception은 기본 트랜잭션 속성에서 Rollback을 진행하지 않는다
🔍예상 면접 질문 정리
Error(에러)란?
- 시스템에 무엇인가 비정상적인 상황이 발생한 경우에 사용된다
- 주로 JVM에서 발생시키며 이는 애플리케이션 코드에서 잡을 수 없다
Exception(예외)란?
- 입력 값에 대한 처리가 불가능하거나 프로그램 실행 중에 참조된 값이 잘못된 경우 등 정상적인 프로그램의 흐름을 어긋나는 것을 말한다
- 자바에서 예외는 개발자가 직접 처리할 수 있기 때문에 예외 상황을 미리 예측하여 핸들링할 수 있다
Checked Excetpion 이란?
- RuntimeException을 상속하지 않은 클래스로, 명시적인 에외 처리를 해야 한다
- 컴파일 시점에 확인할 수 있고, 트랜잭션 안에서 동작할 때 Checked Exception이 발생하면 롤백되지 않는다
Unchecked Exception 이란?
- RuntimeException을 상속한 클래스로, 명시적인 예외 처리를 하지 않는다
- 런타임 시점에 확인할 수 있고, 트랜잭션 안에서 동작할 때 Unchecked Exception이 발생하면 롤백된다
Checked Exception을 처리하는 방식은?
- 예외 복구 : 예외 상황을 파악하고 문제를 해결해서 정상 상태로 복구한다
- 예외 처리 회피 : 예외 처리를 직접 담당하지 않고 호출한 쪽으로 넘긴다
- 예외 전환 : 적절한 예외로 전환해서 넘긴다
올바른 예외 처리 방식
- 예외 복구 전략이 명확하고 복구가 가능하다면 Checked Exception을 try-catch로 잡아서 예외를 복구하는 것이 좋다
- 복구가 불가능한 Checked Exception이 발생하면 더 구체적인 Unchecked Exception을 발생시키고 예외에 대하 ㄴ메시지를 명확하게 전달하는 것이 좋다
- 무책임하게 상위 메서드에 throw로 예외를 던지는 것은 상위 메서드의 책임이 증가하기 때문에 좋지 않다
참고)
https://madplay.github.io/post/java-checked-unchecked-exceptions
https://devlog-wjdrbs96.tistory.com/351
https://steady-coding.tistory.com/583
728x90
'야미스터디 > Java' 카테고리의 다른 글
[Java - Stream] 01. 스트림을 학습해야 하는 이유 (0) | 2022.12.07 |
---|---|
[Java] JPA 1차 캐시 📌 (1) | 2022.10.18 |
[Java] Java 버전 별 차이점📌 (0) | 2022.09.06 |
[Java] JDK, JRE, JVM (0) | 2022.09.04 |
[Java] Java 컴파일 과정 📌 (0) | 2022.09.04 |
댓글