파란하늘의 지식창고
반응형

Exception을 사용하는 방법에 대한 글은 아니고 어떻게 쓰는게 좋은지에 대해 정리해보았다.

시대가 변하면 방법도 변하기 마련이다.

이 글의 내용이 정답은 아니고 다만 글을 쓴 시점에서 어떤게 가장 좋을지 정리해본 글이다.

과거 사용하던 에러 처리 방식 1

예전에는 아래와 같은 방식으로 Exception을 모두 감싸고 결과를 반환하는 형태로 개발하던 시절도 있었다.

//대상 객체
public class Article {
	// ... article 설정
}

// 결과 반환 객체
public class ArticleResult extends Article {
	boolean isSuccess;
	String errorCode;
	String errorMessage;
	// ... 에러 관련 설정
}

// 대상 서비스
public ArticleResult save(Article article) {
	// ... 저장하기 위한 프로세스 수행 에러가 나면 try catch 를 통해 해당 에러를 ArticleResult에 저장
	return ArticleResult;	// 결과를 담은 리턴 객체 반환
}

// 응답 결과
{
	"isSuccess" : true,
	"errorCode" : "SUCCESS",
	"errorMessage" : null,
	// article 변수들
}

절대 에러를 반환하지 않는다.

vi로 개발하던 시절 로그 처리에 대한 통합 관리도 되어 있지 않고 오픈 소스를 사용하다 문제가 있어도 버전 변경이 쉽지 않고 디버깅도 어려워 이런 형태의 개발을 많이 했다고 들었다. (오래전 레거시 프로젝트에도 간혹 보이긴한다..)

하지만 Result를 담는 객체를 매번 만들거나 또는 역으로 Result 정보를 담은 상위 객체를 extends해서 사용하는 형태로 개발을 하다보니 에러 관리와 domain 객체의 관리가 은근 귀찮은 방식이다.

또한 try catch로 도배된 코드는 의도한 기능보다 try catch 도배가 더 많아 본래의 기능을 파악하는데 많은 시간을 들여야하는  단점이 있다.

과거 사용하던 에러 처리 방식 2

Java 5 이후 (2004년 9월 30일 릴리즈) 엔 generic이 도입되어 두 벌로 관리하거나 상속하는 객체를 없애고 다음과 같은 형태로 사용하기도 했다.

public class Result<T> {
	private T t;	// 대상 객체가 여기에 지정되는 형태
	boolean isSuccess;
	String errorCode;
	String errorMessage;
	// ... 에러 관련 설정
}

// 대상 서비스
public Result<Article> save(Article article) {
	// ... 저장하기 위한 프로세스 수행
	return Result<Article>;	// 결과를 담은 리턴 객체 반환
}

// 응답 결과
{
	"isSuccess" : true,
	"errorCode" : "SUCCESS",
	"errorMessage" : null,
	"article" : {
		// article 변수들
	}
}

Result 객체를 관리하는 것보다 간결해지긴 했다.

논리적으론 아무 문제 없는 코드이다.

하지만 모든 에러를 처리하기 위해 try catch가 도배될 수 밖에 없어 코드 해석이 난해하긴 마찬가지다.

현재 사용하는 에러 처리 방식

위에 설명한 2가지 방식 이외에 다양한 방법의 에러 처리가 있지만 모두 건너 뛰고 현재 기준으로는 springframework에서 제공하는 GlobalExceptionHandler를 사용하고 있다.

Exception Handling in Spring MVC

@ControllerAdvice는 Controller에 대한 aop 처리를 지원하는 어노테이션이다. 

@ControllerAdvice는 spring 3.2 이후 지원되기 시작하였다. (2012년 12월 04일 릴리즈)

(그 전까진 Controller마다 개별 에러처리 선언을 해야했다. 그래서 상위 Controller로 통합 관리하고 그걸 매번 extends 하는 형태의 개발도 있었다.)

비슷한 시기에 유행이던 playframework도 해당 프레임워크에서 제공하는 Controller를 extends 받아 사용하는 형태여서 그 당시 그게 트렌드인건가 싶었다.

@ControllerAdvice에서 @ExceptionHandler를 통해 전 단계에서 발생한 에러는 모두 받아 처리를 한다.

동작하는 위치는 다음과 같다.

repository -> service -> controller -> (controllerAdvice) -> thymeleaf (html) or json 등등 반환 처리 -> servlet -> client


custom exception을 우선 정의한다. custom exception은 과거에도 동일하게 사용하던 부분이다.

@Data
@EqualsAndHashCode(callSuper = false)
public class BlueskyException extends RuntimeException {
	private static final long serialVersionUID = -2499198692880482249L;

	private final String errorCode;
	
	private String localizedMessage;
	
	private String errorPage = ErrorPage.DEFAULT;

	private String[] errorMessageArgs;
	
	public BlueskyException(String errorCode) {
		this.errorCode = errorCode;
	}
	
	public BlueskyException errorPage(String errorPage) {
		this.errorPage = errorPage;
		return this;
	}
	
	public BlueskyException(Enum<?> errorCode) {
		this.errorCode = resolveErrorCode(errorCode);
	}
	
	public BlueskyException setErrorMessageArgs(String... errorMessageArgs) {
		this.errorMessageArgs = errorMessageArgs;
		return this;
	}
}


해당 에러 로그를 그대로 front 까지 내보내면 불필요한 노출이 되기 때문에 적당하게 필요한 정보만 보여주는 에러 메세지 객체를 정의한다.

@Data
public class ErrorMessage {
	String errorCode;
	String exceptionClassName;
	boolean isDisplayableMessage = false;	//에러 메세지 화면 표시 가능여부
	private String message;
	String object;		//bindException의 경우 에러 발생 ObjectName을 전달, 보통의 경우 code를 전달
	private String field;
}


GlobalExceptionHandler를 통해 공통 에러처리를 정의한다.

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
	
	public static final String RESULT = "result";

	private DefaultMessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver();
	
	/**
	 * custom exception 처리
	**/
	@ExceptionHandler
	@SneakyThrows
	@ResponseStatus(value = HttpStatus.BAD_REQUEST)
	public ModelAndView handleException(BlueskyException exception, HandlerMethod  handlerMethod, NativeWebRequest request) {
		Map<String, ErrorMessage> resultMap = new HashMap<>();
		resultMap.put(RESULT, getErrorMessage(exception));
		return new ModelAndView(exception.getErrorPage(), resultMap);
	}

	// 특정한 에러 처리가 필요한 경우 명시 (ex: SecurityException, BindException, MethodArgumentNotValidException 등등..
	
	/**
	 * 기타 exception 처리
	**/
	@ExceptionHandler
	@ResponseStatus(value = HttpStatus.BAD_REQUEST)
	public ModelAndView handleException(Exception exception) {
		log.debug("error : {}", exception);
		Map<String, ErrorMessage> resultMap = new HashMap<>();
		resultMap.put(RESULT, getErrorMessage(exception));
		return new ModelAndView(ErrorPage.DEFAULT, resultMap);
	}

	private ErrorMessage getErrorMessage(Exception exception) {
		ErrorMessage errorMessage = new ErrorMessage();
		errorMessage.setExceptionClassName(exception.getClass().getSimpleName());
		if (!(exception instanceof BlueskyException)) {
			errorMessage.setMessage(MessageUtil.getMessage(exception.getClass().getSimpleName(), exception.getLocalizedMessage()));
			return errorMessage;
		}
		
		String targetErrorCode = ((BlueskyException) exception).getErrorCode();
		if (targetErrorCode == null || targetErrorCode.contains(" ")) {
			errorMessage.setMessage(targetErrorCode);
			return errorMessage;
		}
		
		String[] errorCodes = messageCodesResolver.resolveMessageCodes(exception.getClass().getSimpleName(), targetErrorCode);
		log.debug("[Exception error message] code : {}", Arrays.asList(errorCodes));
		DefaultMessageSourceResolvable defaultMessageSourceResolvable = new DefaultMessageSourceResolvable(errorCodes, ((BlueskyException) exception).getErrorMessageArgs(), targetErrorCode);
		String localizedMessage = MessageUtil.getMessage(defaultMessageSourceResolvable);
		errorMessage.setErrorCode(((BlueskyException) exception).getErrorCode());
		errorMessage.setMessage(localizedMessage);
		errorMessage.setDisplayableMessage(true);
		
		return errorMessage;
	}
}

대략적인 코드를 소개하였다.

Exception을 사용하는 규칙은 다음과 같다.

  • 시스템이 발생시키는 에러는 try catch를 통해 중간에 개입하지 않고 GlobalExceptionHandler까지 전파되도록 놔둔다.
    의도하지 않은 에러 발생 포인트까지 고려해 try catch를 써서 코드의 에러 처리 구문 비율을 높이는 것이 유지 보수에 더 안좋다고 판단하였고 또한 공통 에러 처리로 해당 처리를 위임하는 것이 더 간결하다.
  • 원래 작성한 흐름의 중간에 나와야 하는 경우 custom Exception을 사용한다.
    custom exception은 의도치 않은 예외 사항이 발생한 것이 아니고 exception이 발생한 상황 또한 흐름의 일부로 보고 개발을 한다.
    custom exception을 throw 할 때 같이 던진 errorCode를 통해 GlobalExceptionHandler에서 에러를 확인하고 다국어 처리를 한다.
  • 프론트에서는 http status가 200 인 경우 응답 성공, 그 외의 경우 설정된 errorMessage로 반환되는 것을 전제로 개발을 한다.

코드에 흐름과 상관없는 방어 코드로 도배가 되지 않고 간결함을 유지하기 위해 위와 같은 방식의 에러처리를 현재 사용하고 있다.

이 방법이 정답이 아닐 수도 있다.

다만 공통된 에러 처리를 aop로 위임하고 코드는 해당 기능에 대한 구현만 집중하여 간결한 코드가 되면 다른 사람이 봐도 쉽게 기능 파악을 할 수 있게 된다.

이런 방식의 에러 처리가 현재로선 가장 좋은 방법이 아닐까 싶다.

반응형
profile

파란하늘의 지식창고

@Bluesky_

내용이 유익했다면 광고 배너를 클릭 해주세요