[CS] 자바의 정석 독서 #14 - 예외처리

2025. 11. 11. 07:36·언어공부/Java | Kotlin

책 읽은지 거의 한 달정도 됐는데.. CS지식을 쌓아봅시다


예외

컴공개 때 파이썬에 에러가 Syntax 에러와 Runtime 에러가 있다고했는데, 자바는 컴파일러를 사용하니 컴파일에러와 런타임에러로 나뉜다.

 

런타임에러 중에서 예외와 에러는 구분되는 개념인데, 오류가 매우 심각하여 발생 후 복구할 수 없는 오류는 에러에 해당하고, 예외는 프로그램 코드로 적당히 수습할 수 있는 오류다

Exception in thread "main" java.lang.StackOverflowError
Exception in thread "main" java.lang.ClassCastException

스택오버플로우가 일어나면 프로그램이 강제종료될 수 밖에 없기 때문에 에러를 터뜨리고, 밑의 ClassCastException은 캐스팅만 수정하면 잘 문제없이 돌아가기 때문에 에러(Error)와 예외(Exception)으로 나뉜다.


예외클래스

자바에서 실행 중 발생할 수 있는 오류는 클래스로 정의되었고, 당연히 모든 클래스들의 상위클래스인 Object 클래스부터 상속 중이다.

 

Object - Throwable - Exception / Error 의 상속트리를 가지고 있다.

모든 예외의 최고조상은 Excpetion이 된다. Exception에는 IOException, ClassNotFoundException 등 모든 예외를 포함하며, 런타임에러 클래스들을 모아둔 RuntimeException도 존재한다. RuntimeException의 하위클래스들이 런타임에러가 일어났을 때 호출된다. 대표적인데 NPE


try-catch

런타임에러는 어쩔 수 없다만 예외는 처리를 해야한다. 일어날 수 있는 예외를 미리 알아내고 이 예외가 발생했을 때 대응하는 코드를 짜두어야한다.

 

try {
   	//코드
} catch (Exception e) {
  	//코드
}

이런식으로

예외도 클래스기 때문에 참조변수를 선언할 수 있으며, catch문 내에서 중복선언하면 컴파일에러가 터진다. 서로 다른 catch 문 내에서는 상관없음

 

try 문 수행하다가 예외가 생기면 catch문으로 이동해 catch 블럭의 코드를 실행하고 catch 블럭을 빠져나오는 형태다. 예외가 발생되면 예외클래스의 인스턴스가 만들어지고, catch 블럭의 괄호에 해당하는 예외클래스를 instanceof 연산을 통해 대조한다. 그리고 해당되는 예외가 있으면 catch문이 작동되고, 없으면 처리되지않는거.

 

그래서 위 코드처럼 Exception을 괄호에 넣으면 모든 예외가 처리된다. 근데 예외처리가 좀 애매해져서 (이게 무슨 예외인지 알 수 없으니) 지양하라는 사람도 있고.. 아마 이펙티브 자바에 보면 분명히 "catch문에 들어갈 예외를 Exception으로 선언하지 마라"라는 내용이 있을 것 같다.

 

try 블럭에서 예외가 발생하면 예외가 발생한 코드 이하로는 실행되지 않는다.


멀티 catch 블럭

        try {
            System.out.println(0/0);
        } catch (ClassCastException | ArithmeticException e) {
            System.out.println(e.getMessage());
        }

이런식으로 '|' 기호를 통해 catch 문 내에서 여러 에러를 선언할 수 있다. JDK7부터 가능하며, 다만 catch 블럭 내에서 어떤 오류가 불러와졌는지는 instanceof 연산자를 사용해 구분하지 않는 이상 모른다. 그래서 특정 예외클래스에만 선언된 메서드를 불러올 수 없다. catch 문 내에서는 어떤 예외클래스를 불러왔는지 모르기 때문이다.

 

| 기호는 논리연산자가 아니다. 기호는 같다.

 

        try {
            System.out.println(0/0);
        } catch (Exception | ClassCastException | ArithmeticException e) {
            System.out.println(e.getMessage());
        }
java: Alternatives in a multi-catch statement cannot be related by subclassing
  Alternative java.lang.ClassCastException is a subclass of alternative java.lang.Exception

멀티 catch 문에서 상위 예외클래스와 하위 예외클래스는 동시에 선언할 수 없다. 상위클래스 하나만 선언해야한다.


finally

예외발생여부에 관계없이 반드시 실행해야하는 코드가 있을 때 finally를 통하여 실행시킬 수 있다. 만약 try문에서 return 문이 있더라도 try문 return 직전까지 실행 → finally 실행 → try문 return 실행 순으로 실행된다.


예외메서드

예외클래스에는 몇 가지 메서드들을 가지고 있는데, 예외클래스 자체에는 없고 Throwable 클래스에서 상속받은 것이다.

 

메서드는 printStackTrace()와 getMessage() 등이 있는데, 

printStackTrace()
getMessage()

어차피 둘 다 출력은 똑같은데 printStackTrace()는 예외가 발생한 메서드의 정보와 에러메시지를 표출하고 getMessage()는 예외클래스에 저장된 에러메시지를 확인할 수 있다. 만약 지정되지 않을 경우 null로 출력된다.


예외 터뜨리기

Exception e = new Exception("예외!");
throw e;

이런 식으로 예외를 던질 수도 있고, 예외도 클래스이기 때문에 Exception을 상속한 클래스를 만들어 커스텀에러를 만들고 던질 수도 있다.

 

https://dev-dx2d2y-log.tistory.com/51

 

[GDG] 홍대 맛집 아카이빙 프로젝트 #20 - mySQL과 각종 코드 보강하기

mySQL 사용하기오늘은 H2DB를 벗어나서 MySQL로 DB를 연동시켜볼까 한다. 기본적으로는https://chaeyami.tistory.com/244 [SpringBoot] JPA + MySQL 연동MySQL과 Workbench 설치MySQL 설치하기 [MySQL] MySQL, MySQL Workbench 설치

dev-dx2d2y-log.tistory.com

여기서 한 번 해봤는데.. 이 때는 전역Exception에 집중할 때라 커스텀예외처리는 큰 비중이 없네

 

생성자의 인자로 넘겨주는 메시지는 getMessage()를 실행시킬 때 나오는 메시지가 된다.


예외선언하기

try-catch문을 선언하는 것 이외에도 예외를 메서드 선언부에 같이 선언할 수 있다. 그래서 예외처리를 할 때 메서드 선언부에 예외를 선언하거나 try-catch문을 통해 예외처리를 하거나 둘 중 하나를 선택할 수 있다.

 

private static void sendMail(String password, MailContentDTO contents) throws MessagingException { }

비동기처리할 때 썼던 코드의 예외 던지는 부분

만약 내가 예외처리를 전부 try-catch문을 통해서 처리하고 있다면 저 예외선언은 필요없다.

또 예외는 , 를 기준으로 여러 개 선언할 수 있다.

 

이것은 '이 메서드를 호출하는 쪽에게, 이 메서드에서 이러한 예외가 발생할 수 있다'라고 선언하는 것으로, 결국에는 호출하는 쪽에서 예외처리를 해주어야한다.

 

https://dev-dx2d2y-log.tistory.com/108

 

[비동기-2편] 비동기처리로 클라이언트 풀어주기

더보기비동기 시리즈0편:스프링 초기세팅하기 - https://dev-dx2d2y-log.tistory.com/102 1편:javax.mail로 메일보내기 - https://dev-dx2d2y-log.tistory.com/1031.5편:내부클래스로 코드 간략화 - https://dev-dx2d2y-log.tistory.co

dev-dx2d2y-log.tistory.com

비동기처리에서 작성 코드들에 이런부분에서의 리팩토링이 필요했는데, 메일 보내는 부분에서 MessagingException 예외가 발생할 수 있다. 메시지 전송 부분에 try-catch 문으로 예외처리를 해줬는데, 정작 그러면 이 메일을 보내는 메서드의 선언부에서 예외선언이 필요없고, 그 메서드를 호출하는 부분에서도 try-catch 문이나 예외선언이 필요없다.

 

그러니까

예외가 발생할 수 있는 부분이 생겼을 때

 

1. try-catch 문으로 예외가 발생부분을 감싸서 해결한다.

2. 해당부분을 그대로 두고 메서드에 예외를 선언한다.

2-1. 메서드의 호출부에서 다시 1 또는 2를 선택한다.

 

만일 예외가 계속 넘겨져서 main 메서드에 도달했으나 main메서드에도 예외처리가 되어있지 않으면 main 메서드가 종료되고 프로그램도 종료된다.

 

어제 했던 비동기 메일전송 예제를 예로 들어보면 메일 전송할 때 Transport.send() 메서드를 사용하는데,

    public static void send(Message msg) throws MessagingException {
		msg.saveChanges(); // do this first
		send0(msg, msg.getAllRecipients(), null, null);
    }
private static void send0(Message msg, Address[] addresses,
		String user, String password) throws MessagingException {

	if (addresses == null || addresses.length == 0)
	    throw new SendFailedException("No recipient addresses");

이렇게 여러 문제(이메일주소가 null이거나 하는 등..) SendFailException이 발생해서 이것을 처리해야한다. try-catch 문을 사용하는 것이 아니라 메서드 선언부에 예외선언만 반복되기에 예외처리를 내가 해야하는거

 

public class SendFailedException extends MessagingException

그리고 SendFailedException은 MessagingException의 하위클래스이기에 MessagingException을 선언해도 무방하다. 그래서 예외선언부에 Exception 예외를 선언하면 모든 예외가 걸러지는 것이다. 근데 그러면 예외가 발생해도 무슨 예외인지 모르니까 예외처리에 난항을 겪겠지

 

이처럼 두 가지의 방법이 있는데,

예외가 발생한 메서드 자체적으로 예외처리를 하던가, 예외선언을 통해 호출한 메서드에서 예외처리를하게 하는 방법이 있다.

 

이럴 때는 자체적으로 처리해도 될 경우에는 전자, 특정 인자를 넘겨주어야하거나 하는 경우는 후자를 선택하는 것이 좋다. 예외메시지에 무엇이 잘못됐는지 전달해야한다거나..


TWR

I/O에 대해서 배울 때 반드시 close를 해야하는 객체들이 존재한다. 그래서 만약 I/O 관련해서 객체를 호출하여 작업을 수행하고 finally 블록에 close() 메서드를 넣어 객체가 반드시 닫히도록 설정할 수 있는데, 문제는 close() 메서드 역시 예외를 발생시킬 수 있다.

 

그러면 finally 블록에 try-catch문을 추가해서 또 예외처리를 해주어야하는데

public static void mian() {
        DataInputStream dis = null;
        FileInputStream fis = null;

        try {
            fis = new FileInputStream("files");
            dis = new DataInputStream(fis);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                dis.close();
            } catch(Exception e) {
                e.printStackTrace();
            }
        }
    }

별로 가독성이 좋진 못하다. 그리고 문제는 try블럭과 finally블럭 모두에게서 에러가 발생하면 try 블록의 에러는 무시된다. 그래서 근본적으로 왜 에러가 발생했는지 알지 못한다.

 

그래서 JDK7부터 도입된 TWR (Try-With-Resources)를 사용할 수 있다. 사용법은 try 블록에 괄호를 추가하여 객체를 생성하면, try 블록을 벗어난 순간 바로 close()가 호출된다. 그리고 catch 문이나 finally 문이 실행된다.

 

    public static void mian() {
        try (FileInputStream fis = new FileInputStream("files");
            DataInputStream dis = new DataInputStream(fis)) {

            System.out.println("작업 수행하기");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

그래서 이렇게 코드도 줄일 수 있다는거

다만 TWR의 괄호에 들어갈 객체들은 AutoCloseable 인터페이스의 구현체여야한다. DatatInputStream은 DataInputStream -  FileInputSteam - InputStream의 상속구조를 가지고, InputStream은 다시 Closeable의 구현체이며, Closeable은 AutoCloseable을 상속했다.


만약 자동 close() 부분에서 에러가 발생하면 어떻게 될까?

 

package com.ums.h2sm.Mail;

import org.thymeleaf.processor.element.AbstractAttributeTagProcessor;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class ThreadTest {
    public static void mian() {
        try (AutoCloseResource ar = new AutoCloseResource()) {
            ar.work(false);
        } catch (IOException e) {
            e.printStackTrace();
        }

        try (AutoCloseResource ar = new AutoCloseResource()) {
            ar.work(true);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

class AutoCloseResource implements AutoCloseable {
    public void work(boolean exception) throws IOException {
        System.out.println("WORK 메서드 호출됨");

        if (exception) {
            System.out.println("work 메서드에서 오류가 발생했습니다.");
            throw new IOException("work에서 오류발생!");
        }
    }

    public void close() throws IOException {
        System.out.println("close 호출됨");
        throw new IOException("Close 과정에서 오류가 발생했습니다.");
    }
}

이런코드가 있다고 치자. 위 코드를 실행하면 work에 전해진 값이 false일 경우 그냥 close()가 호출되어 AutoCloseResource 객체가 닫히고, true일 경우에는 work 메서드에서 오류가 발생하고 close 메서드에서 한 번 더 오류가 발생한다.

 

우선 false인경우 close 과정에서만 오류가 발생했다.

 

true인 경우

workd에서 생긴 오류가 먼저 출력되고 close에서 발생한 오류는 suppressed (억제된) 이라는 머리말과 함께 출력되었다. 두 예외가 동시에 발생할 수는 없기 때문에, 실제 예외는 work에서 발생한 예외를, close에서 발생한 예외는 억제된 예외로 다룬다. 억제된 예외의 정보는 실제 발생된 예외에 저장된다.

 

public final synchronized void addSuppressed(Throwable exception)

예외클래스들의 공통조상인 Throwable 클래스에도 addSuppressed() 메서드를 통해 억제된 예외를 추가할 수 있다.


연결된 예외

예외는 다른 예외를 발생시킬 수 있다.

org.springframework.beans.factory.BeanCreationException: 
Error creating bean with name 'smsfApplication': ...에러 메시지 내용...
Caused by: org.springframework.dao.InvalidDataAccessResourceUsageException:...중략...; SQL statement:
Caused by: org.hibernate.exception.SQLGrammarException: could not prepare statement [Table "MEMBER" not found (this database is empty); SQL statement:
Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "MEMBER" not found (this database is empty); SQL statement: select m1_0.id,m1_0.email,m1_0.username from member m1_0 where m1_0.id=? [42104-232]

스프링 프로젝트할 때 가끔 예외가 줄줄이 발생할 때가 있는데,

여기서의 caused by:... 가 연결된 예외다.

 

예외 A가 예외 B를 발생시킬 때, A를 B의 원인예외(cause exception)이라고 한다.

 

저 에러메시지를 보자면 가장먼저 JdbcSQLSyntaxErrorException이 발생했고, 예외처리과정에서 다시 SQLGrammarException을 발생시키고, SQLGrammarException의 예외처리에서 다시 InvalidDataAccessResourceUsageException을 발생시키게된거고... 이렇게 올라가는 것이다.

 

저게 아마 @PostConstruct 어노테이션을 통해 프로젝트 빌드 전  스프링 JPA를 이용해 Member 객체를 save 할 때 일어난 것 같은데,

 

스프링JPA에서의 save 메서드가 JdbcSQLSyntaxErrorException을 일으키면 save 메서드를 호출한 쪽에서 이를 걸러내 다시 SQLGrammarException을 발생시키고 하는 순으로 최종적으로는 스프링에서 빌드과정에서 오류가 발생함을 인지하고 BeanCreationException을 발생시키는 순으로 일어난다.


그렇다면 왜 이렇게 여러 번 에러를 발생시킬까?

그 이유는 여러 가지 예외를 큰 부류의 하나의 예외로 묶어 처리하면 간편하기 때문이다.

 

위의 에러메시지 예시에서

SQLGrammarException은 hibernate가 발생시킨에러인데, 만약 hibernate가 아닌 다른 모듈을 사용했을 때 에러가 발생한 경우, 모듈 하나하나에 대응하는 예외처리를 해주어야한다.

 

그래서 스프링에서 다시 DataAccessException으로 모든 SQLGrammar 예외를 묶어서 처리하게된 것이고, 결과적으로 스프링에서는 그 DB나 ORM마다 다른 예외처리를 할 필요없이 DataAccessException만 감지하면 되기 때문에 여러 예외를 한 번에 묶어서 처리할 수 있게 된다.

 

물론 이 과정에서 오류들을 묶어주는 클래스가 필요하다.

JDBC의 SQLException, Hibernate의 SQLGrammarException, MyBatis의 PersistenceException과 같이 라이브러리마다 다른 오류들을 감지하고 이를 예외처리해야하는데, 여기에 SQLExceptionTranslator가 사용된다.

 

https://minforbackup.tistory.com/entry/DB-%EC%A2%85%EB%A5%98%EB%A1%9C%EB%B6%80%ED%84%B0-%EC%98%88%EC%99%B8-%EC%B6%94%EC%83%81%ED%99%94%ED%95%98%EA%B8%B0-feat-SQLExceptionTranslator-%EB%9C%AF%EC%96%B4%EB%B3%B4%EA%B8%B0

 

DB 종류로부터 예외 추상화하기 (feat. SQLExceptionTranslator 뜯어보기)

목차 1. 개요 2. DB 종류 추상화하기 3. SQLException -> DataAccessException : 예외 전환 4. JdbcTemplate의 DB 추상화 전략 : 데이터베이스 벤더의 오류 코드 활용해서 예외 포장하기 5. JdbcTemplate에서 예외 전환

minforbackup.tistory.com

이 글을 참고하면 좋을듯하다.

 

이외에도 반드시 예외처리를 해야하는 'checked 예외'를 예외처리를 하지 않아도되는 'unchecked 예외'로 바꿀 수도 있다.

checked 예외는 컴파일러가 감지하기 때문에 반드시 예외처리를 해줘야하고, unchecked 예외는 컴파일러가 감지하지 않기 때문에 굳이 예외처리를 하지 않아도되지만 런타임에러로 발생하면 서버가 뻗어버릴 수 있다.

 

 자바 초기에는 모든 예외를 checked 예외로 처리했어야했는데, 요즘에는 예외처리를해도 예외를 처리할 수 없는 경우가 생겨났다.

 

가령 위에서 나온 DB처리, 네트워크 끊김 등..

이 경우에는 개발자가 예외처리를 아무리해봤자 방법이 없기 때문에 의미없는 try-catch문만 나열되게 된다.

 

그래서 그냥 RuntimeException을 발생시키고 서버가 다운되면 그 때 고쳐서 다시 서버를 작동하는 방식으로 변하게되었다.

가령 DB에러를 감지하게된다면, 예외처리를 이것저것 붙이는 것이 아니라 RuntimeException 하나를 작동시켜 서버를 멈추게한 후 DB에러의 원인을 감지하는 식으로 작동된다.

'언어공부 > Java | Kotlin' 카테고리의 다른 글

[CS] 자바의 정석 독서 #17 - StringBuffer, StringBuilder  (0) 2025.11.19
[CS] 자바의 정석 독서 #16 - String 클래스 파헤치기  (0) 2025.11.18
[CS] 이펙티브 자바 독서 #3 - 아이템85. 직렬화를 피하라!  (0) 2025.10.23
[CS] 스레드란 무엇일까?  (0) 2025.10.15
[CS] 이펙티브자바 독서 #2 - 아이템18  (0) 2025.10.14
'언어공부/Java | Kotlin' 카테고리의 다른 글
  • [CS] 자바의 정석 독서 #17 - StringBuffer, StringBuilder
  • [CS] 자바의 정석 독서 #16 - String 클래스 파헤치기
  • [CS] 이펙티브 자바 독서 #3 - 아이템85. 직렬화를 피하라!
  • [CS] 스레드란 무엇일까?
Radiata
Radiata
개발을 합니다.
  • Radiata
    DDD
    Radiata
  • 전체
    오늘
    어제
    • 분류 전체보기 (211)
      • 신년사 (3)
        • 2025년 (2)
        • 2026년 (1)
      • CS (59)
        • JVM (12)
        • 백엔드 (20)
        • 언어구현 (1)
        • 객체지향 (1)
        • 논리회로 (5)
        • 컴퓨터구조 (9)
        • 데이터베이스 (1)
        • 컴퓨터 네트워크 (10)
      • 언어공부 (64)
        • Java | Kotlin (48)
        • JavaScript | TypeScript (9)
        • C | C++ (6)
      • 개인 프로젝트 (11)
        • [2025] Happy2SendingMails (3)
        • [2026] 골든리포트! (8)
        • [2026] 순수자바로 개발하기 (0)
        • 기타 이것저것 (0)
      • 팀 프로젝트 (29)
        • [2025][GDG]홍대 맛집 아카이빙 프로젝트 (29)
      • 알고리즘 (13)
        • 백준풀이기록 (11)
      • 놀이터 (0)
      • 에러 수정일지 (2)
      • 고찰 (24)
        • CEOS 23기 회고록 (2)
  • 블로그 메뉴

    • CS
    • 언어공부
    • 개인 프로젝트
    • 팀 프로젝트
    • 알고리즘
    • 고찰
    • 신년사
    • 컬러잇 개발블로그
  • 링크

    • 컬러잇 개발블로그
  • 공지사항

  • 인기 글

  • 태그

    144
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
Radiata
[CS] 자바의 정석 독서 #14 - 예외처리
상단으로

티스토리툴바