Language/Java

[effective java] 아이템 75. 예외의 상세 메시지에 실패 관련 정보를 담으라.

JOYERIM 2023. 8. 12. 14:05

 

 

 

 

 

핵심 요약

 

  • 스택 추적은 예외 발생 시점의 메소두 호출 정보를 제공하여 디버깅을 돕는다.
  • 예외 메시지에는 관련 매개변수와 필드 정보를 포함하여 구체적인 실패 원인을 알 수 있게 해야 한다.
  • 예외 클래스를 확장하고, 필요한 상세 정보를 얻기 위한 접근자 메소드를 제공하는 것이 좋다.

 

 

 

 

 

 

스택 추적(Stack Trace)

 

  • 프로그램이 실행되면서 메소드 호출이 일어날 때마다 해당 메소드의 정보가 스택에 쌓인다.
  • 이때, 프로그램에서 예외가 발생하면 그 시점의 호출 스택을 표시하는 정보를 "스택 추적"이라고 한다.
    • ex. 예외가 발생한 순간의 메소드 호출 순서, 위치 정보, 관련 파일명 등
  • 예외가 발생하고 해당 예외를 잡아내지 못하면, JVM은 그 예외의 스택 추적 정보를 자동으로 콘솔에 출력한다.
  • 예외 객체의 toString 메소드를 호출하여 얻은 문자열을 기반으로 한다.
    • 형식: 예외 클래스 이름 출력 + 상세 메시지

 

예시 1
public static void main(String[] args) {
    String str = null;
    System.out.println(str.length()); // NullPointerException 발생
}
Exception in thread "main" java.lang.NullPointerException
    at com.example.Main.main(Main.java:3)

 

 

 

 

 

 

실패 순간을 포착하려면 발생한 예외에 관여된 모든 매개변수와 필드의 값을 실패 메시지에 담아야 한다.

 

  • cf. 상세 메시지에 보안과 관련된 정보는 담아서는 안 된다.
  • 관련 데이터를 모두 담아야 하지만 장황할 필요는 없다. 즉, 문서와 소스코드에서 얻을 수 있는 정보는 길게 작성할 필요가 없다.
  • 예외의 상세 메시지와 최종 사용자에게 보여줄 오류 메시지를 혼동해서는 안 된다.
  • 예외의 생성자에서 필요한 정보를 모두 받아서 상세 메시지를 미리 생성하는 것도 좋은 방법이다. 
    • 예외를 사용하는 곳마다 상세 메시지를 생성하는 로직을 반복해서 작성하지 않아도 되기 때문이다.

 

예시 2 - IndexOutOfBoundsException
/**
 * IndexOutOfBoundsException을 생성한다.
 *
 * @param lowerBound 인덱스의 최솟값
 * @param upperBound 인덱스의 최댓값 + 1
 * @param index 인덱스의 실젯값
 */
public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) {
   // 실패를 포착하는 상세 메시지를 생성한다.
   super(String.format("최솟값: %d, 최댓값: %d, 인덱스: %d", lowerBound, upperBound, index));
   
   // 프로그램에서 이용할 수 있도록 실패 정보를 저장해둔다.
   this.lowerBound = lowerBound;
   this.upperBoudn = upperBound;
   this.index = index;
}
  • IndexOutOfBoundsException의 새로운 생성자는 인덱스의 최솟값, 최댓값, 인덱스 값을 받는다.
  • 이 생성자는 전달된 값을 이용하여 상세한 메시지를 생성하고, 해당 정보를 내부 필드에 저장한다.
  • 자바 9에서는 IndexOutOfBoundsException에 정수 인덱스 값을 받는 생성자가 추가되었다.
    • 그러나 최솟값과 최댓값까지 받는 생성자는 추가되지 않았다.
// 상세 메시지 생성이 예외 생성자에 없는 경우
try {
    // ... 
} catch(IndexOutOfBoundsException e) {
    throw new IndexOutOfBoundsException("최솟값: " + lowerBound + ", 최댓값: " + upperBound + ", 인덱스: " + index);
}
// 상세 메시지 생성이 예외 생성자에 포함된 경우
public IndexOutOfBoundsException(int lowerBound, int upperBound, int index) {
    super(String.format("최솟값: %d, 최댓값: %d, 인덱스: %d", lowerBound, upperBound, index));
}

try {
    // ... 
} catch(IndexOutOfBoundsException e) {
    throw new IndexOutOfBoundsException(lowerBound, upperBound, index);
}
  • 상세 메시지 생성 로직이 예외 클래스 내부에 숨겨져 있어, 코드 중복이 줄어들게 된다.

 

 

  • 물론 검사 예외에 비해 비검사 예외에서는 중요도가 떨어지긴 하지만, 상세 정보를 알려주는 접근자 메소드를 사용하는 것이 좋다.

 

예시 3
public class DetailedIndexOutOfBoundsException extends IndexOutOfBoundsException {
    private final int lowerBound;
    private final int upperBound;
    private final int index;

    public DetailedIndexOutOfBoundsException(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;
    }
}
try {
    // ... 
} catch(DetailedIndexOutOfBoundsException e) {
    System.out.println("실제 인덱스 값: " + e.getIndex());
    // 추가적인 처리
}

 

 

 

 

 

추가 내용

 

 

[Spring] 스프링의 다양한 예외 처리 방법(ExceptionHandler, ControllerAdvice 등) 완벽하게 이해하기 - (1/2)

예외 처리는 애플리케이션을 만드는데 매우 중요한 부분을 차지한다. Spring 프레임워크는 매우 다양한 에러 처리 방법을 제공하는데, 어떠한 방법들이 있고 가장 좋은 방법(Best Practice)은 무엇인

mangkyu.tistory.com

 

 

[Spring] @RestControllerAdvice를 이용한 Spring 예외 처리 방법 - (2/2)

예외 처리는 robust한 애플리케이션을 만드는데 매우 중요한 부분을 차지한다. Spring 프레임워크는 매우 다양한 에러 처리 방법을 제공하는데, 앞선 포스팅에서 @RestControllerAdvice를 사용해야 하는

mangkyu.tistory.com

 

 

예시 4
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product {
    
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private Double price;

    @Builder
    public Product(String name, Double price) {
        this.name = name;
        this.price = price;
    }

    // ... 기타 메서드 및 필드 ...
}
@Data
@Builder
public class ProductDto {
    private Long id;
    private String name;
    private Double price;
    // ... 필요한 필드 추가 가능 ...
}
@Service
public class ProductService {

    @Autowired
    private ProductRepository productRepository;

    public ProductDto findProductById(Long productId) {
        Product product = productRepository.findById(productId)
                .orElseThrow(() -> new ProductNotFoundException("Product with ID " + productId + " not found."));

        return convertToDto(product);
    }

    private ProductDto convertToDto(Product product) {
        return ProductDto.builder()
                .id(product.getId())
                .name(product.getName())
                .price(product.getPrice())
                .build();
    }

    public ProductDto createProduct(String name, Double price) {
        Product product = Product.builder()
                .name(name)
                .price(price)
                .build();

        productRepository.save(product);

        return convertToDto(product);
    }
}
@RestController
@RequestMapping("/products")
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("/{productId}")
    public ResponseEntity<ProductDto> getProduct(@PathVariable Long productId) {
        ProductDto product = productService.findProductById(productId);
        return new ResponseEntity<>(product, HttpStatus.OK);
    }

    @PostMapping("/")
    public ResponseEntity<ProductDto> createProduct(@RequestBody ProductDto productDto) {
        ProductDto createdProduct = productService.createProduct(productDto.getName(), productDto.getPrice());
        return new ResponseEntity<>(createdProduct, HttpStatus.CREATED);
    }
}
public class ProductNotFoundException extends RuntimeException {
    public ProductNotFoundException(String message) {
        super(message);
    }
}
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ProductNotFoundException.class)
    public ResponseEntity<String> handleProductNotFoundException(ProductNotFoundException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
    }

    // ... 다른 예외 핸들러 메서드들 ...
}
  • ProductNotFoundException이 발생할 때 어떤 상품ID로 인해 발생했는지 그 정보를 메시지에 포함시켰다.
  • 또한 해당 예제에서는 GlobalExceptionHandler에서 해당 예외를 핸들링할 때 이 정보를 클라이언트에게 전달한다.