HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🚀
Random Bit Flip
/
🔬
[2기 - 조셉] 5주차 RBF
/
⚠️
자바 예외 이해 + DB 예외처리
⚠️

자바 예외 이해 + DB 예외처리

들어가기 앞서: 도움 받은 강의와 책들

  • 시간이 있으시다면 아래 강의, 책을 직접 보시는 것이 도움 될 것입니다만, 시간적 여유가 없을 분들을 위해 제가 봤을때 도움이 되는 내용들을 정리해서 공유합니다.
  • 특히 아래 내용들을 기반으로 정리하였습니다:
    • 인프런의 스프링DB 1편 강의, 김영한
      • 가장 도움을 많이 받았습니다. JDBC 기본 원리에 대한 정리도 잘 되있으니 관련해서 보충이 필요하다면 추천드립니다.
    • 이펙티브 자바 10장
      • 단 CheckedException(검사예외)와 RuntimeException/UncheckedException(비검사예외) 에 대한 부분은 요즘 추세와 맞지 않을 수 있다고 느끼니, 유의하시길(해리 강사님이나, 위의 인프런 강의에서나 런타임예외 사용 추세를 언급합니다)
      •  

간단 요약

  • RuntimeException을 기본으로 사용
  • CheckedException은 무조건 예외 처리를 해야 하는 중요한 상황 && 그 예외를 애플리케이션 코드 차원에서 해결할 수 있을 때 사용
  • CheckedException은 예외 처리를 강제하기에, 예외 번역을 통해 RuntimeException으로 변환해 준다.
  • 예외 번역 시에는 반드시 new로 생성한 예외에 기존 예외를 담아 준다: Stack Trace 찍기 위함이다.
  • DB 접근 기술의 경우 스프링이 에러코드를 읽어 적절한 예외로 변환해주는 예외 번역 기능을 제공한다.
    • 예외 번역을 통해, 적절한 세부 예외로 변환하고,
    • 처리할 수 있는 예외의 경우 처리해준다.
 

자바의 예외

예외 개념

  • 예외는 catch하거나, throws해서 해당 메서드를 호출한 메서드로 전달해야 한다.
  • 예외의 종류, 계층은 다음과 같다:

예외 계층도

classDiagram Object <|-- Throwable Throwable <|-- Exception Throwable <|-- Error Exception <|-- SQLException Exception <|-- IOException Exception <|-- RuntimeException Error <|-- OutOfMemoryError RuntimeException <|-- NullPointerException RuntimeException <|-- IllegalArgumentException
  • 계층도에 대한 설명
    • Throwable
      • 최상위 예외, 예외도 객체이기 때문에 Object의 자손
    • Error
      • 애플리케이션 차원에서는 복구 불가능한 시스템 예외
      • 개발자가 해결할 영역이 아님. 건들지 말자
      • Error는 catch할 것이 못 되기에 그것의 상위 예외인 Throwable도 사용하지 말자!
        • Throwable을 잡으면 그 하위에 있는 Error도 자연히 잡히기 때문에
    • Exception
      • 위와 같은 이유로 인해, 실질적으로 개발자가 애플리케이션 로직에서 사용하는 최상위 예외
      •  

CheckedException(검사 예외)와 UncheckedException(비검사 예외)의 구별!

  • CheckedException
    • Exception 중 RuntimeException을 제외한 부분
    • 컴파일러가 해당 예외를 처리(catch 하거나, 메서드 시그니처에 throws 선언)하였는지 검사(Check)하는 종류의 예외임 → 예외 처리가 강제된다!
    • CheckedException을 새로 만들고 싶다면 extends Exception!
  • UnCheckedException
    • RuntimeException과 그 하위 예외 + Error (위에서 언급했듯이, Error는 사용하는 게 아니기에) RuntimeException으로 퉁쳐서 말할 때가 많음)
    • 컴파일러는 해당 종류의 예외를 처리했는지 검사하지 않음!(Unchecked) → 처리 강제 X
    • UnCheckedException을 새로 만들고 싶다면 extends RuntimeException!
  • 비교
    • 예외 처리가 강제되느냐의 문제입니다.
    • 예외 처리가 강제되는 CheckedException의 경우 런타임에 생길수도 있는 예외를 컴파일 시점에 미리 잡을 수 있다는 장점이 있지만,
    • 예외처리가 강제된다는 점에서, 처리할 수 없는 경우에는 문제가 됩니다.
    • 예외 처리가 강제되지 않는 UncheckedException은 반대입니다.
 
 

어떨 때 사용할까?

  • 기본적으로 RuntimeException 쓰자!
  • CheckedException의 경우, 이 예외를 처리하지 못 할 경우 정말 치명적인 문제가 생기며 && 그 예외를 처리해줄 수 있을 때만 사용하자.
 

왜 CheckedException을 가급적 안 쓰는 편이 나을까?

  • 실제로 발생하는 CheckedException은 처리할 수 없는 것들이 많다.
    • 네트워크 연결 문제나, DB 연결 상태가 깨져서 발생하는 예외를 개발자가 애플리케이션 코드에서 잡아줄 수는 없다!
  • 예외 처리 강제로 인해 해당 예외에 대한 의존성이 생긴다.
    • 예시) Repository
      • Repository 인터페이스를 규정, 그 구현체로 JdbcRepository 만들었을 경우 JdbcRepository는 SQLException을 던지게 된다.
      • 구현체 JdbcRepository의 메서드 시그니처에 throws문이 붙기 때문에, 해당 메서드를 규정하고 있는 인터페이스의 메서드 시그니처에도 throws 문이 붙어야 하는데, 이 경우 JDBC와 관련된 SQLException에 대한 의존성을 Repository 인터페이스 자체가 갖게 된다.
      • 만약 JPA로 변경한다면, 위의 SQLException에 대한 코드는 불필요한데도 인터페이스가 해당 예외를 달고 있다는 문제점이 생긴다.
  • 종합해보면
    • 어차피 처리도 못할 예외를
    • throws 하게 되면 인터페이스도 오염시키고, 해당 계층의 윗 계층까지 오염시키게 된다.
      • 일단적인 웹 어플리케이션의 경우라면, Repository를 호출한 Service도, Service를 사용하는 Controller도 ,...
    • 그냥 catch해서 UncheckedException, RuntimeException으로 변환해주는 것이 낫다! → 예외 번역
    •  

애플리케이션 코드에서 처리할 수 없는 예외는 어떻게 다룰까?

  • 예외가 catch되지 않고 끝까지 간다면
    • 자바 콘솔 애플리케이션의 경우
      • 예외가 main() 쓰레드까지 올라가고, 예외 로그와 함께 시스템 종료
    • 웹 어플리케이션의 경우
      • WAS가 해당 예외를 받아서 처리: 보통은 오류 페이지를 보여줌
  • 따라서 웹 어플리케이션의 경우
    • 서블릿 오류 페이지
    • 스프링을 사용할 경우 ControllerAdvice 사용하여 예외를 공통 처리할 수 있음
      • 저 같은 경우 스프링 MVC 강의에서 학습했었는데, 3주차 과제, 웹 애플리케이션 개발 중이시라면 해당 방식을 학습, 적용하셔서 예외 처리하시면 깔끔하리라 생각합니다.
    • 위 경우 보통 500에러가 될 것임
  • 어차피 개발자가 애플리케이션 코드에서 처리하지 못 할 예외라면, 적절한 오류 로깅 + 빠르게 장애 상황을 인지할 수 있는 알림(슬랙, 지라 등등)이 중요하다!
    • 예외와는 무관하게, 위와 관련하여 읽어볼만한 이야기는:
      • “함께 자라기” 라는 책에서는(88쪽, 실수는 예방하는 것이 아니라 관리하는 것이다) 실수를 예방하는 것 보다도 관리하는 것의 중요성에 대해서 이야기합니다. 문제가 안 생기게 노력하는 것도 중요하지만, 어차피 실수(여기서는 예외와 같은 문제상황이 되겠죠)는 어차피 터질 수 밖에 없기 때문에 상황이 발생했을 때 관리할 수 있는 역량을 강조하고 있습니다. 실제로 조직이나, 학습 양 쪽 모두 실수 예방보다 관리의 관점에서 접근할 때 성과가 더 잘 나온다는 얘기도 있네요.
      • 실제 기업에서 장애대응을 어떻게 운영하는가에 대한 글입니다: 우아한 장애대응
      • 저희가 스프링부트 1주차에서 학습했던 Profile을 이용하여 상황에 따라 콘솔, 슬랙 알람 등으로 나누어 로깅하는 전략에 대한 글입니다: 링크
 

예외 번역

  • 어차피 개발자가 애플리케이션 코드에서 처리할 수 없는 CheckedException은 거추장스럽기만 합니다. RuntimeException을 상속받는 새 예외를 정의하거나, 적절한 RuntimeException(의 하위 예외)로 변환해줍시다.
  • 예외를 catch하여 다른 예외를 던져 줍니다.
    • void save(UUID voucherId) { try { //Jdbc 사용 코드 } catch (SQLException e) { throw new SomeException(e); } }
    • 위의 코드의 경우 CheckedException인 SQLException을 잡아서, RuntimeException인 SomeException으로 전환, 다시 예외를 던지고 있습니다.
    • 이제 예외처리가 강제되지 않습니다.
    • 중요!!! 새 예외를 생성할 때는 인자로 기존 예외를 담아줘야 합니다! 아니면 예외의 Stack trace가 날라가 버려서 원인 파악에 상당한 문제가 생깁니다!
  • UnCheckedException은 예외처리가 강제되지 않기에 실수로 놓칠 수도 있습니다. 때문에
    • JavaDocs에 표기해줍니다.
      • /** * @throws ???Exception if 예외 터지는 상황 */
    • 예외 표기가 필요하다 싶을 때는 메서드 시그니처에 명시해줍니다. (명시가 강제되지 않는 것일 뿐, 표기는 가능)
    •  

Stack Trace

  • 위에서 언급했듯이, 예외 번역을 통해 새 예외를 생성할 때는 인자로 반드시 기존 예외를 담아줍니다.
  • 예외 발생시 로그에는 stack trace를 다음과 같이 담습니다.
    • log.error("예외 발생, 메시지: {}, ", e.getMessage(), e);
    • {} 없이 추가적으로 e를 전달하면 Stack Trace 출력됩니다. (위의 경우 {}하나, 전달받는 인자 하나 + e라서 출력)
  • 예외 발생시 e.stackTrace()로 Stack Trace 찍지 말고 로그로 남깁시다.
 

처리 가능한 예외

  • 데이터 접근 계층에서 발생하는 오류라고 해서 모두 처리 불가능한 것은 아님
  • 예를 들어, 특정 객체를 저장하려고 할 때
    • 객체의 ID값을 애플리케이션에서 할당한다면 (즉, DB 측에서 ID를 할당하는 상황이 아니라면)
    • PK값을 실수로 이전에 저장된 것과 똑같은 값으로 저장하려고 할 때 예외가 발생할 것임
    • 이 경우, PK인 ID값을 변경해주고, 저장을 다시 시도한다면, 해당 예외로 인해 발생한 문제는 해결될 것임
  • 어떻게 DB 계층에서 발생한 예외의 종류를 알 수 있을까? → 에러 코드
    • e.getErrorCode()를 통해 에러 코드를 얻을 수 있음
    • 해당 에러코드일 경우 상황에 맞는 예외(Ex. DuplicatePrimaryKeyException)를 던지고, 위 계층에서 해결을 시도하면 됨
      • 하지만 이 에러코드의 경우 각 DB에 따라 달라짐! → 스프링 예외 번역기를 통해 해결 가능
        • H2DB의 DuplicateKeyCodes 에러 코드는 23001, 23505
        • DB2의 DuplicateKeyCodes 에러 코드는 803
 

스프링의 예외 처리 기술

  • 스프링 데이터 접근 예외 계층
    • classDiagram RuntimeException <|-- DataAccessException DataAccessException <|-- NonTransientDataAcessException DataAccessException <|-- TransientDataAccessException NonTransientDataAcessException <|-- BadSqlGrammarException NonTransientDataAcessException <|-- DataIntegrityViolationException TransientDataAccessException <|-- QueryTimeoutException TransientDataAccessException <|-- OptimisticLockingFailureException TransientDataAccessException <|-- PessimisticLookingFailureException DataIntegrityViolationException <|-- DuplicateKeyException
    • 스프링은 데이터 접근 예외를 새로 규정하지 않아도 되게 위와 같이 사전 정의해 두었음
    • 분류
      • DataAccessException: 스프링 제공 예외의 최상위 예외
      • TransientException: 동일 SQL을 다시 시도했을 때 성공할 가능성이 있는 일시적 예외
        • 예시: 쿼리 타임아웃, 락
      • NonTransientException: 일시적이지 않음, 같은 SQL을 아무리 반복해서 시도해도 실패
        • 예시: SQL 문법 오류, DB 제약조건 위배
    • 스프링 예외 번역기
      • 스프링은 DB에러코드를 읽어들여, 상황에 맞는 예외로 알아서 변환해 줌
      • } catch (SQLException e) { var exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource); DataAccessException resultException = exceptionTranslator.translate("상황을 설명할 수 있는 글", sql, e); log.info("resultException = {}", resultException); //SQL 문법이 잘못되었을 경우에는 BadSqlGrammarException으로 변환된 것을 알 수 있음
      • translate() 메서드
        • 파라미터1: 읽을 수 있는 설명
        • 파라미터2: 실행한 SQL
        • 파라미터3: 발생한 Exception
      • org.springframework.jdbc.support.sql-error-codes.xml
        • 각 DB별 예외 코드, 거기에 해당하는 스프링 예외가 정리되어 있음
        • 대략 아래와 같은 식:
          • <bean id="DB2" name="Db2" class="org.springframework.jdbc.support.SQLErrorCodes"> <property name="databaseProductName"> <value>DB2*</value> </property> <property name="badSqlGrammarCodes"> <value>-007,-029,-097,-104,-109,-115,-128,-199,-204,-206,-301,-408,-441,-491</value> </property> <property name="duplicateKeyCodes"> <value>-803</value> ... <bean id="H2" class="org.springframework.jdbc.support.SQLErrorCodes"> <property name="badSqlGrammarCodes"> <value>42000,42001,42101,42102,42111,42112,42121,42122,42132</value> </property> <property name="duplicateKeyCodes"> <value>23001,23505</value> ...
        • 스프링이 알아서 DB 별 코드를 읽어, 예외를 번역해주기에 DB를 변경한다고 해도 예외 처리 코드를 바꿀 필요 없음!
        •