[Spring boot]예외처리 -2 (try-catch와 ExceptionHandler, ControllerAdvice)

GyeongEun Kim·2023년 5월 17일
0

본격적으로 예외를 처리하는 방법에 대해서 알아보자
먼저 가장 기본적인 try-catch문이 있다.

try-catch

try {
	addFriend();
} catch (FriendException e){
	System.out.println("이미 친구입니다");
}

try 블럭에는 시도할 함수 및 로직을 적어준다.
catch블럭에는 try에서 발생한 예외를 처리하는 코드를 적는다. 이때 매개변수로 어떤 예외를 catch할지 명시한다.
위 예시에서는 FriendException객체를 매개변수로 받고 있는데, 더 포괄적으로 Exception객체를 받을 수도 있다. 이것은 그러나 좋지 않은 방법이다.
구체적으로 어떤 예외가 발생했는지 알기 힘들고, 모든 예외에 대해 처리를 동일하게 해주어야 하기 떄문이다. 따라서 위와같이 구체적인 예외 객체를 적어주자.

finally

try-catch구문에 finally를 사용할 수도 있다. 필수는 아니고 선택사항이다.
finally 블럭에는 예외발생 여부와 상관없이 반드시 실행되어야할 구문을 적어준다.
예외가 발생하면, try->catch->finally순으로 실행되고
예외가 발생하지 않으면, try->finally순으로 실행된다.

그런데 try-catch문을 사용하면 가독성이 좋지 않고, 예외 종류가 많아짐에 따라 catch블럭이 많아지게 된다.
이런 불편함을 해결하기 위해 Spring에서는 @ExceptionHandler@ControllerAdvice 어노테이션을 제공한다.

ExceptionHandler

@ExceptionHandler는 @Controller 와 @RestController가 붙은 클래스에서 발생한 예외를 잡아 메서드 한곳에서 처리할 수 있도록 하는 기능을 한다.

//간소화된 코드입니다
@RestController
public class FriendController {

	@GetMapptin("/exception")
	public void generate() {
		friendService.generateException();
	}

@ExceptionHandler(NullPointerException.class)
public void handleException(NullPointerException e) {
	System.out.println("NPE 발생");
	}
}

@RestController어노테이션이 붙은 FriendController클래스가 있다. 그리고 genertate메서드에서 friendService클래스의 generateException메서드를 호출한다.
generateException메서드는 NullPointerException을 발생시킨다.
이를 @ExceptionHandler를 사용하여 처리하기 위해 handleException이라는 메서드를 만들었다.

이 메서드를 통해 FriendController에서 발생하는 모든 NullPointerException을 한꺼번에 처리할 수 있다.
지금은 편의상 generate라는 메서드 하나밖에 없지만, 실제 컨트롤러에는 많은 메서드들이 service레이어의 메서드를 호출할 것이다.
그곳에서 발생하는 모든 NPE들을 메서드 한곳에서 처리할 수 있는 것이다.

그런데 @ExceptionHandler@RestController@Controller가 붙은 빈에서 발생하는 예외를 처리해준다고 했는데 위 예시는 Service단에서 발생한 예외가 아닌가요? 라고 생각할 수 있다.
컨트롤러에서 서비스 레이어의 메서드를 호출하고, 호출된 메서드내에서 예외가 발생했으므로 함수 호출 스택의 가장 밑에 쌓여있는 것은 컨트롤러 레이어이다.
따라서 Controller에서 해당 메서드를 호출하여~ 예외가 발생했다고 할 수 있다.

그렇지만 아직도 불편한 점이 있다!
모든 컨트롤러마다 @ExceptionHandler를 사용하여 여러 메서드를 작성해야한다.

프로젝트 전역에서 발생하는 NPE를 모두 같은 방법으로 처리하고 싶다면 어떻게 할까? 현재로써는 각 컨트롤러마다 중복된 코드를 적어줘야만 한다.

그래서 @ControllerAdvice라는 것이 등장한다.

ControllerAdvice & RestControllerAdvice

ControllerAdvice 혹은 RestControllerAdvice를 사용하면 모든 컨트롤러에서 발생헐 수 있는 예외를 한곳에서 처리할 수 있다.

아래의 예시를 보자

@RestControllerAdvice
public class GlobalExceptionHandler {
	
@ExceptionHandler(NullPointerException.class)
public String NPE() {
		return "목록이 존재하지 않습니다";
}

@ExceptionHandler(DuplicateFriendException.class)
public ResponseEntity<ErrorResponse> DuplicateFriend (DuplicateFriendException e) {
	 return ErrorResponse.toResponse(e.getErrorCode()); 
}	

@ExceptionHandler(NoUserException.class)
public ResponseEntity<ErrorResponse> NoUser(NoUserException e) {
	return ErrorResponse.toResponse(e.getErrorCode());
}

}

@RestControllerAdvice가 붙은 GlobalExceptionHandler클래스가 있다.
이 클래스 내부에 위에서 @ExceptionHandler를 사용하여 예외처리를 해주었던 메소드들을 작성하면 된다.
여러 컨트롤러에 중복되게 작성할 필요없이 이 클래스에 한번만 작성하면 된다.

+) 위 예시에서는 사용자 정의 예외와 에러코드를 직접 만들어 사용하였다. 그리고 REST API를 사용하여 Response형식을 통일하기 위해 ErrorResponse라는 객체를 만들어 사용하였다.

public class FriendException extends RuntimeException{
    private ErrorCode errorCode;

    public FriendException(ErrorCode errorCode){
        super(errorCode.getMessage());
        //RuntimeException 생성자의 매개변수로 에러 메세지를 넘김
        this.errorCode = errorCode;
        
    }
}
public enum ErrorCode {

	ALREADY_FRIENDS(HttpStatus.BAD_REQUEST, "이미 친구입니다."),
	
	...

	UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "권한이 없습니다.");

	private final HttpStatus httpStatus;
	private final String message;

}
public class ErrorResponse {
	private HttpStatus httpStatus;
	private String message;

	public static ResponseEntity<ErrorResponse> toResponse(ErrorCode errorCode){
		return ResponseEntity
				.status(errorCode.getHttpStatus())
				.body(ErrorResponse.builder()
									.httpStatus(errorCode.getHttpStatus())
									.message(errorCode.getMessage())
									.build());
	}
}
profile
내가 보려고 쓰는 글

0개의 댓글