금융사에서는 healthCheck라는 api를 따로 두는 경우가 있다. 서버가 살아있는지 정기적으로 api call을 한다.
서버가 정상 작동 중이라면 UP, 다운 상태라면 DOWN을 리턴한다.
Adapter 서버에서는 은행사 응답 UP을 제외한 모든 경우에 DOWN을 주도록 코드를 개선했다.
as-is
WebClientResponseException 발생 시에만 try-catch 블록에 의해 DOWN 응답이 전송되고 있다. 그 외에 Exception, 서버 자체가 다운됐을 경우에는 UP도 DOWN도 응답을 보내지 않고 있어 문제가 됐다. (개발계)
to-be
(1) 은행측 happy case 응답이 정확히 일치하는 경우에만 UP을 보내고 그 외의 경우에는 모두 DOWN 처리
(2) Exception 발생 시에 로깅한 뒤, DOWN 처리.
이 과정을 진행하며 예외처리에 애매모호하게 알고 있던 부분을 정리해본다.
- catch block에서 throw를 하지 않을 경우 문제가 생기는가?
- catch에서 throw한 에러는 global exception handler가 처리하는가?
[ 1 ] 먼저 예외와 오류
먼저 예외와 오류의 차이점을 알아보자.
예외는 개발자가 처리 가능한 선에서 발생하는 문제 상황이다. 그러면 개발자가 처리 불가능한 문제 상황도 있겠지? 그게 바로 오류이다.
오류가 발생하면 으아악 큰일났다~ 하고 집에서 회사로 출근해야한다.
예외가 발생하면 흠. 그렇군. 한 뒤에 다음날 출근해서 고치면 된다.
- Error(오류)
시스템에 비정상적인 상황이 발생했을 경우이며 예측이 어렵고 기본적으로 복구가 불가능합니다.
e.g. OutOfMemoryError, StackOverflowError, etc
- Exception(예외)
시스템에서 포착 가능하여(try-catch) 복구 가능하며 자바의 경우 예외 처리를 강제합니다. (빨간색 밑줄이 그어지며 컴파일 에러가 발생하는 걸 본 적 있을것이다!)
예외는 checked exception과 uncheckced exception으로 나뉜다.
e.g. IOException, FileNotFoundException, etc
런타임 예외는 개발자들이 미리 알아내기 어려운 경우가 많다 (NPE라던가~) unchecked exception은 이런 식의 사전에 체크하지 못한 예외들이다.
- RuntimeException
런타임시에 발생하는 예외이며 자바의 경우 예외 처리를 강제하지 않습니다.
e.g. NullPointerException, ArrayIndexOutOfBoundsException, etc
코틀린의 모든 예외 클래스는 최상위 예외 클래스인 Throwable 을 상속한다
자바에서 checked exception은 컴파일 에러가 발생하기 때문에 무조건 try-catch로 감싸거나 throws로 예외를 전파해야 한다.
try {
Thread.sleep(1);
} catch (InterruptedException e) {
// 예외 처리
}
에러의 전파!! 도대체 무슨 말인가 싶죠?? 저도 이해하는데 한참 걸렸습니다만
아주 쉬운 말이랍니다~
A 클래스에서 B 클래스의 b 메소드를 호출하고 있다고 쳐봅시다.
이 때 b 메소드에서 예외가 발생했습니다! 이 때 try catch를 사용해서 예외를 포착했다칩시다.
그리고 throws를 해서 예외를 A 클래스에게 "야, 네가 처리해~"하고 떠넘겨버리는 것.
이것을 예외의 전파라고 합니다 ^^
코틀린은 checked exception을 강제하지 않습니다.
아래의 코드는 코틀린에서 컴파일 오류가 발생하지 않으며 정상작동 된다.
Thread.sleep(1);
짤막한 팁. 코틀린에서 try-catch는 식(expression)이다. (반면 자바의 try-catch는 문(statement)이다!)
'식'은 수학에서 자주 쓰던 그 '식'의 의미가 맞다. =을 사용해서 값을 대입해줄 수 있다.
fun max(a: Int, b: Int) = if (a > b) a else b
값을 '대입'해주는 =을 사용하므로 당연히 return은 사용되지 않는다. return은 메서드나 펑션에서 사용하는 것이니까.
따라서 ... try catch 또한 =을 사용하여 대입해줄 수 있다.
// try catch 결과를 result에 대입하는 예제
val result = try {
"1234".toInt()
} catch (e: NumberFormatException) {
println("catch 동작")
}
println("result = ${result}") //result = 1234
// catch block 동작 예제
val result = try {
"a".toInt()
} catch (e: NumberFormatException) {
println("catch 동작")
}
println("result = ${result}") // catch 동작 // result = kotlin.Unit // Unit으로 반환됨
또한 표현식이므로, try - catch 자체를 return 할 수도 있다.
return try {
...
} catch (e: Exception){
...
}
[ 2 ] throw는 예외를 떠넘기고, return은 결과를 돌려준다.
Kotlin에서 throw와 return은 모두 제어 흐름을 변경하는 연산자이지만, 역할이 다르다.
return은 함수에서 값을 반환하고 실행을 종료하는 반면,
throw는 예외를 발생시키고 프로그램 실행을 중단시키는 역할을 한다.
1. return
- return은 함수를 종료하고 결과를 반환하는 데 사용된다.
- 가장 가까운 enclosing 함수 또는 익명 함수에서 즉시 반환한다.
2. throw
- throw는 예외를 발생시켜 프로그램 실행을 중단시킨다.
- 발생한 예외는 try-catch 블록으로 잡아서 처리하거나, 함수 호출 스택을 따라 전파된다.
3. throw와 return의 비교
- return은 함수를 정상적으로 종료하고 결과를 반환하는 반면, throw는 예외를 발생시켜 비정상적인 종료를 유발한다.
- return은 값을 반환하지만, throw는 예외 객체를 던진다.
[ 3 ] catch block에서 throw를 하지 않을 경우 문제가 생기는가?
Spring에서 throw 키워드를 사용하지 않으면, 예외가 발생해도 메소드 호출자에게 전파되지 않고 내부적으로 처리된다.
예외를 처리할 책임이 해당 메소드 내에서 完결된다.
그러니 예외를 의도한 대로 잘 처리한다면 문제될 것이 없다.
throw를 할 경우 global exception handler가 명시하고 있는 예외인 경우에는 geh에서 처리한다.
우리 adapter 서버에서는 geh가 캐치한 뒤 공통적인 응답 모듈을 만들어 리턴하는 역할을 맡고 있었는데
UP/DOWN 응답값만 송신하면 되는 healthcheck api 입장에서는
- 상위로 예외를 전파시킬 필요가 없고
- 로깅을 제대로 한 뒤 DOWN 응답을 제대로 보내는 것이 중요하다고 생각되어
코드를 개선하게 됐다.
[ 4 ] catch에서 throw한 에러는 global exception handler가 처리하는가?
geh에서 구체적으로 예외를 명시하고 있다면, 그렇다.
먼저 try - catch와 global exception handler의 차이를 살펴보자
둘 다 예외처리에 사용되지만
try - catch는 아주 정확한 구간에서 예외를 검증하고 싶을 때 사용하고
gbh는 내가 예측하지 못한 구간에서 발생하는 전반적인 예외를 검증하고 싶을 때 사용한다. 어느 controller 든지, service든지, exception 이 발생하면 gbh가 자동으로 캐치해오기 때문이다.
그렇다면 catch 구문에서 throw 한 exception은 gbh가 핸들링할까?
checked exception도 Global Exception Handler에서 처리할 수 있다. https://umbum.dev/896/
checked exception은 반드시 try-catch나 상위로 throws 둘 중 하나를 해야 하기 때문에, checked exception을 처리하는 척 하면서 unchecked exception(=런타임 예외)으로 래핑해서 다시 던지면 Global Handler로 들어가게 할 수 있다.
RuntimeException을 상속 받은 RuntimeIOException 클래스를 하나 만들고, try-catch로 감싼 다음 RuntimeIOException에 checked exception을 담아서 던지도록 한다. (stack trace에 남는다.)
try {
// IOException 발생 ( checked exception )
} catch (Exception e) {
throw new RuntimeIOException(e) // this is unchecked exception!!!
}
/* global exception handler */
@ExceptionHandler(RuntimeIOException.class)
public void handleRuntimeIOException(final RuntimeIOException runtimeIOException)
'【 개발 이야기 】' 카테고리의 다른 글
e.stackTrace와 e.stackTraceToString (1) | 2025.06.18 |
---|---|
[kotlin] infix function (0) | 2025.06.17 |
[IntelliJ] 초심자를 위한 디버깅 시작하기 (2) | 2025.06.04 |
IV (0) | 2025.06.02 |
[git] rebase 이해하기 (0) | 2025.06.02 |