스터디/[토비의 스프링 3.1 Vol 1] (2025.03)

[토비의 스프링 3.1 Vol 1] 4장. 예외 처리

ttoance 2025. 3. 29. 09:00
반응형

4.1 사라진 SQLException 

  • JdbcTemplate 적용 이전에 있던 throws SQLException 선언이 적용 후에는 사라졌다. 

 

4.1.1 초난감 예외처리

예외 블랙홀 

  • 예외를 잡고는 아무것도 하지 않는다. 예외 발생을 무시해버리고 정상적인 상황인 것처럼 다음 라인으로 넘어간다.
try {
	...
}
catch (Exception e) {}

 

  • 콘솔 로그를 누군가 계속 모니터링 하지 않는 한 심각한 문제로 남아있게 된다. 
} catch (SQLException e) {
	System.out.println(e);
}



} catch (SQLException e) {
	e.printStackTrace();
}

 

무의미하고 무책임한 throws

  • 위의 케이스보다는 조금 낫긴 하지만 메소드 선언에서 의미 있는 정보를 얻을 수 없다. 

 

4.1.2 예외의 종류와 특징 

 

자바에서 throw통해 발생시킬 수 있는 예외 세 가지

1) Error 

  • java.lang.Error클래스의 서브클래스 
    • 에러는 시스템에 뭔가 비정상적인 상황이 발생했을 경우에 사용된다. 
    • 그래서 주로 자바 VM에서 발생시키는 것이고 애플리케이션 코드에서 잡으려고 하면 안 된다. 
    • OutOfMemoryError나 ThreadDeath 같은 에러는 catch블럭에서 잡아도 대응 방법이 없다. 
    • 따라서, 시스템 레벨에서 특별한 작업을 하는 게 아니라면 애플리케이션에서는 이런 에러에 대한 처리는 신경 쓰지 않아도 된다. 
  • Exception과 체크 예외 
    • java.lang.Exception클래스와 그 서브클래스로 정의되는 예외
    • 에러와 달리 개발자들이 만든 애플리케이션 코드의 작업 중에 예외상황이 발생했을 경우에 사용된다. 
    • 체크 예외와 언체크 예외 unchecked exception 으로 구분된다. 
      • 체크 예외 : Exception 클래스의 서브클래스이면서 RuntimeException 클래스를 상속하지 않은 것
      • 후자 : RuntimeException을 상속한 클래스 

 

  • RuntimeException과 언체크/런타임 예외 
    • java.lang.RuntimeException 클래스를 상속한 예외들은 명시적인 예외처리를 강제하지 않기 때문에 언체크 예외라고 불린다. 
    • 런타임 에러는 주로 프로그램의 오류가 있을 때 발생하도록 의도된 것들이다. 
    • 예시
      • 오브젝트를 할당하지 않은 레퍼런스 변수를 사용하려고 시도했을 때 발생하는 NullPointerException
      • 허용되지 않는 값을 사용해서 메소드 호출할 때 발생하는 IllegalArgumentException 
    • 이런 예외는 코드에서 미리 조건을 체크하도록 주의 깊게 만든다면 피할 수 있지만 개발자 부주의로 발생할 수 있다. 
    • 따라서 런타임 예외는 예상하지 못했던 예외상황에서 발생하는 게 아니기 때문에 굳이 catch나 throws를 사용하지 않아도 되도록 만들었다. 

 

 

 

4.1.3 예외처리 방법 

 

예외 복구 

  • 첫번째 예외처리 방법은 예외상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것이다. 
    • 예를 들어 사용자가 요청한 파일을 읽으려고 시도했는데 해당 파일이 없거나 문제가 생겨서 IOException이 발생했다고 했을때 사용자에게 상황을 알려주고 다른 파일을 이용하도록 안내해서 예외상황을 해결 할 수 있다. 
    • 네트워크가 불안해서 서버 접속 안되는 환경이라면 정해진 횟수만큼 일정시간 대기했다가 다시 재시도 하는 방법이 있을 수 있다. 

예외 회피 

  • 예외처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져 버리는 것이다. 
    • throws문으로 선언해서 예외가 발생하면 알아서 던져지게 하거나 catch문으로 일단 예외를 잡은 후에 로그를 남기고 다시 예외를 던지는 방법이다. 

 

예외 전환 

  • 예외 회피와 비슷하게 예외를 복구해서 정상적인 상태로는 만들 수 없기 때문에 예외를 메소드 밖으로 던지는 것이다. 
  • 예외 회피와 다르게, 발생한 예외를 그대로 넘기는 게 아니라 적절한 예외로 전환해서 던진다는 특징이 있다. 
  • 예외 전환은 보통 두 가지 목적으로 사용된다. 
    • 첫째는 내부에서 발생한 예외를 그대로 던지는 것이 그 예외상황에 대한 적절한 의미를 부여해주지 못하는 경우에, 의미를 분명하게 해줄 수 있는 예외로 바꿔주기 위해서이다. 
      • 예를 들어, 새로운 사용자를 등록하려고 시도했을 때 아이디 같은 사용자 있으면 SQLException이 발생, 이 경우 DAO 메소드가 SQLException을 그대로 밖으로 던져버리면, 알 수 없는데 이때 정보를 해석해서 DuplicateUserIdException 같은 예외로 바꿔서 던지는 것이다. 

 

  • 이때 중첩 예외 nested exception 으로 만드는 것이 좋다. 
    • 그렇다면 getCause() 메소드 통해서 처음 발생한 예외가 무엇인지 확인할 수 있다. 

  • 두 번째 목적은 예외를 처리하기 쉽고 단순하게 만들기 위해 포장하는 것이다. 
    • 대표적으로 EJBException을 들 수 있다. 
      • EJB 컴포넌트 코드에서 발생하는 대부분의 체크 예외는 비즈니스 로직으로 볼 때 의미 있는 예외이거나 복구 가능한 예외가 아니다. 이런 경우 런타임 예외인 EJBException으로 포장해서 던지는 편이 낫다. 

 

 

  • 사용자 아이디가 중복됐을 때 사용하는 DuplicateUserIdException을 만든다. 
  • 필요하면 언제든  잡아서 처리할 수 있도록 별도의 예외로 정의하기는 하지만, 필요없다면 신경 쓰지 않아도 되도록 RuntimeException을 상속한 런타임 예외로 만든다. 

 

4.2 예외 전환 

  • JDBC는 자바 표준 JDK에서도 가장 많이 사용되는 기능 중의 하나이다. 
  • JDBC는 자바를 이용해 DB에 접근하는 방법을 추상화된 API 형태로 정의해놓고, 각 DB 업체가 JDBC 표준을 따라 만들어진 드라이버를 제공하게 해준다. 

 

4.2.1 JDBC의 한계 

  • DB를 자유롭게 바꾸어 사용할 수 있는 DB프로그램을 작성하는 데는 두 가지 한계가 있다. 
    • 첫 번째 문제는 JDBC 코드에서 사용하는 비표준 SQL 때문이다. 
      • 해결하고자 한다면 1) 호환 가능한 표준 SQL만 사용하거나 2) DB 별로 별도의 DAO만들어 SQL을 외부에 
    • 두 번째 문제는 SQLException 이다. 
      • DB마다 SQL만 다른 것이 아니라 에러의 종류와 원인도 제각각이라 DB에 독립적인 유연한 코드를 작성하는 건 불가능에 가깝다. 

 

4.2.2 DB 에러 코드 매핑을 통한 전환 

  • DB별 에러 코드를 참고해서 발생한 예외의 원인이 무엇인지 해석해 주는 기능을 만들면 된다. 

  • JDK 1.6에 포함된 JDBC 4.0 부터는 기존에 JDBC의 단일 예외 클래스였던 SQLEXCEPTION을 스프링의 DataAccessException과 비슷한 방식으로 세분화해서 정의하고 있다. 
    • SQL 문법 오류인 경우는 SQLSyntaxErrorException, 제약조건 위반인 경우는 SQLIntegrityConstraintViolationException과 같은 식으로 세분화된 예외를 사용하도록 만들었다. 

 

4.2.3 DAO 인터페이스와 DataAccessException 계층구조 

DAO를 굳이 따로 만들어서 사용하는 이유는 무엇일까?

  • 가장 중요한 데이터 엑세스 로직을 담은 코드를 성격이 다른 코드에서 분리해놓기 위해서다.
    • DAO를 사용하는 쪽에서는 DAO가 내부에서 어떤 데이터 액세스 기술을 사용하는지 신경 쓰지 않아도 된다. 

 

그런데 메소드 선언에 나타나는 예외 정보가 문제가 될 수 있다. 

public void add(User user) throws SQLException 

// 이상적인 클래스 
public interface UserDao {
	public void add(User user);
    ....
}

 

  • 가장 단순한 해결 방법은 throws Exception으로 선언하는 것이지만, 무책임한 선언이다. 
  • 다행인 것은 JDBC보다 늦게 등장한 JDO, Hibernate, JPA 등의 기술은 SQLException을 사용하지 않고 런타임 예외를 사용하기 때문에 throws Exception을 선언해주지 않아도 된다. 
  • 다만 애플리케이션에서 별도로 처리가 필요한 부분에 대해서는 단지 인터페이스를 추강화하고, 일부 기술에서 발생하는 체크 예외를 런타임 예외로 전환하는 것만으로는 불충분하다. 
    • JDBC로 만든 DAO에는 SQLException이, JPA서는 PersistenceException이, 하이버네이트에서는 HibernateException이 던져진다. 

 

데이터 엑세스 예외 추상화와 DataAccessException 계층구조 

  • 스프링은 자바의 다양한 데이터 엑세스 기술을 사용할 때 발생하는 예외들을 추상화해서 DataAccessException 계층구조 안에 정리해두었다. 
  • DataAccessException은 자바의 주요 데이터 엑세스 기술에서 발생할 수 있는 대부분의 예외를 추상화하고 있다. 
    • 데이터 액세스 기술에 상관없이 공통적인 예외도 있지만
    • 일부 기술에서만 발생하는 예외도 있다. 

 

4.2.2 기술에 독립적인 UserDao 만들기 

 

 

DataAccessException 활용 시 주의사항 

  • DuplicateKeyException은 아직까지는 JDBC를 이용하는 경우에만 발생한다. 
  • 데이터 액세스 기술을 하이버네이트나 JPA를 사용했을 때도 동일한 예외가 발생할 것으로 기대하지만 실제로 다른 예외가 던져진다. 
    • 그 이유는 SQLException에 담긴 DB의 에러 코드를 바로 해석하는 JDBC의 경우와 달리
    • JPA나 하이버네이트, JDO 등에는 각 기술이 재정의한 예외를 가져와 스프링이 최종적으로 DataAccessExcception으로 변환하기 때문이다. 

 

  • SQLException을 직접 해석해 DataAccessException으로 변환하는 코드는 다음과 같다. 

 

 

 

4.3 정리 

  • 예외를 잡아서 아무런 조취를 취하지 않거나 의미 없는 throws 선언을 남발하는 것은 위험하다.
  • 예외를 복구하거나 예외처리 오브젝트로 의도적으로 전달하거나 적절한 예외로 전환해야 한다. 
  • 좀 더 의미 있는 예외로 변경하거나, 불필요한 catch/throws를 피하기 위해 런타임 예외로 포장하는 두 가지 방법의 예외 전환이 있따. 
  • 복구할 수 없는 예외는 가능한 한 빨리 런타임 예외로 전환하는 것이 바람직하다. 
  • 애플리케이션의 로직을 담기 위한 예외는 체크 예외로 만든다.
  • JDBC의 SQLException은 대부분 복구할 수 없는 예외이므로 런타임 예외로 포장해야 한다. 
  • SQLException의 에러 코드는 DB에 종속되기 때문에 DB에 독립적인 예외로 전환될 필요가 있다. 
  • 스프링은 DataAccessException을 통해 DB에 독립적으로 적용 가능한 추상화된 런타임 예외 계층을 제공한다. 
  • DAO를 데이터 액세스 기술에서 독립시키려면 인터페이스 도입과 런타임 예외 전환, 기술에 독립적인 추상화된 예외로 전환이 필요하다. 

 



토비 스프링 소스코드 ▶ 
https://github.com/AcornPublishing/toby-spring3-1/tree/main

토비 스프링 ▶ https://product.kyobobook.co.kr/detail/S000000935358

반응형