CodeGym /행동 /JAVA 25 SELF /사용자 정의 예외 만들기

사용자 정의 예외 만들기

JAVA 25 SELF
레벨 24 , 레슨 1
사용 가능

1. 소개

Java 표준 라이브러리에는 이미 많은 예외가 있습니다: NullPointerException, IllegalArgumentException, IOException 등. 그러나 때로는 표준 예외만으로는 프로그램에서 발생한 오류를 명확하고 이해하기 쉽게 설명하기에 충분하지 않을 수 있습니다.

실생활 예시:
당신은 은행 애플리케이션을 작성하고 있습니다. 사용자가 계좌에 있는 돈보다 더 많은 금액을 인출하려고 합니다. IllegalArgumentException을 던질 수도 있습니다. 하지만 InsufficientFundsException 같은 자체 예외를 제공하면 훨씬 더 이해하기 쉽습니다. 그러면 코드만 보아도 무슨 일이 있었는지 바로 알 수 있습니다.

사용자 정의 예외는 애플리케이션을 올바르게 커스터마이징하는 방법입니다. 문제를 세밀하게 다룰 수 있게 해 주고, 이름만으로도(논리적으로 이름을 지었다면!) 무슨 일이 발생했는지 바로 드러납니다. 또한 자기 문서화 성격이 있어: 시그니처에 throws MyException가 있는 메서드는 어떤 오류가 발생할 수 있는지 즉시 알려줍니다. 게다가 잔액, 거래 금액 등과 같은 추가 필드를 넣어 코드에서 적절히 활용할 수도 있습니다.

2. 사용자 정의 예외는 어떻게 만들까요?

매우 간단합니다: 표준 예외 클래스 중 하나를 상속하는 새 클래스를 만드세요.

  • 체크 예외(checked) — Exception을 상속하세요.
  • 언체크 예외(unchecked) — RuntimeException을 상속하세요.

예: 체크 예외

public class InvalidCredentialsException extends Exception {
    public InvalidCredentialsException(String message) {
        super(message); // 상위 클래스에 메시지를 전달합니다
    }
}

이제 코드에서 이 예외를 던질 수 있습니다:

if (!login.equals("admin") || !password.equals("1234")) {
    throw new InvalidCredentialsException("잘못된 로그인 또는 비밀번호");
}

예: 언체크 예외

public class NegativeBalanceException extends RuntimeException {
    public NegativeBalanceException(String message) {
        super(message);
    }
}

언제 checked, 언제 unchecked?

  • 체크 예외 — 오류가 예상 가능하고 이를 처리할 수 있을 때(예: 검증 오류, 파일 없음, 잘못된 사용자 데이터).
  • 언체크 예외 — 프로그램 로직의 버그와 관련 있을 때(예: 0으로 나누기, 불변식 위반).

3. 생성자: 예외를 더 정보성 있게 만들기

보통 사용자 정의 예외 클래스에는 최소한 String message 매개변수를 받는 생성자를 하나 구현합니다. 하지만 종종 다른 생성자들도 추가합니다:

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

설명:

  • message — 오류에 대한 텍스트 설명입니다.
  • cause — 원인(다른 예외)으로, 한 오류를 다른 오류로 감싸고 싶을 때 사용합니다.

팁: 어떤 생성자가 필요한지 잘 모르겠다면, 문자열을 받는 생성자만이라도 추가하세요.

4. 코드에서 사용자 정의 예외 사용하기

예를 들어 살펴봅시다: 사용자가 있고, 그 사용자에게 점수가 있으며, 100을 초과하여 추가할 수 없습니다.

public class User {
    private String name;
    private int score;

    public User(String name) {
        this.name = name;
        this.score = 0;
    }

    public void addScore(int points) throws ScoreLimitExceededException {
        if (score + points > 100) {
            throw new ScoreLimitExceededException("점수 한도를 초과했습니다! 추가하려 한 값: " + points);
        }
        this.score += points;
    }
}

예외 클래스:

public class ScoreLimitExceededException extends Exception {
    public ScoreLimitExceededException(String message) {
        super(message);
    }
}

처리:

try {
    user.addScore(60);
    user.addScore(50); // 여기서 예외가 던져집니다!
} catch (ScoreLimitExceededException e) {
    System.out.println("오류: " + e.getMessage());
}

결과:

오류: 점수 한도를 초과했습니다! 추가하려 한 값: 50

아마 이런 의문이 들 수 있습니다: 예외 대신 if 조건을 사용해 false나 다른 특별한 값을 반환하여 작업이 실패했음을 알리는 방법은 어떨까요? 예를 들면 다음과 같습니다:

public boolean addScore(int points) {
    if (score + points > 100) {
        return false; // 처리하고 싶지 않다면 아무 RuntimeException이나 던질 수도 있습니다
    }
    this.score += points;
    return true;
}

이런 접근은 더 쉬워 보일 수 있지만, 심각한 오류나 애플리케이션 로직 위반이 걸려 있을 때는 단점이 있습니다.

첫째, 실패를 나타내기 위해 false나 다른 값을 반환하면 호출 측 코드가 항상 확인해야 합니다. 개발자가 이를 깜박하면 오류가 눈치채지 못한 채 지나가 예측 불가능한 동작으로 이어질 수 있습니다. 반면 예외는 처리를 강제(체크 예외의 경우)하고, 최소한 문제를 명확히 신호합니다(잡히지 않은 경우).

둘째, 예외는 오류의 의미를 더 명확히 전달합니다. false를 반환하는 것은 무엇이든 의미할 수 있습니다: “실패”, “적용 불가”, “사용 불가”. ScoreLimitExceededException은 “점수 한도가 초과되었다”는 사실을 분명하고 일의적으로 알려 줍니다. 이는 코드의 가독성과 유지보수성을 높입니다.

셋째, 예외는 오류 처리를 중앙집중화할 수 있게 해 줍니다. if 검사를 코드 전반에 흩뿌리는 대신, addScore가 호출되는 곳에서 던져진 예외를 한 곳에서 잡아 사용자에게 메시지를 보여주거나, 로그를 남기거나, 트랜잭션을 롤백하는 등의 결정을 할 수 있습니다.

마지막으로, 한도 초과와 같은 문제는 정말로 예외적인 상황입니다(이름 그대로). 프로그램의 정상 흐름은 점수가 성공적으로 추가되는 것을 전제로 합니다. 그렇지 않다면 비즈니스 로직이나 객체의 불변식이 깨진 것이며, 이는 예외를 사용할 이상적인 시나리오입니다.

5. 예외에 사용자 정의 필드 추가하기

오류 처리를 돕는 추가 데이터를 예외에 넣는 것이 유용할 때가 있습니다.

예:

public class ScoreLimitExceededException extends Exception {
    private int currentScore;
    private int attemptedAdd;

    public ScoreLimitExceededException(String message, int currentScore, int attemptedAdd) {
        super(message);
        this.currentScore = currentScore;
        this.attemptedAdd = attemptedAdd;
    }

    public int getCurrentScore() { 
        return currentScore; 
    }
    public int getAttemptedAdd() { 
        return attemptedAdd; 
    }
}

사용:

if (score + points > 100) {
    throw new ScoreLimitExceededException(
        "점수 한도를 초과했습니다!",
        this.score,
        points
    );
}

6. 유용한 팁

예외 이름은 어떻게 지을까요?

Java에서는 사용자 정의 예외에 Exception 접미사를 붙이는 것이 관례입니다: InvalidUserInputException, InsufficientFundsException, ScoreLimitExceededException.

자신의 예외를 그냥 Error 또는 Warning이라고 이름 짓지 마세요 — 다른 개발자(그리고 몇 주 뒤의 본인)에게 혼란을 줄 수 있습니다.

어디서, 언제 사용자 정의 예외를 던질까요?

  • 사용자 입력 검증 시(예: 빈 이름, 음수 나이).
  • 비즈니스 규칙 위반 시(예: 한도 초과, 계좌 잔액보다 많은 금액 인출 시도).
  • 외부 서비스 작업 오류 시(예: 서비스 불가, 시간 초과).

7. 사용자 정의 예외를 만들 때 흔한 실수

오류 № 1: 잘못된 클래스를 상속함.
Exception(또는 RuntimeException)에서 상속하고, Throwable이나 Error에서 상속하지 마세요.

오류 № 2: 메시지를 받는 생성자가 없음.
문자열(String message)을 받는 생성자가 없으면 예외는 묵묵부답이 되어 디버깅이 어려워집니다.

오류 № 3: 비즈니스 로직에 표준 예외를 사용함.
자체의 ‘말이 되는’ 예외가 필요한 곳에서 NullPointerException이나 IllegalArgumentException을 던지지 마세요.

오류 № 4: 사용자 정의 예외의 남용.
사소한 매번 상황마다 별도의 예외 클래스를 만들지 마세요. 도메인에 특화되지 않은 오류라면 표준 예외를 사용하세요.

오류 № 5: 직렬화 누락(드물지만 발생).
예외를 네트워크로 전송하거나 저장해야 한다면 implements Serializable을 구현하는 것이 좋습니다. 그러나 단순한 애플리케이션에서는 필수는 아닙니다.

1
과제
JAVA 25 SELF, 레벨 24, 레슨 1
잠금
게임 문지기: 플레이어 점수 검사
게임 문지기: 플레이어 점수 검사
1
과제
JAVA 25 SELF, 레벨 24, 레슨 1
잠금
비밀번호 요새: 사용자 데이터 보호
비밀번호 요새: 사용자 데이터 보호
1
과제
JAVA 25 SELF, 레벨 24, 레슨 1
잠금
놀이공원 연령 검사: 유연한 오류 메시지
놀이공원 연령 검사: 유연한 오류 메시지
1
과제
JAVA 25 SELF, 레벨 24, 레슨 1
잠금
마법 유물 창고 관리: 초과 수용에 대한 정확한 세부 정보
마법 유물 창고 관리: 초과 수용에 대한 정확한 세부 정보
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION