1. throws
지금까지는 예외를 처리하는 방법을 배웠습니다. 이제부터는 예외를 발생시키는 방법을 알아봅시다. 정확하게 말하면 자바에서는 예외를 던질 수 있습니다. 다음 예제를 봅시다.
package c.exception;
public class ExceptionSample {
public static void main(String[] args) {
ExceptionSample sample = new ExceptionSample();
sample.throwException(13);
}
public void throwException(int number) {
try {
if(number>12) {
throw new Exception("Number is over than 12");
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
java.lang.Exception: Number is over than 12
at c.exception.ExceptionSample.throwException(ExceptionSample.java:13)
at c.exception.ExceptionSample.main(ExceptionSample.java:7)
1년 열두 달을 처리하는 로직이 필요하다고 생각해봅시다, 메소드의 매개 변수로 13월이 넘어왔다면, 정상적인 처리가 불가능 할 것입니다. 이럴 때 반드시 예외를 발생 시킬 필요는 없지만, 필요에 의해서 "우리는 13월이라는 것은 존재하지 않으니 예외를 발생시킨다"고 정의할 수 있습니다. 이러한 상황에서 try 블록 내에서 throw라고 명시 한 후 개발자가 예외 클래스의 객체를 생성하면 됩니다. 그러면 다른 예외가 발생한 상황과 동일하게 throw 한 문장 이후에 있는 모든 try 블록 내의 문장들은 수행되지 않고, catch 블록으로 이동합니다.
catch 블록 중에 throw한 예외와 동일하거나 상속 관계에 있는 예외가 있다면 그 블록에서 예외를 처리할 수 있습니다. 만약 해당하는 예외가 없다면 메소드 밖으로 던져버립니다. 이럴 때 사용하는 것이 throws 구문입니다. throws 구문은 다음과 같이 메소드 선언할 때 사용하면 됩니다.
package c.exception;
public class ExceptionSample {
public static void main(String[] args) {
ExceptionSample sample = new ExceptionSample();
try {
sample.throwException(13);
} catch(Exception e) {
e.printStackTrace();
}
}
public void throwException(int number) throws Exception{
if(number>12) {
throw new Exception("Number is over than 12");
}
}
}
try-catch 블록으로 throwException 메소드를 묶지 않아도 throws가 선언되어 있기 때문에 전혀 문제가 없습니다. 하지만 이렇게 throws로 메소드를 선언하면 개발이 어려워집니다. 왜냐하면, 이 throwException()이라는 메소드는 Exception을 던진다고 throws 선언을 해 놓았기 때문입니다. throwException() 메소드를 호출한 메소드에서는 throwException() 메소드를 수행하는 부분에는 반드시 try-catch 블록으로 감싸주어야합니다. 그렇지 않을 경우에는 컴파일 에러가 발생합니다. 이렇게 해 놓고 main() 메소드에서 throwException()메소드를 호출합니다. throws 구문으로 선언되어 있는 메소드를 호출한 메소드에서 try-catch로 그 호출 부분을 감싸야 합니다.
하지만 반드시 이렇게 try-catch로 묶어 주어야 하는 것은 아닙니다. 예외를 throws하는 메소드를 호출한 메소드에서도 다시 throws 해 버리면 됩니다. 하지만 이미 throws한 것을 throws하는 방법은 그리 좋은 습관이 아닙니다. 가장 좋은 방법은 throws 하는 메소드를 호출하는 메소드에서 try-catch로 처리하는 것입니다.
package c.exception;
public class ExceptionSample {
public static void main(String[] args) throws Exception{
ExceptionSample sample = new ExceptionSample();
sample.throwException(13);
}
public void throwException(int number) throws Exception{
if(number>12) {
throw new Exception("Number is over than 12");
}
}
}
이 클래스를 컴파일하고 실행하면 다음과 같이 예외 메시지가 나타납니다.
Exception in thread "main" java.lang.Exception: Number is over than 12
at c.exception.ExceptionSample.throwException(ExceptionSample.java:12)
at c.exception.ExceptionSample.main(ExceptionSample.java:7)
throws와 throw에 대해서 정리해 보겠습니다.
● 메소드를 선언할 때 매개 변수 소괄호 뒤에 throw라는 예약어를 적어 준 뒤 예외를 선언하면, 해당 메소드에서 선언한 예외가 발생했을 때 호출한 메소드로 예외가 전달된다. 만약 메소드에서 두 가지 이상의 예외를 던질 수 있다면, implements처럼 콤마로 구분하여 예외 클래스 이름을 적어주면 된다.
● try 블록 내에서 예외를 발생시킬 경우에는 throw라는 예약어를 적어 준 두 예외 객체를 생성하거나, 생성되어 있는 객체를 명시해준다. throw한 예외 클래스가 catch 블록에서 선언되어 있지 않거나, throws 선언에 포함되어 있지 않으면 컴파일 에러가 발생한다.
● catch 블록에서 예외를 throw할 경우에도 메소드 선언의 throws 구문에 해당 예외가 정의되어 있어야만 한다.
예외를 throw하는 이유는 해당 메소드에서 예외를 처리하지 못하는 상황이거나 미처 처리하지 못한 예외가 있을 경우에 대비하기 위함입니다. 자바에서 예외 처리를 할 때 throw와 throws는 매우 중요합니다.
throws를 이용해서 예외처리 책임을 무분별하게 전가하지 말아야합니다. 모든 예외는 적절하게 복구되든지, 아니면 작업을 중단시키고 개발자에게 통보돼야 합니다.
2. 내가 예외를 만들 수도 있다고?
Error와 관련된 클래스는 개발자가 손댈 필요도 없고, 손 대어서도 안됩니다. 하지만 Error가 아닌 Exception을 처리하는 예외 클래스는 개발자가 임의로 추가해서 만들 수 있습니다. 단 한가지 조건이 있습니다. Throwable이나 그 자식 클래스의 상속을 받아야만 한다는 것입니다. Throwable 클래스의 상속을 받아도 되지만, Exception을 처리하는 클래스라면 java.lang 패키지의 Exception 클래스의 상속을 받는 것이 좋습니다.
package c.exception;
public class MyException extends Exception{
public MyException() {
super();
}
public MyException(String message) {
super(message);
}
}
예외 클래스가 되기 위한 조건은 위와 같이 간단합니다. 예외 클래스를 확장하면 됩니다. 이렇게 만든 예외 클래스는 어떻게 사용할까요? 앞에서 사용한 예제 메소드를 복사하여 다음과 같이 throwException2()라는 메소드를 만듭시다.
package c.exception;
public class ExceptionSample {
public static void main(String[] args) throws Exception{
ExceptionSample sample = new ExceptionSample();
sample.throwException2(13);
}
public void throwException(int number) throws Exception{
if(number>12) {
throw new Exception("Number is over than 12");
}
}
public void throwException2(int number) throws Exception{
try {
if(number>12) {
throw new Exception("Number is over than 12");
}
} catch(MyException e) {
e.printStackTrace();
}
}
}
이와 같이 우리가 직접 만든 예외를 던지고, catch 블록에서 사용하면 됩니다. MyException이 예외 클래스가 되려면 Throwable 클래스의 자식 클래스가 되어야 합니다. 만약 MyException을 선언할 때 관련된 클래스를 확장하지 않았을 때 이 부분에서 제대로 컴파일이 되지 않습니다.
3. 자바 예외 처리 전략
자바에 예외를 처리할 때에는 표준을 잡고 진행하는 것이 좋습니다. 아니, 표준을 정해 두어야만 합니다. 어떻게 예외를 처리할지를 알아두는 것이 좋습니다. 물론 완벽한 전략은 아니고 상황에 따라 맞추어 사용하는 것이 좋습니다.
앞에서 예외를 직접 만들 때 Exception 클래스를 확장하여 나만의 예외 클래스를 만들었습니다. 그런데, 이 예외가 항상 발생하지 않고, 실행시에만 발생할 확률이 매우 높은 경우에는 런타임 예외로 만드는 것이 나을 수도 있습니다. 즉, 클래스 선언시 extends Exception 대신에 extends RuntimeException으로 선언하는 것입니다. 이렇게 되면, 해당 예외를 던지는 메소드를 사용하더라도 try-catch로 묶지 않아도 컴파일시에 예외가 발생하지 않습니다. 하지만, 이 경우에는 예외가 발생할 경우 해당 클래스를 호출하는 다른 클래스에서 예외를 처리하도록 구조적인 안전장치가 되어 있어야만 합니다. 여기서 안전 장치란 try-catch로 묶지 않은 메소드를 호출하는 메소드에서 예외를 처리하는 try-catch가 되어 있는 것을 이야기합니다.
그리고 여러분들이 catch 문장에서 처리를 할 때 다음과 같이 처리하는 것은 반드시 피해야만 합니다.
try{
//예외 발생 가능한 코드
} catch(SomeException e){
//여기 아무 코드 없음
}
catch 문장 내에서 아무런 처리를 하지 않는 것입니다. 이렇게 되면, 나중에 문제가 발생해서 예외가 발생했을 경우 catch 문 내에서 아무런 작업을 하지 않기 때문에 문제가 어디서 발생했는지 전혀 찾을 수가 없습니다. 따라서 개발 표준을 잡을 때 catch 문 내에서 어떻게 처리할지를 명시적으로 선언해 두어야만 합니다. 다시 말해서 catch 문에서 로그에 남기는 등의 작업을 하고 예외를 throw를 이용하여 던져주어야 문제가 발생한 정확한 원인을 찾을 수 있습니다.
현재 런타임 예외가 보편화되고 있는 추세입니다. 런타임 예외는 catch를 통해 복구나 처리할 수 있습니다. 하지만 보통은 복구가 불가능하고, 런타임 예외로 던져야하기 때문에 API를 만들때부터 런타임 예외를 던지도록 만듭니다.
● 임의의 예외 클래스를 만들 때에는 반드시 try-catch 로 묶어줄 필요가 있을 경우에만 Exception 클래스를 확장한다.
● 일반적으로 실행시 예외를 처리할 수 있는 경우에는 RuntimeException 클래스를 확장하는 것을 권장한다(아주 중요함)
● catch 문 내에 아무런 작업 없이 공백을 놔 두면 안된다.
4. 어떤 예외를 어떻게 처리할 것인가?
어떤 서비스 계층에서 user정보를 삽입하는 SQL로직에서 에러가 났다고 생각해봅시다. 만약 USER의 아이디가 중복되어 유저 삽입이 실패했다면 DuplicateUserIdException과 같이 예외를 바꿔서 던져주는 것이 좋습니다. 이렇게 발생하는 예외의 종류에 따라 예외를 처리할 객체가 무엇인지 구분하는 것도 중요합니다.
DuplicatedUserIdException은 충분히 복구 가능한 예외이므로 다른 메소드를 호출하여 처리할 수 있습니다. 굳이 체크예외로 만들지 말고, 런타임 예외로 만드는 것이 낫습니다. 하지만 대부분 복구가 불가능하기 때문에 런타임 예외로 포장해서 던지서 다른 메소드들이 신경쓰지 않게 해주는것이 편합니다.
public void add(User user) throws DuplicateUserIdException, SQLException{
try{
//user 정보를 DB에 추가하는 코드
} catch(SQLException e){
//ErrorCode가 Mysql의 "Duplicate Entry(1062)" 이면 예외 전환
if(e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY)
throw DuplicateUserIdException();
else
throw e; // 그 외의 경우는 SQLException 그대로 처리
}
}
5. 예외 처리 방식 코드 예제
5.1 예외 복구 방식
다음 코드는 예외를 복구하는 방식의 코드입니다. maxretry만큼 예외가 발생할 수 있는 코드를 시도하는 방식입니다. 네트워크 환경이 좋지 않아 다시 시도하는 방식에서 효율적입니다. 최대 횟수를 넘기면 예외를 직접 발생시킵니다.
int maxretry = MAX_RETRY;
while(maxretry -- > 0) {
try {
// 예외가 발생할 가능성이 있는 시도
return; // 작업성공시 리턴
}
catch (SomeException e) {
// 로그 출력. 정해진 시간만큼 대기
}
finally {
// 리소스 반납 및 정리 작업
}
}
throw new RetryFailedException(); // 최대 재시도 횟수를 넘기면 직접 예외 발생
5.2 예외처리 회피 방식
다음은 예외처리 회피 방식입니다. 이 방식은 예외를 던지고 그 처리를 회피하는 방식이기 때문에 호출한 방식에서 다시 예외를 받아서 처리하도록 하거나, 해당 메소드에서 이 예외를 던지는 것이 최선의 방법이라고 있을때만 사용해야 합니다.
public void add() throws SQLException {
... // 구현 로직
}
5.3 예외 전환
catch(SQLException e) {
...
throw DuplicateUserIdException();
}
예외를 처리하는 방법보다도 예외를 잡고 아무런 처리도 하지 않는 것은 정말 위험한 행위입니다. catch를 두고 비워두거나 예외가 발생했을 때 그 원인을 파악하기가 어려워 개발과 유지보수에 어려움을 겪을 수 있습니다. 따라서 어떤 처리를 하는지 모르더라도 무자적 catch하고 무시하거나, throw해버리는 행위를 할 때는 신중해야합니다.
참고사이트
'Java' 카테고리의 다른 글
[Java] 22. JAR (Java ARchive Files) 파일 (0) | 2019.06.04 |
---|---|
[Java] 21. 어노테이션 (0) | 2019.04.20 |
[Java] 20. 예외 처리(2) (0) | 2019.04.18 |
[Java] 20. 예외 처리(1) (0) | 2019.04.17 |
[Java] 19. 중첩 클래스(Nested Class) (0) | 2019.04.11 |