75. 예외의 상세 메시지에 실패 관련 정보를 담으라
👉 들어가며
예외가 발생하면 자바 시스템은 예외 객체의 클래스 이름과 함께 상세 메시지(detail message)를 자동으로 기록합니다. 이 정보는 스택 트레이스(stack trace)에 통합되어 예외의 원인과 발생 위치를 파악하는 데 중요한 단서가 됩니다. 따라서 예외의 상세 메시지는 얼마나 많은 정보를 담고 있느냐에 따라 문제 해결의 난이도가 크게 달라집니다.
✅ 핵심 요약
예외 메시지의 중요성: 예외 메시지는 프로그램 실패 원인을 진단하기 위한 첫 번째 정보이므로 관련된 모든 정보를 담아야 합니다.
필수 포함 정보: 예외와 관련된 모든 매개변수와 필드 값을 메시지에 포함시켜야 합니다.
보안 주의사항: 비밀번호, 암호화 키 등 보안에 민감한 정보는 절대 포함하지 않아야 합니다.
대상 구분: 예외 메시지는 사용자가 아닌 프로그래머와 SRE(Site Reliability Engineer)를 위한 것임을 명심해야 합니다.
📚 예외 메시지와 스택 트레이스 이해하기
스택 트레이스란?
스택 트레이스는 예외가 발생했을 때 시스템이 자동으로 출력하는 정보로, 예외의 발생 위치와 호출 스택을 보여줍니다. 여기에는 예외 클래스 이름과 상세 메시지가 포함됩니다.
java.lang.IndexOutOfBoundsException: Index: 11, Size: 10
at java.util.ArrayList.rangeCheck(ArrayList.java:653)
at java.util.ArrayList.get(ArrayList.java:429)
at com.example.MyClass.main(MyClass.java:22)
이 예시에서, Index: 11, Size: 10
이 바로 예외의 상세 메시지입니다. 이 정보는 문제 해결에 중요한 단서가 됩니다.
예외 메시지의 목적과 중요성
예외 메시지는 프로그래머나 운영자가 로그나 콘솔을 통해 예외를 분석할 때 유일하게 가질 수 있는 정보일 수 있습니다. 특히 다음과 같은 경우에 그 중요성이 더욱 부각됩니다:
1. 개발 환경이 아닌 프로덕션 환경에서 발생한 예외
2. 로그만으로 문제를 분석해야 하는 상황
3. 재현하기 어려운 간헐적 문제
🎨 효과적인 예외 메시지 설계 방법
1. 관련 정보 포함하기
예외 메시지에는 예외와 관련된 모든 매개변수와 필드 값을 포함시켜야 합니다
// 좋지 않은 예
throw new IllegalArgumentException("잘못된 인덱스");
// 좋은 예
throw new IllegalArgumentException("인덱스: " + index + ", 범위: 0~" + (size - 1));
2. 예외 생성자 활용하기
예외 클래스를 직접 설계할 때는 생성자에서 필요한 정보를 받아 상세 메시지를 생성하는 것이 좋습니다
/**
* 인덱스 범위 예외를 생성한다.
*
* @param lowerBound 인덱스의 최솟값
* @param upperBound 인덱스의 최댓값 + 1
* @param index 실제 인덱스 값
*/
public class IndexOutOfBoundsException extends RuntimeException {
private final int lowerBound;
private final int upperBound;
private final int index;
public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) {
// 실패에 대한 상세 메시지를 생성한다
super(String.format("최솟값: %d, 최댓값: %d, 인덱스: %d",
lowerBound, upperBound, index));
// 프로그램에서 이용할 수 있도록 실패 정보를 저장한다
this.lowerBound = lowerBound;
this.upperBound = upperBound;
this.index = index;
}
// 접근자 메서드
public int getLowerBound() { return lowerBound; }
public int getUpperBound() { return upperBound; }
public int getIndex() { return index; }
}
이렇게 설계하면 다음과 같은 장점
일관된 메시지 형식: 같은 예외 타입에 대해 일관된 형식의 메시지를 제공합니다.
코드 중복 감소: 예외를 던지는 모든 곳에서 메시지 포맷을 반복하지 않아도 됩니다.
프로그래밍 접근성: 접근자 메서드를 통해 예외 정보에 프로그래밍 방식으로 접근할 수 있습니다.
3. 자바 9의 변화와 개선사항
자바 9부터는 IndexOutOfBoundsException
클래스에 인덱스 값을 받는 생성자가 추가되었습니다:
// Java 9+
public IndexOutOfBoundsException(int index) {
super("Index out of range: " + index);
}
하지만 이 생성자는 최솟값과 최댓값은 받지 않습니다. 이는 IndexOutOfBoundsException
이 다양한 컨텍스트에서 사용될 수 있기 때문입니다.
👓 예외 메시지 작성 시 주의사항
1. 보안 정보 제외
예외 메시지에는 보안에 민감한 정보를 포함하지 않아야 합니다
// 안좋은 예 - 보안 문제
throw new IllegalArgumentException("유효하지 않은 신용카드 번호: " + cardNumber);
// 좋은 예
throw new IllegalArgumentException("유효하지 않은 신용카드 번호");
다음은 절대 포함해서는 안 되는 정보의 예시입니다:
비밀번호
암호화 키
개인식별정보(PII)
신용카드 번호
API 키
접근 토큰
2. 사용자 메시지와 예외 메시지 구분
예외 메시지는 개발자와 운영자를 위한 것이며, 최종 사용자를 위한 메시지와는 명확히 구분되어야 합니다
try {
// 작업 수행
} catch (Exception e) {
// 로그에는 상세한 예외 정보를 기록
logger.error("데이터 처리 실패: " + e.getMessage(), e);
// 사용자에게는 친화적인 메시지 표시
showUserFriendlyMessage("서비스 일시적 오류가 발생했습니다. 잠시 후 다시 시도해주세요.");
}
3. 간결하고 명확하게 작성하기
예외 메시지는 필요한 정보를 모두 담되, 불필요하게 장황해서는 안 됩니다
// 너무 장황한 예
throw new IllegalArgumentException(
"입력 값 " + input + "은(는) 유효하지 않습니다. 이 필드는 숫자만 허용하며, " +
"입력된 값에는 문자가 포함되어 있습니다. 숫자만 입력해주세요. 예: 123");
// 적절한 예
throw new IllegalArgumentException(
"숫자만 허용되는 입력에 문자가 포함됨: " + input);
🥼 실제 활용 사례와 모범 사례
표준 라이브러리 예외 클래스의 메시지 설계
자바 표준 라이브러리의 예외 클래스들
은 대체로 좋은 예외 메시지 설계를 보여줍니다
// NumberFormatException의 예
throw new NumberFormatException("For input string: \"" + s + "\"");
// IllegalArgumentException의 예
throw new IllegalArgumentException("Illegal character for radix " + radix + ": " + ch);
커스텀 예외 설계 패턴
복잡한 시스템에서는 다음과 같은 패턴으로 예외를 설계하는 것이 좋습니다
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
private final Map<String, Object> data = new HashMap<>();
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public BusinessException addData(String key, Object value) {
data.put(key, value);
return this;
}
@Override
public String getMessage() {
return String.format("[%s] %s %s",
errorCode.getCode(),
super.getMessage(),
data.isEmpty() ? "" : data.toString());
}
// 접근자 메서드들
public ErrorCode getErrorCode() { return errorCode; }
public Map<String, Object> getData() { return Collections.unmodifiableMap(data); }
}
이 패턴은 다음과 같은 장점
에러 코드를 통한 예외 분류
동적 데이터 추가 가능
메시지 포맷의 일관성 유지
프로그래밍 방식의 예외 정보 접근
실전 활용 예시
try {
processOrder(order);
} catch (BusinessException e) {
if (e.getErrorCode() == ErrorCode.INSUFFICIENT_STOCK) {
// 재고 부족 상황 처리
int availableStock = (Integer) e.getData().get("availableStock");
logger.warn("재고 부족 발생: " + e.getMessage());
// 사용자에게 재고 상황 안내
}
}
// 예외 발생 부분
if (requestedQuantity > availableStock) {
throw new BusinessException(ErrorCode.INSUFFICIENT_STOCK,
"요청한 수량이 재고를 초과합니다")
.addData("requestedQuantity", requestedQuantity)
.addData("availableStock", availableStock)
.addData("productId", product.getId());
}
🎯 결론
예외 메시지는 단순한 오류 알림 그 이상입니다. `디버깅, 로깅, 유지보수의 핵심 정보이자, 시스템의 신뢰성과 투명성을 보여주는 창구입니다. 예외를 설계할 때는 메시지에 충분한 정보를 담아야 하며, 프로그래밍적으로도 활용 가능한 구조를 갖추는 것이 바람직합니다.
예외 메시지는 항상 개발자와 운영자가 문제를 빠르게 진단할 수 있도록 작성되어야 합니다. 중요한 매개변수 값, 실패한 조건, 관련 필드 등을 포함하고, 민감한 정보는 절대 노출해서는 안 됩니다. 또한 사용자에게 보여줄 메시지와는 철저히 분리해야 하며, 시스템 내부용 메시지는 일관된 형식과 수준으로 관리되어야 합니다.
복잡한 시스템에서는 에러 코드까지 함께 설계해 예외를 구조적으로 관리하고, 로깅 시스템과도 잘 연동되도록 설계하면 운영상의 큰 도움이 됩니다. 마지막으로, 예외 메시지도 테스트의 대상이 되어야 하며, 예외 클래스는 '설계 대상'이라는 점을 항상 기억해야 합니다.
잘 설계된 예외 메시지는 버그보다 빠르고, 로그보다 정확하다. 다시 말해, 예외 메시지는 ‘개발자를 위한 문서’이며 ‘운영을 위한 도구’입니다.
Last updated