2022. 3. 6. 20:45ㆍSpring
안녕하세요. 오늘은 Spring에서의 Exception 처리에 대해 다루도록 하겠습니다.
본 내용은 토비의 스프링 3.1 을 학습하고 정리한 글입니다.
자세한 정리본은 깃허브를 참고해주세요.
문제의 예외 처리 방법
예외를 처리하는 건 코드를 짜면서 굉장히 번거로운 일이 될 수 있습니다.
그래서 많은 사람들이 예외 처리를 할 때 문제가 되는 코드를 짜곤 하는데요.
지금부터 어떤 예외 처리 방법이 문제가 되는지 알아보도록 할게요.
📌 예외 블랙홀
예외에 대한 아무런 대응없이 진행되는 코드
종종 코드를 짜게 되면 아래와 같은 코드를 확인할 수 있습니다.
try {
...
} catch(SQLException e) {
}
위와 같은 처리는 프로그램 실행 중 오류로 인해 예외가 발생했는데, 무시하고 계속 진행해버립니다.
이런 코드는 비정상적인 동작, 메모리나 리소스가 소진, 예상치 못한 문제를 일으키는 등의 문제가 발생할 수 있습니다.
무엇보다, 시스템 오류나 이상한 결과의 원인을 찾기 매우 힘듭니다.
정상적인 것처럼 다음 라인으로 넘어가겠다는 의도가 있는게 아니라면
연습 중에도 절대 만들어서는 안되는 코드입니다.
try {
...
} catch(SQLException e) {
System.out.println(e);
}
마찬가지 문제로, 위와 같이 로깅을 남겨두는 경우도 있습니다.
개발 중에는 이 메시지가 눈에 띌 수 있어도 대부분 다른 로그나 메시지에 금방 묻혀버리면 놓치기 쉽상입니다.
📌 무책임한 throws
컴파일 에러를 피하려고 무분별한 throws들을 적곤 합니다.
모든 예외를 무조건 던져버리는 throws Exception을 모든 메소드에 기계적으로 넣는 개발자들도 있습니다.
이러한 예외 처리는 실행 중 예외 상황이 발생하는지, 혹은 습관적으로 붙여놓은지 그 역할을 확인할 수 없게 됩니다.
무엇보다, 적절한 처리를 통해 복구될 수 있는 예외 상황도 제대로 처리할 수 없게됩니다.
📌 예외 처리 핵심 원칙
✔️ 예외 상황에 대한 적절한 복구
✔️ 작업을 중단 후 운영자 또는 개발자에게 통보
예외의 종류
Java의 예외는 크게 Error와 Exception으로 나눌 수 있습니다.
그 중, Exception은 예외를 반드시 체크해야하는지의 여부에 따라
CheckedException과 UncheckedException으로 구분할 수 있습니다.
📌 Error
java.lang.Error 클래스의 서브클래스
Error는 시스템에 비정상적인 상황이 발생했을 경우에 사용합니다.
Error는 주로 자바 VM에서 발생시키는 것이고 애플리케이션 코드에서 잡으려고 하면 안됩니다.
OutOfMemoryError
나 ThreadDeath
같은 에러는 catch 블록으로 잡아봤자 아무런 대응 방법이 없기 때문입니다.
그래서 시스템 레벨에서 특별한 작업을 하는 게 아니라면,
애플리케이션에서는 이런 에러에 대한 처리는 신경 쓰지 않아도 됩니다.
Exception
java.lang.Exception 클래스의 서브 클래스
Error와 달리 애플리케이션 코드에서 예외가 발생하였을 경우에 사용됩니다.
Exception 은 CheckedException과 UncheckedException 으로 구분할 수 있습니다.
📌 Checked Exception
- Exception Class의 서브 클래스
- RuntimeException Class를 상속하지 않음
- Transaction으로 Rollback이 진행 안됨
반드시 예외를 체크해야하는 경우 사용합니다.
프로그램 상으로 의도한 예외가 아니기 때문에 반드시 예외를 처리하는 코드를 함께 작성해야 합니다.
예외 처리 코드가 없다면 컴파일 에러가 발생합니다.
catch 문으로 잡거나 throws를 통해 메소드 밖으로 던질 수 있습니다.
만약 예외를 처리하지 않으면 컴파일 에러가 발생합니다.
IOException이나 SQLException 등과 같은 예외가 있습니다.
✔️ Transaction
기본적으로 Transaction은 Unchecked Exception이나 Error의 경우만을 처리합니다.
즉, 추가 설정을 하지 않았다면 기본적으로 Transaction에 의한 Rollback이 되지 않습니다.
그렇다면, 왜 CheckedException은 왜 rollback이 되지 않도록 해주었을까요?
CheckedException은 반드시 처리되어야 하는 예외이기 때문입니다.
해당 예외가 예상되면 throw하며 해당 코드에서 처리한다는 것을 명시합니다.
하지만 예외되는 상황이 있기에 Transaction 설정에 rollbackFor이나 attribute가 있습니다.
📌 Unchecked Exception
- Exception Class의 서브 클래스
- RuntimeException Class를 상속
- Transaction으로 Rollback이 진행 됨
반드시 예외를 체크하지 않아도 되는 경우 사용합니다.
이처럼 명시적인 예외처리를 강제하지 않기 때문에 언체크 예외라고 부릅니다.
✔️ 런타임 예외
언체크 예외는 런타임 예외라고도 부릅니다.
체크 예외와는 달리 따로 처리하지 않아도 컴파일 에러가 발생하지 않습니다.
예외를 피할 수 있지만 개발자가 부주의해서 발생할 수 있는 경우를 위해 만든 예외입니다.
즉, 주로 프로그램의 오류가 있을 때 발생하도록 의도됩니다.
✔️ 예시
대표적으로 오브젝트를 할당하지 않은 레퍼런스 변수를 사용하려고 시도했을 때 발생하는 NullPointerException
이나,
허용되지 않는 값을 사용해서 메소드를 호출할 때 발생하는 IllegalArgumentException
등이 포함됩니다.
✔️ 낙관적 예외
런타임 예외는 낙관적인 예외와 가깝다고 볼 수 있습니다.
복구할 수 있는 예외는 없다고 가정하고 예외가 생겨도 어차피 런타임 예외이므로 시스템 레벨에서 알아서 처리해줄 것이고,
필요한 경우에 잡아서 복구하거나 대응할 수 있기 때문입니다.
📌 Application Exception
시스템 또는 외부의 예외 상황이 원인이 아니라 애플리케이션 자체의 로직에 의해 의도적으로 발생시키고,
반드시 catch해서 무엇인가 조치를 취하도록 요구하는 예외입니다.
애플리케이션 예외는 시스템 레벨 오류를 보고하는 데 사용되지 않고,
대신 비즈니스 메소드에서는 애플리케이션 예외를 사용하여 클라이언트에 알립니다
예를들어 메소드에 올바르지 않은 파라미터를 입력했을 때의 예외가 있습니다.
대부분의 경우 애플리케이션 예외가 발생하면 정상적인 처리로 진행하며 처리합니다.
정상 완료가 아닌 예외의 경우도 시스템 오류가 아니기 때문에 정상 흐름입니다.
Application Exception 처리 방법
애플리케이션 예외는 "성공/실패 경우 각각 다른 종류의 리턴 값 반환" 이나 "비즈니스적인 의미를 띤 예외를 던지기"로 해결할 수 있는데요. 아래 예시를 통해 확인해볼게요.
은행 시스템에서 출금을 요청하는 사용자가 있을 때, 사용자의 잔액을 먼저 확인합니다.
이 때, 요청자의 잔액이 부족했을 때를 가정하면 다음과 같습니다.
✔️ 성공/실패 경우 각각 다른 종류의 리턴 값 반환
잔액 부족(실패) 시 -1이나 0 등의 특별히 정한 특정 값을 반환할 수 있습니다.
발생할 수 있는 문제
⚠️ 예외상황에서의 결과 값에 대한 표준 값이 존재하지 않고, 개발자 간의 의사소통이 미흡해 생기는 문제
⚠️ if 블록으로 범벅된 코드
✔️ 비즈니스적인 의미를 띤 예외를 던지기
InsufficientBalanceException와 같은 체크 예외를 던질 수 있습니다.
그래서 잔고 부족과 같이 자주 발생할 수 있는 예외 상황을 처리하도록 로직 구현을 강제해주는 것이 좋습니다.
체크 예외를 통한 처리는 런타임 예외로 만들어두는 것보다 상대적으로 안전합니다.
⚠️ 무책임한 throws
예외 처리 방법
예외 처리 방법에 대해 알아보고 상황에 맞는 처리를 생각해보겠습니다.
📌 예외 복구
예외상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것
예를 들어 IOException이 발생했을 때,
사용자에게 상황을 알려주고 다른 파일을 이용하도록 안내해서 예외상황을 해결할 수 있습니다.
이처럼 다른 작업 흐름으로 자연스럽게 유도하여 처리하는 방법이 있습니다.
혹은 네트워크의 불안으로 발생한 SQLException 발생하는 경우에는 재시도를 해볼 수 있습니다.
일정 시간 대기했다가 다시 접속을 시도해보는 방법이 있습니다.
📌 예외 처리 회피
예외 처리를 자신을 호출한 쪽으로 던지는 것
throws 문으로 선언해서 예외가 발생하면 알아서 던져지게 하거나 catch 문으로 일단 예외를 잡은 후에 로그를 남기고 다시 예외를 던지는rethrow할 수 있습니다.
예를 들어 ResultSet이나 PreparedStatement 와 같은 경우,
메서드에 SQLException을 던져 호출하는 쪽에서 처리하게끔 정의합니다.
public interface ResultSet extends Wrapper, AutoCloseable {
...
boolean next() throws SQLException;
void close() throws SQLException;
...
}
하지만, 위에서 언급했듯이 무책임한 throws가 되지 않도록 주의해야 합니다.
📌 예외 전환
예외를 적절한 예외로 전환해서 메소드 밖으로 던지는 것
예외 회피와 비슷하게 예외를 복구해서 정상적인 상태를 만들 수 없기 때문에 예외를 메소드 밖으로 던지는 것이다.
하지만 예외 회피와는 달리, 적절한 예외로 전환해서 던집니다.
예외 전환의 목표
1. 의미를 분명하게 해줄 예외로 바꾸는 것
2. 예외를 처리하기 쉽고 단순하게 만들기 위해 포장wrap하는 것
어차피 복구하지 못할 예외라면 애플리케이션 코드에서는 런타임 예외로 포장해서 던져버리고,
아래의 모든 처리를 하는게 낫습니다.
✔️ 예외처리 서비스 등을 이용한 자세한 로그
✔️ 관리자에게 메일 등으로 통보
✔️ 사용자에게 안내 메시지를 보여주기
그럼 지금까지 예외처리에 대해 알아보았습니다.
오타나 잘못된 내용이 있다면 댓글로 남겨주세요!
감사합니다 ☺️
'Spring' 카테고리의 다른 글
Spring Transaction, 제대로 이해하기 (0) | 2022.03.13 |
---|---|
@ExceptionHandler, @ControllerAdvice (3) | 2022.03.08 |
Spring, 3-Tier-Architecture (0) | 2021.05.06 |
Spring MVC, 동작 원리 (4) | 2021.05.02 |
Spring MVC, DB 연결하기 (0) | 2021.04.18 |
Backend Software Engineer
𝐒𝐮𝐧 · 𝙂𝙮𝙚𝙤𝙣𝙜𝙨𝙪𝙣 𝙋𝙖𝙧𝙠