[토비의 스프링] 4장 예외

susu·2022년 11월 8일
0
post-thumbnail

📌 초난감 예외처리

나쁜 예외처리

1. 예외를 잡기만 하고 아무런 처리를 하지 않는 경우

try {

	...

} catch (Exception e) {
}

예외를 보고도 무시해버리겠다는 태도는 매우 나쁜 태도이다.
특히 자바, 스프링 개발자라면 더더욱 …
한 곳에서 발생한 예외가 캐스케이드되어 다른 곳에서 또 다른 에러를 일으킬 수 있다.

2. 예외 상황을 출력만 해주는 경우

try {

	...

} catch (Exception e) {
	**System.out.println(e);**
}
try {

	...

} catch (Exception e) {
	**e.printStackTrace();**
}

예외가 발생했다는 것을 감지할 수는 있겠으나, 다른 디버깅 로그에 묻혀 놓칠 가능성이 크다.
예외를 처리할 때에 반드시 지켜야 할 핵심 원칙은 딱 하나다.
모든 예외는 적절하게 복구되어야 하며,
그게 아니라면 작업을 중단시키고 개발자에게 분명하게 통보되어야 한다.

3. 의미없는 throws

...

public void method1() throws Exception {
	
	method2();
}

public void method2() throws Exception {

	method3();
}

public void method3() throws Exception {
	...
}

throws Exception 을 쓰면 해당 예외를 무조건 던지게 된다.
하지만 실행 중 발생할 수 있지만 발생하면 안되는 예외와는 분명 구분되어야 한다.
결과적으로 적절한 처리를 통해 복구될 수 있는 예외 상황도 제대로 다룰 수 있는 기회를 박탈당한다.
이러한 예외처리는 지양되어야 한다.

예외의 종류와 특징

자바에서 throw를 통해 발생시킬 수 있는 예외에는 크게 세 가지가 있다.

1. Error

java.lang.Error 클래스의 서브클래스들이다.
주로 시스템에 뭔가 비정상적인 상황이 발생했을 경우에 사용된다. (주로 자바 VM상에서 발생하는 오류)
따라서 애플리케이션 코드 상에서 잡으려고 하면 안 된다.
즉, 시스템 레벨에서 특별한 작업을 하는 게 아니라면 애플리케이션에서 해당 예외에 대해 신경 쓸 필요는 없다.

2. Exception과 체크 예외

체크 예외는 명시적으로 처리가 필요한 예외이며,
추가적으로 RuntimeException을 상속하지 않는 예외이기도 하다.

체크 예외가 발생할 수 있는 메소드를 사용할 경우,
복구가 가능한 예외들이기 때문에 반드시 예외를 처리하는 코드외 함께 작성해야 한다.

catch문으로 예외를 잡거나 throws로 예외를 자신을 호출한 클래스로 메소드 밖으로 던져서 해결해야 하는데, 이를 해결하지 않으면 컴파일 에러가 발생한다

3. RuntimeException과 언체크/런타임 예외

java.lang.RuntimeException 클래스를 상속한 예외들은 명시적인 예외처리를 강제하지 않으므로 언체크 예외런타임 예외라고도 한다.
이들 역시 Error와 마찬가지로 catch문으로 잡으려 하거나 throws로 던지지 않아도 되지만, 명시적으로 잡거나 throws 해줘도 된다.

예외 처리 방법

1. 예외 복구하기

첫번째 예외처리 방법은 예외 상황을 파악하고 문제를 해결해 정상화하는 복구다.

  • 예를 들어 어떤 파일의 I/O 시도에 대한 IOException이 발생했다 치자.
    이때 사용자에게 상황을 알려주고 다른 파일을 이용하도록 안내하는 것을 통해 문제를 해결할 수 있다.
    예외로 인해 작업이 불가능하므로 다른 작업으로 유도하는 것이다.
    이러한 경우 예외 상황을 정상화하여 복구한 것으로 볼 수 있다.
    하지만 이러한 IOException이 사용자에게 그대로 던져질 경우 예외가 복구됐다고 볼 수 없다.
  • 원격 DB에 접속하다 실패해서 SQLException이 발생하는 경우 재시도를 해볼 수 있다.
    네트워크 접속이 원활하지 않아 예외가 발생했으므로 일정 시간 대기 후 다시 접속을 시도하게 하여 복구를 시도하는 것이다.
    ```java
    // 재시도를 통해 예외를 복구하는 예제 코드
    
    int maxretry = MAX_RETRY;
    
    while (maxretry -- > 0) {
    	try {
    		... // 예외 발생 가능성이 있는 시도
    		return; // 작업 성공
    	} 
    	catch (someException e) {
    		// 로그 출력. 정해진 시간만큼 대기
    	}
    	finally {
    		// 리소스 반납. 정리 작업.
    	} 
    }
    ```

즉 예외의 처리란 사용자가 예외가 발생했다는 것을 알려야 할지라도,
애플리케이션 상에서는 정상적으로 설계된 작업 흐름을 따라 진행되어야 함을 의미한다.

예외처리를 강제하는 체크 예외들은 이렇게 예외를 어떻게든 복구할 가능성이 있는 경우에 사용한다.
API를 사용하는 개발자로 하여금 예외상황이 발생할 수 있음을 인식하도록 도와주고,
이에 대한 적절한 처리를 시도해보도록 요구하는 것이다.

2. 예외처리 회피

예외처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져버리는 것이다.
throws 문으로 선언해서 예외가 발생하면

  • 알아서 던져지게 하거나
  • catch 문으로 일단 예외를 잡은 후에 로그를 남기고 다시 예외를 던지는(= rethorw)

것이다.
즉, 직접 처리하는 것을 회피하는 것이다.

예외처리를 회피하려면 반드시 다른 객체나 메소드가 예외를 대신 처리할 수 있도록 아래와 같이 던져줘야 한다.

public void add() throws SQLException {
	// JDBC API
}
public void add() throws SQLException {
	try {
	 // JDBC API
	} catch(SQLException e) {
		// 로그 출력
		throw e;
	}
}

JDBC와 같은 템플릿이 사용하는 콜백 객체들의 메소드에는 모두 throws SQLException이 붙는다.
SQLException을 처리하는 일은 콜백 객체의 역할이 아니라고 보기 때문이다.

하지만 콜백과 템플릿처럼 긴밀하게 역할을 분담하고 있는 관계가 아니라면
자신의 코드에서 발생하는 예외를 그냥 던져버리는 건 무책임한 책임회피일 수 있다.

예외를 회피하려면 회피하는 의도가 분명해야 한다.
콜백/템플릿처럼 긴밀한 관계에 있는 다른 객체에게 예외처리 책임을 분명히 지게 하거나,
자신을 사용하는 쪽에서 예외를 다루는 게 최선의 방법이라는 확신이 있어야 한다.

3. 예외 전환

예외를 복구해 정상 상태로 만들 수 없기에 예외를 메소드 밖으로 던지는 것이다.
예외 회피와 비슷하지만, 발생한 예외를 그대로 넘기지 않고 적절한 예외로 전환해서 던진다는 특징이 있다.

예외 전환은 보통 두 가지 목적으로 사용한다.

  • 내부에서 발생한 예외를 그대로 던지면 예외의 의미를 확인하기 어려우므로,
    의미를 분명하게 해줄 수 있는 예외로 바꿔주기 위한 경우.
  • 예외를 처리하기 쉽고 단순하게 만들고자 포장(wrap)하는 경우

첫번째 목적은 예상하고 복구 가능한 예외 상황에서 사용한다.
(ex. 아이디 중복으로 인해 발생하는 SQLException)

public void add(User user) throws DuplicatieUserException, SQLException {
	try {

		// JDBC를 이용해 user 정보를 추가하는 코드,
		// 또는 SQLException이 발생할 가능성이 있는 또 다른 메소드 코드

	} catch (SQLExcpeption e) {
		// Errorcode가 MySQL의 "Duplicate Enrty(1062)" 이면 예외 전환
		if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY) throw DuplicateUserIdException();
		else throw e;
	}
}

보통 전환하는 예외에 원래 발생한 예외를 담아 중첩 예외로 만드는 것이 좋다.
중첩 예외는 getCause() 메소드를 이용해 처음 발생한 예외에 관해 확인할 수 있다.

두번째 목적은 예외처리를 강제하는 체크 예외를 언체크 예외인 런타임 예외로 바꾸려는 경우에 사용한다.
대표적으로 EJBException을 들 수 있다.
EJB 컴포넌트 코드에서 발생하는 대부분의 체크 예외는 비즈니스 로직으로 볼 때 의미있는 예외거나 복구 가능한 예외가 아니므로,
이런 경우 런타임 예외인 EJBException으로 포장해서 던지는 편이 낫다.

예외처리 전략

런타임 예외의 보편화

실제 엔터프라이즈 서버 환경에서는 수많은 사용자가 동시에 요청을 보내고,
각 요청이 독립적인 작업으로 취급된다.
애플리케이션 차원에서 예외상황을 미리 파악하고, 예외가 발생하지 않도록 차단하는 게 좋다.

또는 프로그램 오류나 외부 환경으로 인해 예외가 발생하는 경우,
빨리 해당 요청의 작업을 취소하고 서버 관리자나 개발자에게 통보해주는 편이 낫다.

자바의 환경이 서버로 이동하면서 체크 예외의 활용도와 가치는 점점 떨어지고 있다.
따라서 대응이 불가능한 체크 예외라면 빨리 RuntimeException으로 전환해 던지는 것이 낫다.

최근의 표준 또는 오픈소스 프레임워크에서는 API가 발생시키는 예외를 체크 예외 대신 언체크 예외로 정의하는 것이 일반화되고 있다.

애플리케이션 예외

런타임 예외 중심의 전략은 복구할 수 있는 예외는 없다고 가정하고,
예외가 생겨도 어차피 런타임 예외이므로 시스템 레벨에서 알아서 처리해줄 것이고,
꼭 필요한 경우는 런타임 예외라도 잡아서 복구하거나 대응해줄 수 있으니 문제될 것이 없다는 낙관적인 태도를 기반으로 하고 있다.

반면 애플리케이션 외부의 예외상황이 원인이 아닌,
애플리케이션 자체 로직에 의해 의도적으로 발생시키고 반드시 catch해서 조치를 취하도록 요구하는 예외도 있다.
이런 예외들을 일반적으로 애플리케이션 예외라고 한다.

📌 예외 전환

DAO를 굳이 따로 만들어서 사용하는 이유는,
데이터 액세스 로직을 담은 코드를 다른 성격의 코드들로부터 분리해놓기 위함이다.

하지만 DAO의 사용 기술과 구현 코드는 전략 패턴과 DI를 통해 이를 사용하는 클라이언트에게 감출 수 있지만,
메소드 선언에 나타나는 예외정보가 문제가 될 수 있다.

따라서 DAO의 인터페이스를 분리해 기술에 독립적인 인터페이스로 만들어야 한다.
그러기 위해선 인터페이스 도입예외 전환, 기술에 독립적인 추상화된 예외로 전환해야 한다.

스프링의 DataAccessException

스프링은 자바의 다양한 데이터 액세스 기술을 사용할 때 발생하는 예외들을 추상화해서 DataAccessException 계층구조 안에 정리해놓았다.

🗣 스프링에서 제공하는 데이터 접근 계층의 모든 예외는 런타임 예외이다.
NonTransient 예외 : 일시적인 예외로, 동일한 SQL을 수행할 경우 성공할 가능성이 있다.
Transient 예외 : 일시적이지 않은 예외, SQL 문법 오류나 제약조건 위배 등

  • JDO, JPA, Hibernate처럼 객체나 엔티티 단위로 정보를 업데이트하는 경우 낙관적인 락킹이 발생할 수 있다.
    같은 정보를 두 명 이상의 사용자가 동시에 조회하고 순차적으로 업데이트 할 때,
    뒤늦게 업데이트한 것이 먼저 업데이트한 것을 덮어쓰지 않도록 할 수 있는 편리한 기능이다.
    스프링의 예외 전환 방법을 적용하면 JPA를 썼든 Hibernate를 썼든 상관 없이 DataAccessException의 서브클래스인 ObjectOptimisticLockinFalureException으로 통일시켜 처리할 수 있다.
  • 또 SQL 쿼리를 통해 반환된 결과가 예상과 다른 경우 예외를 발생시켜주기 위해 IncorrectResultSizeDataAccessException이 서브클래스로 정의되어 있다.

DataAccessException 활용 시 주의사항

DataAccessException이 기술에 구애받지 않고 어느정도 추상화된 공통 예외로 변환해주긴 하지만,
근본적인 한계 때문에 완벽함을 기대할 수는 없다.
만약 DAO에서 사용하는 기술의 종류와 상관없이 동일한 예외를 얻고 싶다면 직접 예외를 정의해두고 각 메소드에서 좀 더 상세한 예외 전환을 해줄 필요가 있다.

📌 정리

  • 예외를 잡아 아무런 조치를 취하지 않는다거나, 의미없이 throw 선언을 남발하는 것은 위험함.
  • 예외는 복구하거나, 예외처리 객체를 만들거나 찾아서 적절한 예외로 전환되도록 해야 함.
  • 복구할 수 없는 예외는 런타임 예외로 전환하는 것이 바람직.
  • 애플리케이션의 로직을 담기 위한 예외는 체크 예외로 만드는 것이 좋음.
  • JDBC의 SQLException은 대부분 복구할 수 없는 예외이므로 런타임 예외로 포장해야 한다.
  • SQLException은 DB에 종속되는 예외이므로, DB에 독립적인 예외로 전환되어야 함.
profile
블로그 이사했습니다 ✈ https://jennairlines.tistory.com

0개의 댓글