Java 예외 처리 꿀팁

이상민·2021년 10월 14일
1
post-thumbnail

Java 예외 처리의 Best Practice들을 정리해 봤다

1. Throwable을 잡지 마라

// X
catch (Throwable) {
    // Do something
}
  • Throwable 모든 예외와 오류의 상위 클래스이다. catch 문에도 사용은 할 수 있지만, 모든 예외 뿐만 아니라 오류까지 잡아버리게 된다. 오류는 JVM이 던지는 것으로 어플리케이션에서 처리할 수 있는 것이 아니다

  • 비슷한 이유로 RuntimeException이나 Exception을 잡을 때도 정말 필요한지 생각해보자


2. 사용자 입력을 최대한 빨리 검증하라

  • 사용자 입력이 컨트롤러에 도달하기도 전에 검증을 하면, 예외 처리를 위한 코드를 어플리케이션의 핵심 로직에 최소화할 수 있다
// 검증을 늦게하는 경우
1. 입력을 받는다 (잘못된 엔티티 필드 값)
2. 요청 DTO로 매핑되 컨트롤러로 전달한다
3. 컨트롤러가 서비스로 전달한다
4. 서비스가 DTO를 엔티티로 변환할때 검증한다

// 검증을 빨리하는 경우
1. 입력을 받는다 (잘못된 엔티티 필드 값)
2. 요청 DTO에서 검증한다

3. 예외로 흐름제어하지 마라

  • 예외는 오류이다. 예외를 로직으로 사용해서는 안된다. 아래코드는 Put 메소드의 멱등성을 위해 updatePost()메소드가 던지는 PostNotFoundException을 사용해 포스트가 존재하지 않을 시 savePost()로 분기한다.
    // 예외로 분기하는 잘못된 코드
    @PutMapping("/posts/{postId}")
    public ApiResponse<PostDto> updatePost(
        @PathVariable Long postId,
        @RequestBody final PostRequest postRequest
    ) {
        try {
            PostDto post = postService.updatePost(postId, postRequest);
            return ApiResponse.response(post);
        } catch (PostNotFoundException e) {
            PostDto post = postService.savePost(postRequest);
            return ApiResponse.response(post);
        }
    }

4. 하나의 예외는 하나의 로그에 작성하라

// X
catch (SomeException e) {
    log.error("some info");
    log.error("another info");
}

// O
catch (SomeException e) {
    log.error("some info, another info");
}
  • 개발할때는 문제가 없어 보일 수 있지만, 수 많은 요청이 들어오는 상황이라면 여러 줄로 나눈 로그는 다른 로그와 섞여 멀리 떨어지게 될 수도 있다

5. 예외를 감싸서 던져라

  • 예외 전파라고도 한다. 자세한 것은 이전에 정리한 적 있다

  • 단 예외 체이닝을 하기전, 감싸고 있는 예외가 새로운 효용을 주는지 생각해보자. 아래 같은 예외 체이닝은 별로 도움이 되지 않는다

public void persistCustomer(Customer c) throws MyPersistenceException {
	// persist a Customer
}

public void manageCustomer(Customer c) throws MyBusinessException {
	// manage a Customer
	try {
		persistCustomer(c);
	} catch (MyPersistenceException e) {
		throw new MyBusinessException(e, e.getCode()); 
	}
}

public void createCustomer(Customer c) throws MyApiException {
	// create a Customer
	try {
		manageCustomer(c);
	} catch (MyBusinessException e) {
		throw new MyApiException(e, e.getCode()); 
	}
}

6. 절대 finally 블록에서 예외를 던지지 마라

try {
    service.getSomething();  // 예외를 던지는 메소드
}
finally {
    throw new SomeException();  // 예외 체이닝이 끊긴다 
}
  • 예외가 어디서부터, 왜, 어떤 과정을 거쳐 발생했는지 남기는 것은 중요하다. finally 블록에서 예외를 던진다면 이전까지의 stack trace를 기록할 수 없게 된다

7. 예외 로그를 남기고 던지지 마라

try {
    new Long("xyz");
} catch (NumberFormatException e) {
    log.error(e);
    throw e;
}


17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"

... 무수히 많은 로그들 ...

Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.(Long.java:965)
at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)
at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)
  • 하나의 예외에 대해서 로그도 남기고 전달도 한다면 여러 로그 메시지가 기록되게 된다. 로그를 보며 디버깅할때 매우 힘들어질 수 있다. 둘 중 하나만 하자

  • 예외를 로깅할것이라면 처리할때 로깅해야한다


8. 빨리 던지고 늦게 잡아라

빨리 던져라

  • 예외는 할 수 있는 한 최대한 빨리 던지는 것이 좋다. 에러가 발생한 곳에서 예외를 바로 발생 시키면, 로직 등이 모두 그곳에 있기 때문에 왜 예외가 발생했는지 알기 가장 쉽다. stack trace를 따라 갈때도 예외를 발생 시킨곳이 문제가 있는 곳이기 때문에 디버깅하기 좋다

늦게 잡아라

  • 예외를 늦게 잡으라는 것은 처리하기 적합하지 않은 곳에서 무리하게 예외 처리하려고 하지 말라는 것이다. 예를 들어 최상위 레이어에서 사용자에게 파일의 위치를 입력 받고, 최하위 레이어에서 이 파일의 내용을 읽어 드리는 어플리케이션이 있다. 파일이 존재하지 않는 예외의 처리 방법으로 사용자 입력을 다시 받게 하려면, 최상위 레이어가 예외를 처리해야하고 사이의 레이어들은 체이닝만 해야한다.

참고

https://howtodoinjava.com/best-practices/java-exception-handling-best-practices/

https://dzone.com/articles/9-best-practices-to-handle-exceptions-in-java

profile
편하게 읽기 좋은 단위의 포스트를 추구하는 개발자입니다

0개의 댓글