기본 콘텐츠로 건너뛰기

자바 Checked Exception vs Unchecked Exception: 언제 복구하고 언제 코드부터 고칠까

자바 Checked Exception vs Unchecked Exception: 언제 복구하고 언제 코드부터 고칠까

빠른 답

  • Checked Exception은 호출자가 재시도, 대체 경로, 사용자 안내 같은 대응을 선택할 수 있을 때 API 계약으로 드러내기 좋습니다.
  • Unchecked Exception은 잘못된 값, 잘못된 호출 순서, 깨진 상태처럼 코드 쪽 수정이 필요한 문제를 표현하는 경우가 많습니다.
  • 둘 다 실제로는 실행 중에 발생합니다. 차이는 예외가 아니라 컴파일러가 처리 여부를 강제하느냐에 있습니다.
  • Error는 일반 애플리케이션 로직에서 복구 대상으로 보기보다, JVM이나 프로세스 수준의 심각한 문제로 보는 편이 가깝습니다.

한눈에 비교

검사 시점
Checked Exception 은 컴파일 시점에 처리 여부를 검사하고, Unchecked Exception 은 컴파일러가 강제하지 않습니다.
타입 계층
checked는 보통 Exception 하위 타입 중 RuntimeException 이 아닌 예외이고, unchecked는 RuntimeException 과 그 하위 타입입니다.
의미
checked는 외부 조건 때문에 작업이 실패할 수 있음을 드러내고, unchecked는 값 오류나 상태 불일치처럼 코드 전제가 깨졌음을 드러내는 경우가 많습니다.
호출자 책임
checked는 try-catch 또는 throws 로 책임을 명시해야 하고, unchecked는 필요할 때만 잡으면 됩니다.
관찰 가능한 결과
checked를 처리하지 않으면 컴파일 오류가 보이고, unchecked는 컴파일은 되지만 실행 중 스택 트레이스로 드러납니다.
복구 방향
checked는 재시도, 기본값 사용, 다른 자원 선택으로 이어지기 쉽고, unchecked는 호출 코드 수정이나 상위 경계에서의 실패 응답 변환으로 이어지기 쉽습니다.

시간 흐름으로 이해하기

메서드 선언
checked 예외는 throws 로 메서드 시그니처에 드러납니다.
컴파일 검사
호출부가 그 예외를 처리하지 않으면 컴파일이 멈춥니다.
호출부 처리
호출자는 catch 로 복구하거나 다시 throws 로 위임합니다.
실행 중 발생
checked와 unchecked 모두 실제 throw 는 실행 중에 일어납니다.
전파와 복구
최종 진입점에서 로그, 종료 코드, 사용자 메시지, 실패 응답 같은 형태로 정리됩니다.

왜 둘 다 Exception인데 처리 방식은 다를까

자바는 예외를 단순한 오류 메시지가 아니라 메서드 계약의 일부로 다루려는 성향이 있습니다. 그래서 어떤 실패는 호출자에게 "이 가능성을 알고 결정해 달라"는 식으로 드러내고, 어떤 실패는 "호출 전제가 이미 깨져 있다"는 신호로 남겨 둡니다.

이 차이를 이해할 때 도움이 되는 축은 값, 상태, 외부 조건입니다.

IllegalArgumentException은 값이 잘못됐다는 뜻에 가깝습니다. 메서드가 기대한 범위나 형식에 맞지 않는 입력이 들어왔을 때 주로 보입니다. IllegalStateException은 값 자체보다 현재 객체나 시스템 상태가 그 호출을 받을 준비가 안 됐다는 뜻에 가깝습니다. 반면 IOException은 값도 상태도 맞았지만 파일, 네트워크, 디스크처럼 바깥 조건 때문에 작업이 실패했다는 뜻으로 읽는 편이 자연스럽습니다.

여기서 자주 생기는 오해가 하나 있습니다. Checked Exception을 흔히 "컴파일 타임 예외"라고 부르지만, 예외 자체가 컴파일 중에 발생하는 것은 아닙니다. 더 정확한 표현은 "컴파일 시점에 처리 여부를 검사받는 예외"입니다. 실제 throw는 checked와 unchecked 모두 런타임에 일어납니다.

Error와 Exception의 차이

ErrorException의 하위 타입이 아닙니다. 둘 다 Throwable 아래에 있지만 가지가 다릅니다. 이 차이는 단순한 상속 구조보다 복구 가능성의 관점에서 많이 설명됩니다.

OutOfMemoryError, StackOverflowError처럼 Error로 표현되는 문제는 프로세스 자체가 불안정해질 수 있는 상황과 자주 연결됩니다. 이런 경우 애플리케이션 코드가 정상 흐름 안에서 복구 로직을 세우기 어렵습니다. 그래서 일반 업무 코드에서 catch (Throwable)로 모두 잡아 처리하는 방식은 대개 피하는 편이 낫습니다. 예외와 오류를 한 바구니에 담으면, 값 오류와 JVM 고갈 상태가 같은 방식으로 보이기 때문입니다.

반대로 Exception은 애플리케이션이 의미를 붙여 다룰 여지가 있는 실패를 담는 경우가 많습니다. checked와 unchecked는 그 안에서 다시 "호출자가 이 실패를 미리 알고 대응해야 하는가"라는 언어 차원의 구분으로 나뉩니다.

실전 코드와 출력 예시로 보기

먼저 checked 예외는 호출부가 아무 처리도 하지 않으면 실행 전에 막힙니다. 아래 코드는 IOException 가능성을 메서드 시그니처에 드러냈지만, main에서 아무 처리도 하지 않았습니다.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class CheckedBroken {
    static String readConfig(Path path) throws IOException {
        return Files.readString(path);
    }

    public static void main(String[] args) {
        String text = readConfig(Path.of("app.txt"));
        System.out.println(text);
    }
}

이 경우 관찰되는 것은 런타임 오류가 아니라 컴파일 오류입니다.

$ javac CheckedBroken.java
CheckedBroken.java:10: error: unreported exception IOException; must be caught or declared to be thrown
        String text = readConfig(Path.of("app.txt"));
                                ^
1 error

왜 이런 결과가 나오는지는 메서드 선언만 보면 바로 드러납니다. readConfigthrows IOException을 갖고 있으므로, 호출자는 catch로 잡거나 자기 메서드도 throws로 올려야 합니다. 자바 컴파일러가 여기서 계약 누락을 검사합니다.

같은 메서드라도 호출부가 실패 시 행동을 정하면 이야기가 달라집니다. 파일을 읽지 못했을 때 기본 설정으로 계속 진행하겠다는 선택을 코드에 담을 수 있습니다.

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class CheckedHandled {
    static String readConfig(Path path) throws IOException {
        return Files.readString(path);
    }

    public static void main(String[] args) {
        try {
            String text = readConfig(Path.of("app.txt"));
            System.out.println(text);
        } catch (IOException e) {
            System.out.println("config read failed: " + e.getClass().getSimpleName());
            System.out.println("use default profile: local");
        }
    }
}

/*
실행 결과
config read failed: NoSuchFileException
use default profile: local
*/

이 예시는 checked 예외가 "더 안전하다"는 뜻이라기보다, 호출자가 복구 방향을 코드에 적을 기회를 강제로 받는다는 뜻에 가깝습니다. 실패가 사라지는 것이 아니라, 실패를 어떻게 다룰지 선택 지점이 드러납니다.

반대로 unchecked 예외는 컴파일러가 호출부 처리 여부를 강제하지 않습니다. 컴파일은 통과하지만, 실행 중 전제조건이 깨지면 그때 예외가 터집니다.

public class UncheckedDemo {
    static int divide(int total, int people) {
        if (people < 1) {
            throw new IllegalArgumentException("people must be positive: " + people);
        }
        return total / people;
    }

    public static void main(String[] args) {
        System.out.println(divide(10, 0));
    }
}

/*
실행 결과
Exception in thread "main" java.lang.IllegalArgumentException: people must be positive: 0
    at UncheckedDemo.divide(UncheckedDemo.java:4)
    at UncheckedDemo.main(UncheckedDemo.java:10)
*/

여기서 보이는 메시지는 "복구 가능한 외부 실패"보다 "호출 코드가 잘못된 값을 넘겼다"는 쪽에 더 가깝습니다. 컴파일러가 막지 않는 이유도 비슷한 맥락으로 이해할 수 있습니다. 이 문제를 해결하려면 catch를 추가하는 것보다, divide(10, 0) 같은 호출을 만들지 않도록 코드를 고치는 쪽이 먼저입니다.

언제 무엇을 선택할까

예외 타입을 정할 때는 발생 원인을 분류하는 것보다, 호출자가 그 실패를 받아서 무엇을 바꿀 수 있는지 먼저 보는 편이 덜 헷갈립니다.

  • 재시도 여부가 달라진다면 checked를 고려할 수 있습니다.
  • 다른 파일, 다른 서버, 다른 구현체로 대체할 수 있다면 checked가 잘 맞는 경우가 많습니다.
  • 잘못된 인자, 잘못된 호출 순서, 불변식 위반이라면 unchecked 쪽이 더 읽기 쉽습니다.
  • 이미 상위 계층에서 동일한 방식으로만 실패를 처리한다면 checked를 그대로 길게 끌고 가기보다 경계에서 변환하는 선택도 가능합니다.
  • JVM 자원 고갈처럼 애플리케이션이 의미 있게 복구하기 어려운 문제는 Error로 보는 편이 맞습니다.

예를 들어 파일 읽기 실패는 호출자가 기본 설정을 쓸지, 경로를 다시 받을지, 프로그램을 종료할지 선택할 수 있으니 checked로 드러낼 이유가 있습니다. 반면 null을 넣으면 안 되는 메서드에 null이 들어온 경우는 호출부를 바로잡는 것이 핵심이라 NullPointerException이나 IllegalArgumentException 같은 unchecked 예외가 더 어울립니다.

다만 "외부 자원 실패는 항상 checked"라고 외우면 중간 계층에서 코드가 불필요하게 번거로워질 수 있습니다. 예를 들어 가장 바깥 경계에서는 결국 실패 응답 하나로 바뀔 뿐이라면, 하위 계층의 checked 예외를 도메인 의미가 있는 runtime 예외로 변환하는 쪽이 읽기 쉬운 경우도 있습니다.

커스텀 예외는 어디에 둘까

커스텀 예외를 만들 때도 기준은 같습니다. 이름을 늘리는 것보다, 그 예외를 받은 호출자가 어떤 행동을 실제로 달리하게 되는지가 더 중요합니다.

아래처럼 하위 계층에서는 외부 시스템 실패를 checked로 드러내고, 그 위 서비스 계층에서는 애플리케이션 관점의 runtime 예외로 바꾸는 구성이 자주 보입니다.

import java.io.IOException;

class PartnerUnavailableException extends Exception {
    public PartnerUnavailableException(String message, Throwable cause) {
        super(message, cause);
    }
}

class ProfileLoadException extends RuntimeException {
    public ProfileLoadException(String message, Throwable cause) {
        super(message, cause);
    }
}

class PartnerClient {
    String fetch(String memberId) throws PartnerUnavailableException {
        try {
            return callPartner(memberId);
        } catch (IOException e) {
            throw new PartnerUnavailableException("partner api timeout", e);
        }
    }

    private String callPartner(String memberId) throws IOException {
        throw new IOException("read timed out");
    }
}

class MemberService {
    private final PartnerClient client = new PartnerClient();

    String load(String memberId) {
        try {
            return client.fetch(memberId);
        } catch (PartnerUnavailableException e) {
            throw new ProfileLoadException("member profile load failed", e);
        }
    }
}

이런 구성이 도움이 되는 이유는 계층마다 관심사가 다르기 때문입니다. PartnerClient를 직접 쓰는 쪽은 재시도나 다른 파트너 선택을 고민할 수 있지만, MemberService 바깥에서는 이미 그런 선택지가 사라졌을 수 있습니다. 그 경우에는 도메인 이름이 붙은 runtime 예외가 더 읽기 쉬울 수 있습니다.

이때 중요한 점은 원인 예외를 버리지 않는 것입니다. cause를 연결하지 않으면, 나중에 로그를 봤을 때 실제 실패 지점이 끊겨 버립니다.

자주 틀리는 지점

checked와 unchecked를 단순히 "좋은 예외"와 "나쁜 예외"로 나누면 오히려 판단이 흐려집니다. 몇 가지 오해는 미리 분리해 두는 편이 좋습니다.

  • checked면 더 안전하다는 오해: 컴파일러가 처리 여부를 강제할 뿐, 복구 전략이 자동으로 좋아지는 것은 아닙니다. catch만 해 두고 아무 의미 있는 처리도 하지 않으면 오히려 장애 신호가 묻힐 수 있습니다.
  • unchecked는 절대 잡지 않는다는 오해: 최상위 경계에서는 로깅, 종료 코드 변환, 사용자 메시지 정리를 위해 unchecked 예외를 잡는 경우가 있습니다. 다만 그 지점은 보통 "복구"보다 "정리"에 가깝습니다.
  • throws Exception이면 충분하다는 오해: 너무 넓은 선언은 호출자에게 필요한 정보를 주지 못합니다. 실패 유형이 무엇인지, 어떤 대응이 가능한지 계약이 흐려집니다.
  • catch (Exception)이 편하다는 오해: IllegalArgumentException, InterruptedException, IOException처럼 성격이 다른 문제를 한데 모아 버리면 값 오류와 외부 실패가 같은 톤으로 처리됩니다.
  • catch (Throwable)도 괜찮다는 오해: 이 경우 Error까지 잡게 됩니다. 일반 애플리케이션 코드에서는 범위를 너무 넓게 잡는 편입니다.

특히 InterruptedException은 checked 예외지만 성격이 조금 다르게 느껴질 수 있습니다. 외부 실패라기보다 "이 작업을 멈추라"는 스레드 제어 신호에 가깝기 때문입니다. 이런 예외를 잡고 무시하면 상위 코드가 의도한 취소 흐름이 사라질 수 있습니다. checked와 unchecked 구분만 외우기보다, 그 예외가 값 오류인지, 상태 문제인지, 외부 조건인지, 제어 신호인지 함께 보는 편이 도움이 됩니다.

원문 참고

https://www.maeil-mail.kr/question/50

댓글

이 블로그의 인기 게시물

아이콘 폰트 (icomoon 사용법)

 장난감 프로젝트를 만들다 보면, 아이콘이 필요한 경우가 있다. 간단하게 아이콘을 인터넷에서 검색하여, 이미지로 넣어두고 이미지 태그를 이용하여, 사용하는 경우가 일반적이였지만...  요즘에는 대부분 폰트를 이용하여 아이콘을 노출 한다. 나 같은 경우에도 기본적으로  https://material.io/resources/icons 를 참고하여 아이콘 폰트를 이용할 수 있도록 처리하고, 추가적으로 필요한 아이콘이고, 일상적으로 사용 되지 않는 아이콘의 경우에는  https://icomoon.io 에서 제작하여, 아이콘 폰트로 이용 하곤 한다.  그래서 이번에는 아이콘  https://icomoon.io 의 사용법을 간단히 공유하고자 한다.   들어가자 마자 위의 icoMoonApp버튼을 누르면 아래와 같은 화면이 나타난다.  icomoon에서 무료로 제공하는 아이콘들이 보이면 위에 파란색으로 표시 되어있는 집 모양 세가지를 선택한 후, 아래의 빨간색으로 표시되어있는 Generate Font를 눌러보자.  그리고 나서 바로 다운로드를 요청해보자. icomoon.zip이 다운로드가 될텐데, 압축을 해제해 보면, 아래의 폴더 및 파일들이 있다. 아래에서 중요한 것은 font 폴더와 style.css이다. demo-files fonts demo.html Read Me.txt selection.json style.css <!doctype html > <html> <head> <link rel ="stylesheet" href ="style.css" ></head> </head> <body> <span class ="icon-home" ></span> <span class ="icon-home2" ></span> <span class ="icon-hom...

Chart js와 amchart 비교

Chart js 특징은 위의 그림으로 대체 할 수 있을 듯 하다. 오픈 소스이고, 기본으로 제공하는 차트 종류가 8가지 Canv a s를 이용해서 차트를 그리고, 반응형을 지원한다. amchart amchart는 기본적으로 유료이며, 기본으로 제공하는 차트 종류가 기본적인 차트 + 주식 처럼 보이는 차트 + 지도에 관련된 차트(?) 까지 하면, 기본 제공 하는 종류가 20개 내외 이려나, 일일이 세기에는 양이 좀 많아 보인다. 렌더링은 svg를 통하여 그려지고, 당연 반응형도 지원이 된다. 그러면, 이 둘중에 어떤것이 내 프로젝트에 적합 하냐는 것이 문제이다. 일단, 주식 처럼 보이는 차트나 지도에 관련된 차트(?)가 필요하면, amchart를 선택해야 되는 것은 맞다. 그건 당연한 것이니 빼고 얘기 해보자! 여러 종류의 차트가 필요하다면, 일단은 amchart를 염두해 두는 것이 좋다. 돈 낸 만큼은 하는 듯 하다. 하지만, 기본적인 막대 그래프, 도넛 차트 등, 아주 기본적인 차트들인데, Chart js도 amchart도 그러한 차트가 없을 때가 문제가 된다. 그렇다면, 조금이라도 커스텀이 용이한 것을 찾는 것이 좋을 것이다.  일단 amchart에서 custom이라고 검색 하였을 때, 검색 결과가 61가지가 나온다. 차트의 종류도 많고, 각 차트마다 들어가는 속성이 매우 많기 때문에, 웬만한 내용들은 속성 값을 어떻게 주느냐에 따라서 변경이 가능 하게 된다. 커스텀의 예를 들면, 기본적으로 도넛 파이의 형태를 띄면서, 화살표로 목표를 표시해주는 차트가 필요하다고 생각 해보자. 이것은 amchart로 만든 그래프이고 이것은 chart js로 만든 그래프이다. 모양이 살짝 다르긴 하지만, 완벽하게 똑같이 구현 할 수도 있다. amchart로 만든 그래프의 경우, 저것은 도넛그래프가 아닌 guage 그래프이다. 원래 게이지 그래프는 이와 같...

javascript 압축 파일 다운로드

이번에는 전 게시글의 응용판? 이라고 해야하나....? 어쨋든! 우리는 각각의 파일들을 다운로드 해보았다. 그런데 생각보다 귀찮음?을 느꼇을 것이다. 파일을 각각 다운 받아야 한다는 현실때문에! 그래 파일 두개야 뭐 그렇다 치지... 하지만, 개발자도 사용자도 게으름뱅이이다. 자 결국, 우리가 해야 하는 것은 파일을 한 번에 둘다! 다운 받는 것이다. 물론, 클릭 한번에 여러개의 함수를 엮어서 다운받게 하면 되지만! 크롬에서 자주 봤듯이, 여러개의 파일을 다운로드를 시도하면 <- 여러개의 파일을 다운로드 합니다. 허용 합니까? 하고 물어보는 것을 볼 수 있다. 게다가 다운로드 한 파일들을 찾기도 귀찮다는 것. 자 해결책을 제시해보자면, https://github.com/Stuk/jszip 클라이언트 단에서 파일을 zip파일로 압축을 할 수가 있다! 필요한 작업은 아래와 같다. 0. 데이터 준비 1. BLOB(binary large object)를 만든다. 2. Blob을 URL.createObjectURL을 사용하여, 해당 binary의 주소를 생성. 3. 다운로드가 필요한 파일들을 Zip 객체에 셋팅! 4. a태그를 이용하여, 해당 url 셋팅 하고, 다운로드. 전 게시물과 별로 달라진게 없네... 자 그럼 샘플! 샘플을 보자! http://embed.plnkr.co/NMprnRxqYG0fkHa2J55D/ var util = {} function fixBinary(bin) { //binary to arrayBuffer var length = bin.length var buf = new ArrayBuffer(length) var arr = new Uint8Array(buf) for (var i = 0; i < length; i++) { arr[i] = bin.charCodeAt(i) } return buf } window.onload = function() { ...