자바의 런타임 계열 예외와 checked 예외
자바에서 예외(Exception)은 크게 checked 예외와 unchecked 예외로 나뉘어진다. checked 예외는 코드에서 명시적으로 try-catch-finally 예외 처리를 해야하는 것을 의미하며, unchecked 예외는 그럴 필요가 없는 것을 의미한다. checked 예외에서 try-catch로 예외를 처리하지 않는 경우에는 메소드에 throws 절을 추가해야 한다.
자바에서 checked 예외는 java.lang.Exception 을 상속받는 형태이며, unchecked 예외는 java.lang.RuntimeException을 상속받는 예외이다. checked 예외이든 unchecked 예외이든 두가지 모두 동일한 기능을 수행한다. 따라서, 어느 것이 더 낫다라고 말할 수는 없다. 하지만, 예외 발생시 어떠한 로직을 추가하느냐에 따라서 그 비용적인 측면은 다양할 수 있다.
또한, 트랜잭션, 원격 호출과 같은 지점에서 주로 런타임 계열의 unchecked 예외를 사용하는 것이 일반적이며, 이는 기반 기술 (J2EE, Spring)들이 런타임 계열의 예외에 대해 자동으로 트랜잭션 처리를 해주고 있다. (Spring에서는 checked 예외에 대해서는 트랜잭션 처리가 가능하지만, 관행과 같이 런타임 계열을 사용하는 방식을 권고한다.)
(눈여겨 볼것은 바로 예외발생시 트랜잭션의 roll-back 여부이다. 기본적으로 Checked Exception은 예외가 발생하면 트랜잭션을 roll-back하지 않고 예외를 던져준다. 하지만 Unchecked Exception은 예외 발생 시 트랜잭션을 roll-back한다는 점에서 차이가 있다. 트랜잭션의 전파방식 즉, 어떻게 묶어놓느냐에 따라서 Checked Exception이냐 Unchecked Exception이냐의 영향도가 크다. roll-back이 되는 범위가 달라지기 때문에 개발자가 이를 인지하지 못하면, 실행결과가 맞지 않거나 예상치 못한 예외가 발생할 수 있다. 그러므로 이를 인지하고 트랜잭션을 적용시킬 때 전파방식(propagation behavior)과 롤백규칙 등을 적절히 사용하면 더욱 효율적인 애플리케이션을 구현할 수 있을 것이다.)
비즈니스 예외의 경우, 내부적으로 특정 예외를 지정하여 사용할 때에는 checked 예외를 사용하기도 하지만, 런타임 계열의 예외를 사용하는 것이 추가 코드가 들어가지 않고, try-catch로 별도 로직을 구성하지 않는 이상 checked 예외를 사용하는 것은 여러가지로 고려해서 신중할 필요가 있다. 보통 예외를 먹는다는 표현과 같이 try-catch 구문 안에서 checked 예외를 처리하고 catch 절에서 아무런 행위를 하지 않으면, 예외 발싱시 문제점을 추적하기가 쉽지 않다.
try {
// some business logic
} catch (SomeCheckedException e) {
e.printStackTrace();
// do nothing
}
위의 코드가 대표적으로 예외를 먹는 행위이다. 물론, 예외 처리를 하지 않는 명확한 이유가 있고, 그 안에서 대부분의 예외를 처리한다고 하면, 이와 같은 코드는 크게 상관은 없을 수도 있겠지만, 일반적으로 보아서, 예외를 catch하고 이를 호출하는 측에 아무런 반응을 알리지 않는다면 좋지 않은 코드가 될 가능성이 높다.
또한, 예외를 예외로 받지 않고, 예외 발생시 다양한 코드를 받게끔 처리하는 경우도 있는데, 이러한 경우 예외에 대한 모든 경우들을 검토해야 한다. (마치 HTTP로 통신시 예외에 대한 코드를 정의하듯이 예외코드와 정확한 의미를 상세하게 정의해야 한다.)
이러한 다양한 경우들을 처리하는 것에 대한 부담이 있고, 어떻게 처리할지를 잘 모르겠다면 시스템 내부에서의 예외는 가능하다면 모두 Runtime 계열로 처리하고, UI 접점에서 이러한 예외들을 처리하게끔 하는 것이 가장 나은 방법인 것 같다. UI에서 이러한 다양한 예외들을 어떻게 보여줄 것인지에 대한 고민은 사용자의 요구사항을 들을 필요가 있지만, 대부분이 그냥 시스템에 문제가 있다라는 형태의 오류만을 보기를 원할 것이다. 사실 예외 메시지에 대한 대부분은 개발자들이 보고 싶은 데이터이지 사용자에게 의미있는 정보들은 아니다. (물론, 입력 폼 처리에서 어떠한 정보가 누락되었는지 정합성이 문제가 되었는지는 가능하면 상세하게 보여줄 필요가 있지만, DB 오류 코드나, SQL 구문, 내부 시스템 네트워크 문제, 잘못된 코드 등을 사용자에게까지 보여줄 필요는 없을 것이다.)
결국, 예외는 개발/운영하는 사람들을 위해서 수집하도록 하면 되고, 문제 발생시 추적하여 해결할 수 있는 수준에서 예외 처리를 하면 된다. 즉, UI 접점 내의 서버에는 모든 예외가 런타임 계열로 정의하고(물론, 필요한 경우 checked 예외를 선언하여 처리하면 된다. 다만 이 경우는 세심한 주의가 필요할 뿐이다.), UI에서 발생되는 예외를 비동기로 분석하여 추후 문제 해결시 사용하면 된다. 예외는 전체 시스템 차원에서 처리하는 방식과 일정한 형식을 필요로 하며, 문제를 해결할 수 있는 수준에서 적당하게 사용하면 된다. 또한, 예외코드 사용시 상세한 내용을 문서화/명세화시켜서 남겨두어야 한다.
예외를 처리하는 3가지 방법
1. 예외 복구
[리스트 1] 재시도를 통해 예외를 복구하는 코드
예외복구의 핵심은 예외가 발생하여도 애플리케이션은 정상적인 흐름으로 진행된다는 것이다. 위 [리스트 1]은 재시도를 통해 예외를 복구하는 코드이다. 이 예제는 네트워크가 환경이 좋지 않아서 서버에 접속이 안되는 상황의 시스템에 적용하면 효율 적이다. 예외가 발생하면 그 예외를 잡아서 일정 시간만큼 대기하고 다시 재시도를 반복한다. 그리고 최대 재시도 횟수를 넘기면 예외를 발생시킨다. 재시도를 통해 정상적인 흐름을 타게 한다거나, 예외가 발생하면 이를 미리 예측하여 다른 흐름으로 유도시키도록 구현하면 비록 예외가 발생하였어도 정상적으로 작업을 종료할 수 있을 것이다.
2. 예외처리 회피
[리스트 2] 예외처리 회피
위 [리스트 2]는 간단해 보이지만 아주 신중해야하는 로직이다. 예외가 발생하면 throws를 통해 호출한쪽으로 예외를 던지고 그 처리를 회피하는 것이다. 하지만 무책임하게 던지는 것은 위험하다. 호출한 쪽에서 다시 예외를 받아 처리하도록 하거나, 해당 메소드에서 이 예외를 던지는 것이 최선의 방법이라는 확신이 있을 때만 사용해야 한다.
3. 예외 전환
catch(SQLException e) {
...
throw DuplicateUserIdException();
}
[리스트 3] 예외전환
예외 전환은 위 [리스트 3]에서 처럼 예외를 잡아서 다른 예외를 던지는 것이다. 호출한 쪽에서 예외를 받아서 처리할 때 좀 더 명확하게 인지할 수 있도록 돕기 위한 방법이다. 어떤 예외인지 분명해야 처리가 수월해지기 때문이다. 예를 들어 Checked Exception 중 복구가 불가능한 예외가 잡혔다면 이를 Unchecked Exception으로 전환하여서 다른 계층에서 일일이 예외를 선언할 필요가 없도록 할 수도 있다.
이상으로 예외를 처리하는 3가지 방법을 알아봤다. 하지만 예외를 처리하는 방법보다도 초급 개발자가 가장 잊지 말아야 할 것은 예외를 잡고 아무런 처리도 하지 않는 것은 정말 위험한 행위라는 것이다. try/catch문으로 예외를 잡아놓고 catch를 비워두면 물론 컴파일 오류는 나지 않겠지만, 예외가 발생했을 때 그 원인을 파악하기가 어려워 개발은 물론 유지보수에 아주 치명적인 민폐를 끼치는 일이라고 생각한다. 따라서 어떤 처리를 해야 하는지 모르더라도 무작정 catch하고 무시하거나, throw해버리는 행위를 할 때는 더욱 신중해야 할 것이다.
트랜잭션 (Transaction)
갑자기 "트랜잭션"이라는것이 나와서 뜬금없다고 생각할 수도 있겠지만 트랜잭션과 예외처리는 매우 밀접한 관련이 있다. 트랜잭션과 예외처리가 서로 어떤 관련이 있는지 알아보도록 하자.
트랜잭션은 하나의 작업 단위를 뜻한다.
쇼핑몰의 "상품발송"이라는 트랜잭션을 가정 해 보자.
"상품발송"이라는 트랜잭션에는 다음과 같은 작업들이 있을 수 있다.
- 포장
- 영수증발행
- 발송
이 3가지 일들 중 하나라도 실패하면 3가지 모두 취소하고 "상품발송"전 상태로 되돌리고 싶을 것이다. (모두 취소하지 않으면 데이터의 정합성이 크게 흔들리게 된다. 이렇게 모두 취소하는 행위를 보통 전문용어로 롤백(Rollback)이라고 말한다.)
프로그램이 다음과 같이 작성되어 있다고 가정 해 보자. (※ 아래는 실제 코드가 아니라 어떻게 동작하는지를 간략하게 표현한 pseudo 코드1이다.)
상품발송() {
포장();
영수증발행();
발송();
}
포장() {
...
}
영수증발행() {
...
}
발송() {
...
}
쇼핑몰 운영자는 포장, 영수증발행, 발송이라는 세가지 중 1가지라도 실패하면 모두 취소하고 싶어한다. 이런경우 어떻게 예외처리를 하는 것이 좋겠는가? ^^
다음과 같이 포장, 영수증발행, 발송 메서드에서는 예외를 throw하고 상품발송 메서드에서 throw된 예외를 처리하여 모두 취소하는 것이 완벽한 트랜잭션 처리 방법이다.
상품발송() {
try {
포장();
영수증발행();
발송();
}catch(예외) {
모두취소();
}
}
포장() throws 예외 {
...
}
영수증발행() throws 예외 {
...
}
발송() throws 예외 {
...
}
위와 같이 코드를 작성하면 포장, 영수증발행, 발송이라는 세개의 단위작업 중 하나라도 실패할 경우 "예외"가 발생되어 상품발송이 모두 취소 될 것이다.
만약 위 처럼 "상품발송" 메서드가 아닌 포장, 영수증발행, 발송메소드에 각각 예외처리가 되어 있다고 가정 해 보자.
상품발송() {
포장();
영수증발행();
발송();
}
포장(){
try {
...
}catch(예외) {
포장취소();
}
}
영수증발행() {
try {
...
}catch(예외) {
영수증발행취소();
}
}
발송() {
try {
...
}catch(예외) {
발송취소();
}
}
이렇게 각각의 메소드에 예외가 처리되어 있다면 포장은 되었는데 발송은 안되고 포장도 안되었는데 발송이 되고 이런 뒤죽박죽의 상황이 연출될 것이다.
실제 프로젝트에서도 두번째 경우처럼 트랜잭션관리를 잘못하여 고생하는 경우를 많이 보았는데 이것은 일종의 재앙에 가깝다.
이번 챕터에서는 자바의 예외처리에 대해서 알아보았다. 사실 예외처리는 자바에서 좀 난이도가 있는 부분에 속한다.
보통 프로그래머의 실력을 평가 할 때 이 예외처리를 어떻게 하고 있는지를 보면 그 사람의 실력을 어느정도 가늠해 볼 수 있다고들 말한다. 예외처리는 부분만 알아서는 안되고 전체를 관통하여 모두 알아야만 정확히 할 수 있기 때문이다.
http://homo-ware.tistory.com/161
http://www.nextree.co.kr/p3239/
https://wikidocs.net/229