1. 예외
예외처리 : 프로그램 실행 시 발생할 수 있는 예외에 대비하는 것으로 프로그램 비정상종료를 막고 실행 상태를 유지하는 것.
에러(error) : 발생 시 수습할 수 없는 심각한 오류. (컴파일에러, 런타임에러 등)
예외(exception) : 예외 처리를 통해 수습할 수 있는 덜 심각한 오류.
(예외에는 크게 Error(java.lang.Error), Exception(java.lang.Exception)과 체크예외, RuntimeException과 언체크/런타임 예외 가 있다.)
2. Checked Exception & UnChecked Exception
.
*간단하게 RuntimeException을 상속하지 않는 클래스는 Checked Exception, 반대로 상속한 클래스는 Unchecked Exception.
.
-예외를 처리하는 방법 3가지
- 예외 복구 : 예외 상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 방법.(catch로 잡아서 retry 등)
- 예외 처리 회피 : 예외 처리를 직접 담당하지 않고 호출한 쪽으로 던져 회피하는 방법.(throw e)
- 예외 전환 : 예외 회피와 비슷하게 예외를 던지지만, 그냥 던지지 않고 적절한 예외로 전환해서 넘기는 방법.(throw MyCustomException()).
3. 초난감 예외처리 코드
// 1. 아무 처리도 하지 않는 경우
try {
...
} catch (SQLException e) { // 예외를 잡고 아무것도 하지 않는다. 예외 발생을 무시해버리고 정상적인 상황인 것처럼
// 다음 라인으로 넘어가면 안된다.
}
// 2. 단순 출력
} catch (SQLException e) {
System.out.println(e);
}
// 3. 호출 스택 출력
} catch (SQLException e) {
e.printStackTrace();
}
// 4. 무책임한 throws
public void method1() throws Exception {
method2();
...
}
public void method2() throws Exception {
method3();
...
}
public void method3() throws Exception {
...
}
Tip
*e.printStackTrace() :
•예외 발생 당시의 호출스택(Call stack)에 있던 메소드의 정보와 예외 결과를 화면에 출력함
•예외 상황을 분석하기 위한 용도로 사용 (개발자에게 디버깅 할 수 있는 힌트를 제공)
사용하지 말아야 하는 이유
1.printStackTrace()를 call할 경우 System.err로 쓰여져서 제어하기가 힘듬
2.printStackTrace()는 java 리플렉션을 사용하여 추적하는 것이라서 많은 오버헤드가 발생할 수 있음
3.printStackTrace()는 서버에서 스택정보를 취합하기 때문에 서버에 부하가 발생할 수 있음
4.printStackTrace()는 출력이 어디로 가는지 파악하기 가 어려움 (톰캣같은 경우 catalina.out에 남음)
5.printStackTrace()는 관리가 힘듬 (보통 log4j, logback과 같은 로깅 라이브러리를 사용하여, 로그 패턴 및 로그 메세지를 지정 및 콘솔로그 / 파일로그 형태로 관리할 수 있음)
성능을 중시하는 어플리케이션이라면 e.printStackTrace는 사용하지 말자
throw vs throws
-throw : 메소드 내에서 상위 블럭으러 예외를 던지는 것.(프로그래머의 판단에 따른 처리)
-throws : 현재 메소드에서 상위 메소드로 예외를 던지는 것.(책임전가)
class Test {
public static void calculate() throws ArithemeticException {
int a = 0;
a = 10/a; // 0으로 나누면 예외 발생.
}
public static void main(String[] args) {
try {
Test.calculate(); // 예외를 던짐.
} catch (Exception e) {
System.out.println("메인 메소드가 예외를 잡아서 처리함: " +e);
throw new MyException();
}
}
결론
: 난감한 예외처리를 하지 말고, 서버에 간단 명료하게 원인을 파악할 수 있는 로그(Slf4j log.error 등)와 함께 상황에 따라 적절한 체크 예외를 던지는것이 좋을 것으로 보인다.
ex) id가 중복되었으면 DuplicateUserIdException 클래스를 만들어서 RuntimeException 를 상속받아 예외를 넘겨준다.
다시 정리하면 자바에서 예외는 RuntimeException을 상속하지 않고 꼭 처리해야 하는 Checked Exception 과 반대로 명시적으로 처리하지 않아도 되는 Unchecked Exception 으로 구분할 수 있다.
public class DuplicateUserIdException extends RuntimeException {
puvlic DuplicateUserIdException(Throwable cause) {
super(cause);
}
}
추가적으로..
ExceptionHandler 와 ControllerAdvice를 통한 전역 예외 처리 ExceptionHandler 단일, ControllerAdvice 전역
SLF4J - 추상체를 사용해야 하는 이유
SLF4J - appender
Q&A
-예외 Best Practice 8가지
- THROWABLE(모든 예외의 상위 클래스 - 에러까지 잡는다)예외 처리로 하지 마라.
- 입력을 빨리 검증하라 (예외처리 최소화)
- 예외로 흐름을 제어하지 마라(예외 에서 분기)
- 하나의 예외로그는 한줄의 예외로그로 제어(키값으로 제어 가능 GREP)
- 예외를 감싸서 던져라(예외전파(프로파제이션),예외체이닝,예외래핑)
- FINALLY 블록에서 예외 던지지 마라 ? 이전까지 스택트레이스 기록 불가능
- 예외로그를 남기고 똑같은 예외를 한번 더 던지지마라
- 빨리 던지고(예외는 최대한 빨리) 늦게 잡아라(적절한 위치(최상위 최하위 레이어 등)가 아닌곳에서 무리한 예외처리 할필요 x)
-Checked Exception 에서 롤백처리가 되지 않는가에 대해서(중요)
구글링을 하면 Checked Exception 은 롤백이 되지 않는다고 정리된 표들이 많은데,
이는 특정 출판사에서 출판한 책 내용에서 잘못된(중요 정보 생략)정보로 부터 계속 파생되어 공유되고 있다고 본 것 같습니다.
스프링에서는 설정을 변경하면 디폴트 롤백 설정을 변경 할 수 있다고 합니다.
관련 블로그 내용에서 아래의 내용을 우선 확인하였습니다.
( https://www.notion.so/3565a9689f714638af34125cbb8abbe8 )
-> java와 spring 개념이 혼용되며 혼란을 야기함.
-> spring의 기본적인 트랜잭션 설정은 checked 는 롤백하지 않고, unchecked는 롤백한다.
- but. 기본적인 설정일 뿐 변경할 수 있다.
추가적으로 설정을 변경하여 checked exception 에서 설정을 변경하여 롤백 처리를 한 블로그들입니다.
http://wonwoo.ml/index.php/post/1542
https://techblog.woowahan.com/2606/
-실패 원자성을 갖도록 노력하자
https://brunch.co.kr/@oemilk/178
이펙티브자바 - 가능한한 실패 원자적으로 만들라
-멀티 쓰레드 환경에서 동시성 제어
https://deveric.tistory.com/104
4. 예외 전파(Exception Propagation)
상위 계층으로 예외가 전달될 때 마다 새로운 예외에 포함시켜 다시 던지는 과정
예외 체이닝, 예외 래핑이라고 불리기도 한다.
예외가 다른 계층으로 전달될 때, 이전 예외를 원인으로 가지는 새로운 예외를 던지는 것을 예외 전파라고 한다.
예외 전파의 목적
가장 큰 목적은 첫 예외부터 전파되는 과정을 통해 거치는 예외들을 보존하기 위함.
예외 전파를 통해 stack trace를 쌓고 예외가 어디서부터 어떤 과정을 거쳐 전달됐는지 확인할 수 있다.
예외 전파 예시
5. 예외 처리 Best Practice
1. Throwable을 잡지마라.
Throwable은 모든 예외와 에러의 상위 클래스이다. 모든 예외 뿐만 아니라 에러까지 잡아버리게 된다. 에러는 JVM이 던지는 것으로 애플리케이션에서 처리할 수 있는 것이 아니다.
비슷한 이유로 Exception이나 RuntimeException을 잡을 때도 마찬가지다.
2. 사용자 입력을 최대한 빨리 검증하라.
사용자 입력이 컨트롤러에 도달하기 전에 검증을 하면, 예외 처리를 위한 코드를 최소화할 수 있다.
3. 예외로 흐름을 제어하지 마라.
예외는 오류이다. 예외를 로직으로 사용해서는 안 된다.
Ex) 어떤 메소드에서 던지는 예외를 catch로 잡아 다른 메소드로 분기하면 안 된다.
4. 하나의 예외는 하나의 로그에 작성하라.
많은 요청이 들어오는 상황이라면, 여러 줄로 나눈 로그는 다른 로그와 섞여 멀리 떨어질 수 있다.
5. 예외를 감싸서 던져라.
예외 전파라고도 한다.
단 예외 체이닝을 하기 전, 감싸고 있는 예외가 새로운 효용을 주는지 생각해보자.
6. 절대 finally 블록에서 예외를 던지지 마라.
예외가 어디서부터, 왜, 어떤 과정을 거쳐 발생했는지 남기는 것은 중요하다.
finally 블록에서 예외를 던진다면 이전까지의 stack trace를 기록할 수 없게 된다.
7. 예외 로그를 남기고 던지지 마라.
하나의 예외에 대해서 로그도 남기고 전달도 한다면 여러 로그 메시지가 기록되게 된다. 로그를 보며 디버깅할 때 매우 힘들어질 수 있으므로 둘 중 하나만 하자.
예외를 로깅할 것이라면 처리할 때 해야 한다.
8. 빨리 던지고 늦게 잡아라.
- 빨리 던져라
예외는 할 수 있는 한 최대한 빨리 던지는 것이 좋다. 에러가 발생한 곳에서 예외를 바로 발생시키면, 로직 등이 모두 그곳에 있기 때문에 왜 예외가 발생했는지 알기 가장 쉽다.
stack trace를 따라 갈 때도 예외를 발생시킨 곳이 문제가 있는 곳이기 때문에 디버깅하기 좋다.
- 늦게 잡아라
예외를 늦게 잡으라는 것은 처리하기 적합하지 않은 곳에서 무리하게 예외 처리를 하려고 하지 말라는 것이다. 예를 들어 최상위 레이어에서 사용자에게 파일의 위치를 입력 받고, 최하위 레이어에서 이 파일의 내용을 읽는 애플리케이션이 있다. 파일이 존재하지 않는 예외의 처리 방법으로 사용자 입력을 다시 받게 하려면, 최상위 레이어가 예외를 처리해야 하고 사이의 레이어들은 체이닝만 해야 한다.
6. @ExceptionHandler와 @ControllerAdvice
@ExceptionHandler
@ExceptionHandler는 특정 예외가 발생한 요청을 처리하는 핸들러이다. MVC 컨트롤러 안에서 어떤 요청을 처리하다가 에러를 직접 만들어 발생시키거나 또는 자바에서 기본적으로 지원하는 예외가 발생했을 때, @ExceptionHandler로 정의해서 그 예외들을 어떻게 응답을 보낼지를 설정할 수 있다. @Controller, @RestController가 아닌 @Service 나 @Repository 가 적용된 Bean에서는 사용할 수 없다.
지원하는 메소드 아규먼트를 정의할 수 있습니다. (아래 url 참고) (해당 예외 객체, 핸들러 객체 등)
지원하는 리턴 값은 기본 메소드의 리턴값을 대부분 지원함
REST API의 경우 응답 본문에 에러에 대한 정보를 담아주고, 상태 코드를 설정하려면 ResponseEntity를 주로 사용
만약, 여러개의 에러를 처리하는 핸들러를 정의하고 싶다면 다음과 같이 정의할 수 있습니다. 이와 같은 경우엔 아규먼트를 모든 에러를 다 받을 수 있는 상위타입의 에러로 정의해야 한다.
<예시>
@(Rest)CnotrollerAdvice
: @ControllerAdvice는 @Controller 어노테이션이 있는 모든 곳에서의 예외를 잡을 수 있도록 해준다. @ControllerAdvice 안에 있는 @ExceptionHandler는 모든 컨트롤러에서 발생하는 예외상황을 잡을 수 있다. @ControllerAdvice 의 속성 설정을 통하여 원하는 컨트롤러나 패키지만 선택할 수 있다. 따로 지정을 하지 않으면 모든 패키지에 있는 컨트롤러를 담당하게 된다.
<예시>
7. 예외의 종류와 특징
자바에서는 throw를 통해 발생시킬 수 있는 예외는 크게 세 가지가 있다.
1. Error
java.long.Error 의 서브클래스
시스템에 뭔가 비정상적인 상황이 발생했을 경우에 사용
주로 JVM 에서 발생시키는 것으로 애플리케이션 코드로 잡으려하면 안됨
n 🤷♂️ why? catch로 잡아봤자 대응 방법이 없음
2. Exception과 체크 예외
java.lang.Exception 클래스와 그 서브 클래스
애플리케이션 코드 작업 중에 예외 상황이 발생했을 경우에 사용
RuntimeException을 상속하지 않은 것을 체크 예외라고 부름
체크 예외가 발생할 수 있는 메소드 사용 시 반드시 예외를 처리하는 코드도 함께 작성되어야 함
catch 또는 throws를 통해 처리가 되지 않으면 컴파일 에러가 발생
3. RuntimeException과 언체크 예외
java.lang.RuntimeException 클래스의 서브 클래스
java.lang.RuntimeException 를 상속한 예외들은 명시적인 예외처리를 강제하지 않기 때문에 언체크 에외라고 불림
catch로 잡거나 throws로 선언하지 않아도 됨
주로 프로그램상 오류가 있을 때 발생하도록 의도된 것
ex) NullPointException , IllegalArgumentException
8. Checked Exception과 UnChecked Exception
Unchecked Exception은 개발자가 인지못하고 프로그램이 실행되며 발견하는 예외라 런타임예외라고한다. 개발자가 코드로 처리불가능한 SQLException, IOException등의 Checked Exception은 Unchecked Exception들로 포장을 해준다고 한다. 그 이유는 런타임에 해결이 불가능한 예외의 경우엔 쓸데없이 메소드 시그니처에 throws를 반복적으로 적거나, 의미없는 try-catch하는 것을 방지하기 위함이라고 한다.
둘의 차이점은 크게
Checked Exception = 발생가능성이 충분히 예측가능하고, 런타임에 복구가 가능한 경우
Unchecked Exception = 예측 불가능하거나 런타임에 복구가 불가능한 예외, 트랜잭션이 롤백되어야 하는 비즈니스 예외
Checked Exception은 개발자가 일반적으로 체크예외가 발생했을 때 복구 전략을 갖고 그것을 복구할 수 있는 경우는 그렇게 많지 않으므로 catch문에서 Unchecked Excpeiton으로 바꿔주는것으로 권장한다고 한다.
9. Checked Exception과 Unchecked Exception의 Rollback
스프링의 트랜잭션 어노테이션은 기본 정책이 Unchecked Exception와 Error라고 합니다. 즉, Checked Exception일때는 트랜잭션 어노테이션이 작동하지 않아 롤백이 되지 않고 커밋을 한다.
하지만 이건 단순히 기본설정일뿐, 둘다 롤백여부는 변경할 수 있다.
Checked Exception에서 롤백을 발생시키려면 모든 예외에서 롤백을 처리하면 된다고 합니다. Checked Exception를 try-catch문에서 더 구체적인 Unchecked Exception로 감싸주면 롤백이 가능하다.
10. 체크 예외와 언체크 예외를 발생시켜야 하는 상황
해당 구문을 호출하는 쪽에서 복구할 수 있는 상황에 체크 예외를 발생시킨다. 해당api를 호출하는 쪽에서 복구할 수 있는 상황임을 알리기 위해 체크 예외를 던지면 사용자는 해당 예외를 throw나 catch를 통해 어떻게든 처리해야 한다.
언체크 예외는 전제조건을 만족시키지 못할 때 발생시킨다. 예를 들어 배열의 잘못된 인덱스로 접근을 시도할 경우 전제 조건을 만족시키지 못해 예외가 발생한다. 이런 프로그래밍 오류는 언체크 예외를 사용해야 한다.
11. 실패 원자성
메소드를 만들면서 예외처리를 할 때 가능하면 실패 원자적으로 만들어야 한다. 실패 원자성이란 호출한 메소드가 실패하더라도, 그 객체는 실패하기 전 상태, 즉 메소드 호출 전 상태를 유지해야 한다는 개념이다.
작업 도중 예외가 발생해도 그 객체를 정상적으로 사용할 수 있는 상태라면 호출자가 오류 상태를 복구할 수 있을 테니 더 유용하기 때문이다.
이 실패 원자성을 유지할 수 있는 방법은 다음과 같다.
1. 메소드를 불변 객체로 설계한다.
▫ 불변 객체는 태생이 실패 원자적이며 객체가 불안정한 상태에 빠지지 않는다.
2. 수행에 앞서 매개변수의 유효성을 검사한다.
▫ 객체의 내부 상태를 변경하기 전, 잠재적 예외 가능성을 제거할 수 있다.
public Element pop(){
if(stack.length == 0){
throw new Exception();
}
...
}
3. 객체 상태 변경시 임시 복사본을 사용한다.
▫ 객체의 내부 상태를 변경할때 변경 대상의 값을 다른 변수에 복사하고, 연상 작업을 복사 변수에서 수행하면 실패가 일어나도 원래의 상태를 보존할 수 있다.
4. 실패 가능성이 있는 코드는 객체의 상태 변경 코드보다 앞에 배치한다.
실패 원자성은 권장되지만 항상 만족할 수 있는 것은 아니며, 항상 그래야 하는 것도 아니다.
1. 동시성 오류
▫ 두 스레드가 동기화 없이 같은 객체를 동시에 수정한다면 그 객체의 일관성이 깨질 수 있다. 동시성 오류같은 경우는 일반적으로 예외를 잡더라도 객체를 재사용하기 어렵다. 그러니 ConcurrentModificationException을 잡아냈다고 해서 그 객체가 여전히 사용할 수 있는 상태라고 생각해서는 안 된다. 동시성 오류는 보통 두개의 스레드가 객체의 상태를 변경할때 서로 각자의 기본 상태가 달라지는 걸 캐치하여 발견하기 때문에 두 스레드 중 어떤것이 원자성을 유지할 수 있는 상태인지 구분하기 어렵다.
2. 복구할 수 없는 에러는 실패 원자적으로 만들 필요가 없다.
▫ Error는 복구할 수 없으므로 AssertionError에 대해서는 실패 원자적으로 만들려는 시도조차 할 필요가 없다.
3. 비용이나 복잡도가 큰 경우
▫ 실패 원자성을 위한 비용이나 복잡도가 아주 크다면 넘어가도 된다.
'스터디정리방' 카테고리의 다른 글
[기록] 220112 (0) | 2022.01.16 |
---|---|
[기록] 220105 (0) | 2022.01.09 |
[기록] 211229 (0) | 2022.01.02 |
[기록] 211215 (0) | 2021.12.18 |
[기록] 211208 (0) | 2021.12.02 |
댓글