본문 바로가기

Spring Framework

[Spring] 예외를 처리하는 방법

반응형

이번 글에서는 예외를 처리하는 방법에 대해 정리해 보려고 합니다. (본 글은 "토비의 스프링 3.1" 책을 읽고 책의 내용과 제 생각을 더해서 쓴 글입니다)

 

예외를 처리할 때 반드시 지켜야 할 핵심 원칙은 모든 예외는 적절하게 복구되든지 아니면 작업을 중단시키고 운영자 또는 개발자에게 분명하게 통보해야 합니다. 

 

앞으로는 이 두 가지의 핵심 원칙을 지켜 자바에서 예외처리를 하기 위한 효과적인 방법에 대해 정리해보려고 합니다.

 

1. 자바에서 throw를 통해 발생시킬 수 있는 예외
2. 예외를 처리하는 일반적인 방법과 효과적인 방법
3. 일괄된 예외처리 전략

 

1. 자바에서 throw를 통해 발생시킬 수 있는 예외

 

자바에서 throw 키워드는 만약 어떤 연산을 하다가 예상치 못한 일이 발생했을 때 Exception을 발생시켜 예외가 처리될 수 있도록 합니다. 애플리케이션이 Exception을 적절히 처리하지 못하면 프로그램이 죽거나 오작동할 수 있기 때문입니다. 

 

자바에서는 throw를 통해 발생 시킬수 있는 3가지 예외 상황이 존재합니다.

  • Error
  • Exception과 예외 체크
  • RuntimeException과 언체크/런타임 예외

먼저 Error는 java.lang.Error클래스의 서브 클래스들을 의미하며 에러는 시스템에 뭔가 비정상적인 상황이 발생했을 경우에 사용됩니다. Error는 JVM에서 발생시키는 것이 아니기 때문에 시스템 레벨에서 특별한 작업을 하는 게 아니라면 애플리케이션에서는 이런 에러에 대한 처리는 신경 쓰지 않아도 됩니다.

 

다음 항목은 개발자가 작성한 애플리케이션 코드의 작업 중에 예외상황이 발생했을 경우에 사용됩니다. java.lang.Exception 클래스는 체크 예외와 언체크 예외(unchecked exception)로 구분됩니다. 전자는 Exception 클래스이면서 RuntimeException 클래스를 상속하지 않는 것들이고 후자는 RuntimeException을 상속한 클래스입니다. 

중요한 점은 사용할 메서드가 체크 예외를 던진다면 이를 catch문으로 잡든지, 아니면 다시 throws를 정의해서 메서드 밖으로 던져야 합니다. 그렇지 않을 경우 컴파일 에러가 발생하기 때문입니다.

 

위의 항목을 읽으면서 궁금할 수 있는 점은 왜 RuntimeException을 따로 분리하는 거 인 것 같습니다. RuntimeException은

일반적으로 애플리케이션에서 예상하지 않은 오류 조건을 표시하기 때문에 코드 레벨에서 trhow를 정의하거나 try catch문으로 잡지 않더라도 컴파일 에러가 발생하지 않게 됩니다. 컴파일 단계에서 예상하지 못하는 에러이기 때문입니다.

 

https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=mk1126sj&logNo=220976674605)

 

2. 예외를 처리하는 일반적인 방법과 효과적인 방법

 

이번에는 위에서 발생하는 예외 상황들을 처리하는 일반적인 방법과 효과적인 방법에 대해 정리해보려고합니다. 두 방법의 구분 기준은 일반적인 방법을 일관성을 부여했는지 유무만 있을 뿐 핵심은 예외를 처리하는 방법을 기준으로 정리하려고 합니다.

 

2-1. 일반적인 방법

 

1. 예외 복구

 

첫 번째 예외 처리 방법은 예외상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것입니다. 예를 들어 네트워크 접속이 불안해 원격 DB에 접속에 실패해서 SQLException이 발생하는 경우에 반복적으로 재시도를 하게 만드는 예외처리 코드를 작성할 수 있습니다. 예외처리 코드를 강제하는 체크 예외들은 이렇게 예외를 어떤 식으로든 복구할 가능성이 있는 경우에 사용합니다. 

 

2. 예외처리 회피 

 

두 번째는 예외처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져 버리는 것입니다. throws문으로 선언해서 예외가 발생하면 알아서 던져지게 하거나 catch문으로 일단 예외를 잡은 후에 로그를 남기고 다시 예외를 던질 수 있습니다. 예외처리를 회피하려면 반드시 다른 오브젝트나 메서드가 예외를 대신 처리할 수 있도록 던져줘야 합니다.

 

3. 예외 전환

 

마지막으로 예외 전환은 예외를 복구해서 정상적인 상태로는 만들 수 없기 때문에 예외를 메서드 밖으로 던지는 것을 의미합니다. 예외처리 회피와 다른 점은 발생한 예외를 그대로 넘기는 게 아니라 적절한 예외로 전환해서 던진다는 점에서 차이가 있습니다.

 

public void add(User user) throws DuplicateUserIdException, SQLException {
	try {
    	// JDBC를 이용해 user정보를 DB에 추가하는 코드 (예시)
    }
    catch(SQLException e) {
    	// ErrorCode가 MySQL의 "Duplicate Entry(1062)" 이면 예외전환
        if(e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY)
        	throw DuplicateUserIdException();
        else 
        	throw e;
    }
}

 

위의 코드를 예로 들자면 새로운 사용자를 등록하려고 시도했을 때 아이디가 같은 사용자가 있어 DB 에러가 발생하면 JDBC API는 SQLException을 발생시킵니다. 이경우 DAO를 이용해 사용자를 추가하려고 한 서비스 계층 등에서는 왜 SQLException이 발생했는지 알기 힘듭니다. (예외처리에 대응하기 유연하지 않습니다)

 

때문에 SQLException의 정보를 해석해 DuplicateUserIdException 같은  예외로 전환해서 던져주면 서비스 계층 오브젝트에서 예외의 원인을 해석하고 대응하는 것이 가능해집니다. 

 

예외 전환을 사용하는 사용하는 데는 두 가지 목적이 있습니다. 

첫 번째는 내부에서 발생한 예외를 그대로 던지는 것이 그 예외 상황에 대한 적절한 의미를 부여해주지 못하는 경우가 있는데 이때 의미를 분명하게 해 줄 수 있는 예외로 바꿔주기 위해서 보통 전환하는 예외에 원래 발생한 예외를 담아서 중첩 예외로 만듭니다. 

 

두 번째는 예외를 처리하기 쉽고 단순하게 만들기 위해 포장하기 위함입니다. 주로 예외처리를 강제하는 체크 예외를 언체크 예외인 런타임 예외로 바꾸는 경우에 사용합니다. 복구가 불가능한 예외라면 애플리케이션 코드에서는 런타임 예외로 포장해서 던져버리고 예외처리 서비스 등을 이용해 자세한 로그를 남기거나 개발자에게 통보하는 방법을 사용하는 것이 적절합니다.

 

일반적인 예외 처리 방법에 간단히 정리하자면 비즈니스 적인 의미가 있는 예외는 이에 대한 적절한 대응이나 복구 작업이 필요하기 때문에 체크 예외(예외처리를 강제함)를 사용하는 것이 적절하고, 시스템 예외는 복구할 만한 방법이 없기 때문에 예외 포장으로 런타임 예외로 인식하게 해 트랜잭션 하도록 설계하는 것이 적절합니다. 

 

2-2. 효과적인 예외 처리 방법(예외처리 전략)

 

1. 런타임 예외의 보편화

 

자바 엔터프라이즈 서버 환경에서는 수많은 사용자가 동시에 요청을 보내고 각 요청이 독립적인 작업으로 취급됩니다. 하나의 요청을 처리하는 중에 예외가 발생하면 해당 작업만 중단시키면 그만이죠. 독립형 애플리케이션과 달리 서버의 특정 계층에서 예외가 발생했을 때 작업을 일시 중지하고 사용자와 바로 커뮤니케이션하면서 예외상황을 복구할 수 있는 방법이 없습니다.  

 

때문에 애플리케이션 차원에서 예외상황을 미리 파악하고 예외가 발생하지 않도록 차단하는 것이 좋습니다. 자바의 환경이 서버로 이동하면서 체크 예외의 활용도와 가치가 점점 떨어지고 있기 때문에 대응이 불가능한 체크 예외라면 빨리 런타임 예외로 전환해서 던지는 게 낫습니다. 

 

public class DuplicateUserIdException extends RuntimeException {
	public DuplicateUserIdException(Thorwable cause){
    	super(cause); // 중첩예외를 만들 수 있도록 생성자 추가
    }
}

public void add(User user) throws DuplicateUserIdException, SQLException {
	try {
    	// JDBC를 이용해 user정보를 DB에 추가하는 코드 (예시)
    }
    catch(SQLException e) {
    	// ErrorCode가 MySQL의 "Duplicate Entry(1062)" 이면 예외전환
        if(e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY)
        	throw new DuplicateUserIdException();
        else 
        	throw new RuntimeException(e);
    }
}

 

위의 코드는 아이디 중복 시 사용하는 예외를 어떤 경우라도 런타임 예외처리로 포장해서 던져버리는 예시입니다.

 

런타임 예외를 던지면서 주의해야 할 점은 컴파일러가 예외처리를 강제하지 않기 때문에 런타임 예외를 사용하는 경우 예외의 종류와 원인, 활용 방법에 대해 자세히 설명해야 합니다. (다음에 런타임 예외에 대해 정리해보겠습니다.)

 

2. 애플리케이션 예외

 

애플리케이션 예외는 시스템 또는 외부의 예외상황이 원인이 아니라 애플리케이션 자체의 로직에 의해 의도적으로 발생시키고, 반드시 catch 해서 무엇인가 조치를 취하도록 요구하는 예외입니다. 

 

예를 들어 사용자가 요청한 금액을 은행 계좌에서 출금하는 기능을 가진 메서드가 있다고 가정하면, 현재 잔고를 확인하고 허용하는 범위를 넘어서 출금을 요청했을 때는 작업을 중지하는 등에 예외처리를 고려해야 합니다. 

 

애플리케이션 예외를 처리할 수 있는 방법은 정상적인 흐름을 따르는 코드는 그대로 두고, 잔고 부족과 같은 예외 상황에서는 비즈니스적인 의미를 띤 예외를 던지도록 만드는 것입니다. 

 

try {
	BigDecial balance = account.withdraw(amount); // 인출 로직
} 
catch (InsufficientBalanceException e) {
	// InsufficientBalanceException에 담긴 인출 가능한 잔고금액 정보를 가져온다.
    BigDecial availFunds = e.getAvailFunds();
    
    // 잔고 부족 안내 메시지를 준비하고 출력하도록 진행
}

 

위의 코드에서 InsufficientBalanceException라는 인출 과정과 같은 비즈니스적인 의미를 띤 예외를 던지도록 만들 수 있습니다. 

 

이번 글에서는 예외가 발생하는 상황과 예외를 처리하는 방법에 대해 정리했습니다. 내용에 부족이 있다면 댓글 남겨주시면 감사하겠습니다.

반응형

'Spring Framework' 카테고리의 다른 글

[Spring] spring에 mybatis 적용하기  (0) 2022.11.26
[Spring] PSA(Portable Service Abstraction)  (1) 2022.11.05
[Spring] JdbcTemplate  (0) 2022.10.22
[Spring] TDD란?  (0) 2022.10.14
[Spring] IoC/DI 컨테이너  (0) 2022.10.12